mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
OC Path: add dry-run diff output (#81437)
* OC Path: add dry-run diff output * fix(oc-path): require dry-run for diff output * fix(oc-path): show final newline diff Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(oc-path): show line-ending-only dry-run diffs --------- Co-authored-by: Gio Della-Libera <giodl@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -85,6 +85,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/OpenRouter: stop adding empty DeepSeek V4 `reasoning_content` placeholders to assistant tool-call replay messages and strip empty replay artifacts before follow-up Chat Completions requests, so `openrouter/deepseek/deepseek-v4-pro` no longer fails after tool use. Fixes #82150. (#82158) Thanks @luyao618 and @Suquir0.
|
||||
- OpenAI-compatible providers: honor streaming-usage compatibility metadata when deciding whether to send `stream_options.include_usage`, while keeping bundled Volcengine routes opted in to Ark streaming usage. Refs #44845. (#82181) Thanks @xuruiray.
|
||||
- Gateway/approvals: treat `turnSourceTo` as optional in `canBridgeNoDeviceChatApprovalFromBackend`, matching the existing optional handling of `turnSourceAccountId` and `turnSourceThreadId`. Channels without a recipient concept (webchat, control-ui) leave `turnSourceTo` null on both the approval snapshot and the replay params, so the prior required-string check rejected every backend replay with `APPROVAL_CLIENT_MISMATCH`. Cross-channel replay is still gated by the required `turnSourceChannel` and `sessionKey` checks. Fixes #82132. (#82136) Thanks @ottodeng.
|
||||
- OC Path: add `openclaw path set --dry-run --diff` so addressed edits can be reviewed as a unified diff before writing.
|
||||
|
||||
- Cron: load runtime plugins before isolated cron model and delivery resolution so external channels can be selected for scheduled runs. (#82111) Thanks @medns.
|
||||
- Cron: mirror successful direct scheduled deliveries into the resolved destination session transcript while preserving isolated-delivery awareness policy. (#80786) Thanks @cavit99.
|
||||
- Cron: preserve rotated transcript identity after session-bound scheduled runs compact, so `sessionTarget: "current"` keeps the next user message on the same conversation. Fixes #82164. Thanks @weissfl.
|
||||
|
||||
+10
-3
@@ -135,6 +135,7 @@ matches you can inspect before choosing one to write.
|
||||
| `--json` | Force JSON output (default when stdout is not a TTY). |
|
||||
| `--human` | Force human output (default when stdout is a TTY). |
|
||||
| `--dry-run` | (only on `set`) print the bytes that would be written without writing. |
|
||||
| `--diff` | (with `set --dry-run`) print a unified diff instead of the full bytes. |
|
||||
|
||||
## `oc://` syntax
|
||||
|
||||
@@ -202,6 +203,8 @@ the per-kind AST shape.
|
||||
Use `--dry-run` before user-visible writes when the exact bytes matter. The
|
||||
substrate preserves byte-identical output for parse/emit round-trips, but a
|
||||
mutation can canonicalize the edited region or file depending on kind.
|
||||
Add `--diff` when you want the preview as a focused before/after patch instead
|
||||
of the full rendered file.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -218,6 +221,9 @@ openclaw path find 'oc://session.jsonl/*/event' --file ./logs/session.jsonl
|
||||
# Dry-run a write
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run
|
||||
|
||||
# Dry-run a write as a unified diff
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run --diff
|
||||
|
||||
# Apply the write
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0'
|
||||
|
||||
@@ -375,12 +381,13 @@ openclaw path find 'oc://config.jsonc/plugins/{github,slack}/enabled'
|
||||
### `set <oc-path> <value>`
|
||||
|
||||
Write a leaf. Pair with `--dry-run` to preview the bytes that would be
|
||||
written without touching the file. Exits `0` on a successful write, `1` if
|
||||
the substrate refuses (for example, a sentinel guard hit), `2` on parse
|
||||
errors.
|
||||
written without touching the file. Add `--diff` for a unified diff preview.
|
||||
Exits `0` on a successful write, `1` if the substrate refuses (for example, a
|
||||
sentinel guard hit), `2` on parse errors.
|
||||
|
||||
```bash
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run --diff
|
||||
openclaw path set 'oc://gateway.jsonc/version' '2.0'
|
||||
openclaw path set 'oc://AGENTS.md/Tools/+gh/risk' 'low'
|
||||
```
|
||||
|
||||
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
type OutputRuntimeEnv,
|
||||
formatUnifiedDiff,
|
||||
pathEmitCommand,
|
||||
pathFindCommand,
|
||||
pathResolveCommand,
|
||||
@@ -152,6 +153,92 @@ describe("openclaw path CLI", () => {
|
||||
expect(readFileSync(filePath, "utf-8")).toBe(before);
|
||||
});
|
||||
|
||||
it("CLI-S05 --dry-run --diff prints a unified diff", async () => {
|
||||
const filePath = join(workspaceDir, "gateway.jsonc");
|
||||
const before = '{\n "version": "1.0",\n "enabled": true\n}\n';
|
||||
writeFileSync(filePath, before, "utf-8");
|
||||
const rt = createTestRuntime();
|
||||
await pathSetCommand(
|
||||
"oc://gateway.jsonc/version",
|
||||
"2.0",
|
||||
{ cwd: workspaceDir, human: true, dryRun: true, diff: true },
|
||||
rt,
|
||||
);
|
||||
expect(rt.exitCode).toBe(0);
|
||||
const out = stdoutText(rt);
|
||||
expect(out).toContain("--- ");
|
||||
expect(out).toContain("+++ ");
|
||||
expect(out).toContain('- "version": "1.0",');
|
||||
expect(out).toContain('+ "version": "2.0",');
|
||||
expect(readFileSync(filePath, "utf-8")).toBe(before);
|
||||
});
|
||||
|
||||
it("CLI-S05b --dry-run --diff shows final newline-only byte changes", () => {
|
||||
const out = formatUnifiedDiff(
|
||||
"## Boundaries\n\n- timeout: 5\n",
|
||||
"## Boundaries\n\n- timeout: 5",
|
||||
"AGENTS.md",
|
||||
);
|
||||
expect(out).toContain("--- AGENTS.md");
|
||||
expect(out).toContain("@@ -1,4 +1,3 @@");
|
||||
expect(out).toContain("\n-\n");
|
||||
});
|
||||
|
||||
it("CLI-S05c --dry-run --diff shows line-ending-only byte changes", async () => {
|
||||
const filePath = join(workspaceDir, "AGENTS.md");
|
||||
const before = "---\r\nname: x\r\n---\r\n";
|
||||
writeFileSync(filePath, before, "utf-8");
|
||||
const rt = createTestRuntime();
|
||||
await pathSetCommand(
|
||||
"oc://AGENTS.md/[frontmatter]/name",
|
||||
"x",
|
||||
{ cwd: workspaceDir, json: true, dryRun: true, diff: true },
|
||||
rt,
|
||||
);
|
||||
expect(rt.exitCode).toBe(0);
|
||||
const out = JSON.parse(stdoutText(rt));
|
||||
expect(out.diff).toContain("-name: x\r");
|
||||
expect(out.diff).toContain("+name: x");
|
||||
expect(readFileSync(filePath, "utf-8")).toBe(before);
|
||||
});
|
||||
|
||||
it("CLI-S06 --dry-run --diff includes diff in JSON output", async () => {
|
||||
const filePath = join(workspaceDir, "gateway.jsonc");
|
||||
writeFileSync(filePath, '{ "version": "1.0" }', "utf-8");
|
||||
const rt = createTestRuntime();
|
||||
await pathSetCommand(
|
||||
"oc://gateway.jsonc/version",
|
||||
"2.0",
|
||||
{ cwd: workspaceDir, json: true, dryRun: true, diff: true },
|
||||
rt,
|
||||
);
|
||||
expect(rt.exitCode).toBe(0);
|
||||
const out = JSON.parse(stdoutText(rt));
|
||||
expect(out.dryRun).toBe(true);
|
||||
expect(out.bytes).toContain('"2.0"');
|
||||
expect(out.diff).toContain('-{ "version": "1.0" }');
|
||||
expect(out.diff).toContain('+{ "version": "2.0" }');
|
||||
});
|
||||
|
||||
it("CLI-S07 rejects --diff without --dry-run", async () => {
|
||||
const filePath = join(workspaceDir, "gateway.jsonc");
|
||||
const before = '{ "version": "1.0" }';
|
||||
writeFileSync(filePath, before, "utf-8");
|
||||
const rt = createTestRuntime();
|
||||
await pathSetCommand(
|
||||
"oc://gateway.jsonc/version",
|
||||
"2.0",
|
||||
{ cwd: workspaceDir, json: true, diff: true },
|
||||
rt,
|
||||
);
|
||||
expect(rt.exitCode).toBe(1);
|
||||
expect(JSON.parse(stdoutText(rt))).toMatchObject({
|
||||
ok: false,
|
||||
reason: "--diff requires --dry-run",
|
||||
});
|
||||
expect(readFileSync(filePath, "utf-8")).toBe(before);
|
||||
});
|
||||
|
||||
it("CLI-S03 sentinel-bearing value is refused at emit", async () => {
|
||||
const filePath = join(workspaceDir, "gateway.jsonc");
|
||||
writeFileSync(filePath, '{ "token": "x" }', "utf-8");
|
||||
|
||||
+154
-36
@@ -42,6 +42,7 @@ export interface PathCommandOptions {
|
||||
readonly cwd?: string;
|
||||
readonly file?: string;
|
||||
readonly dryRun?: boolean;
|
||||
readonly diff?: boolean;
|
||||
}
|
||||
|
||||
type OutputMode = "human" | "json";
|
||||
@@ -63,13 +64,19 @@ const defaultRuntime: OutputRuntimeEnv = {
|
||||
// Defense-in-depth: replace the redaction sentinel with `[REDACTED]`
|
||||
// before writing, even if upstream emits it.
|
||||
export function scrubSentinel(s: string): string {
|
||||
if (!s.includes(REDACTED_SENTINEL)) {return s;}
|
||||
if (!s.includes(REDACTED_SENTINEL)) {
|
||||
return s;
|
||||
}
|
||||
return s.split(REDACTED_SENTINEL).join(SCRUB_PLACEHOLDER);
|
||||
}
|
||||
|
||||
function detectMode(options: PathCommandOptions): OutputMode {
|
||||
if (options.json === true) {return "json";}
|
||||
if (options.human === true) {return "human";}
|
||||
if (options.json === true) {
|
||||
return "json";
|
||||
}
|
||||
if (options.human === true) {
|
||||
return "human";
|
||||
}
|
||||
return process.stdout.isTTY ? "human" : "json";
|
||||
}
|
||||
|
||||
@@ -116,11 +123,7 @@ function requireArg<T>(
|
||||
}
|
||||
|
||||
/** Parse an oc-path string; emit structured error and return null on failure. */
|
||||
function tryParse(
|
||||
pathStr: string,
|
||||
runtime: OutputRuntimeEnv,
|
||||
mode: OutputMode,
|
||||
): OcPath | null {
|
||||
function tryParse(pathStr: string, runtime: OutputRuntimeEnv, mode: OutputMode): OcPath | null {
|
||||
try {
|
||||
return parseOcPath(pathStr);
|
||||
} catch (err) {
|
||||
@@ -157,8 +160,12 @@ function catchSentinel<T>(
|
||||
async function loadAst(absPath: string, fileName: string): Promise<OcAst> {
|
||||
const raw = await fs.readFile(absPath, "utf-8");
|
||||
const kind = inferKind(fileName);
|
||||
if (kind === "jsonc") {return parseJsonc(raw).ast;}
|
||||
if (kind === "jsonl") {return parseJsonl(raw).ast;}
|
||||
if (kind === "jsonc") {
|
||||
return parseJsonc(raw).ast;
|
||||
}
|
||||
if (kind === "jsonl") {
|
||||
return parseJsonl(raw).ast;
|
||||
}
|
||||
return parseMd(raw).ast;
|
||||
}
|
||||
|
||||
@@ -177,7 +184,9 @@ function emitForKind(ast: OcAst, fileName?: string): string {
|
||||
}
|
||||
|
||||
function resolveFsPath(path: OcPath, options: PathCommandOptions): string {
|
||||
if (options.file !== undefined) {return resolvePath(options.file);}
|
||||
if (options.file !== undefined) {
|
||||
return resolvePath(options.file);
|
||||
}
|
||||
return resolvePath(options.cwd ?? process.cwd(), path.file);
|
||||
}
|
||||
|
||||
@@ -185,13 +194,72 @@ function formatMatchHuman(match: OcMatch): string {
|
||||
if (match.kind === "leaf") {
|
||||
return `leaf @ L${match.line}: ${JSON.stringify(match.valueText)} (${match.leafType})`;
|
||||
}
|
||||
if (match.kind === "node") {return `node @ L${match.line} [${match.descriptor}]`;}
|
||||
if (match.kind === "node") {
|
||||
return `node @ L${match.line} [${match.descriptor}]`;
|
||||
}
|
||||
if (match.kind === "insertion-point") {
|
||||
return `insertion-point @ L${match.line} [${match.container}]`;
|
||||
}
|
||||
return `root @ L${match.line}`;
|
||||
}
|
||||
|
||||
function splitDiffLines(s: string): readonly string[] {
|
||||
return s === "" ? [] : s.split("\n");
|
||||
}
|
||||
|
||||
export function formatUnifiedDiff(oldBytes: string, newBytes: string, fsPath: string): string {
|
||||
if (oldBytes === newBytes) {
|
||||
return "";
|
||||
}
|
||||
const oldLines = splitDiffLines(oldBytes);
|
||||
const newLines = splitDiffLines(newBytes);
|
||||
let prefix = 0;
|
||||
while (
|
||||
prefix < oldLines.length &&
|
||||
prefix < newLines.length &&
|
||||
oldLines[prefix] === newLines[prefix]
|
||||
) {
|
||||
prefix++;
|
||||
}
|
||||
|
||||
let oldSuffix = oldLines.length - 1;
|
||||
let newSuffix = newLines.length - 1;
|
||||
while (
|
||||
oldSuffix >= prefix &&
|
||||
newSuffix >= prefix &&
|
||||
oldLines[oldSuffix] === newLines[newSuffix]
|
||||
) {
|
||||
oldSuffix--;
|
||||
newSuffix--;
|
||||
}
|
||||
|
||||
const context = 3;
|
||||
const hunkStart = Math.max(0, prefix - context);
|
||||
const hunkOldEnd = Math.min(oldLines.length - 1, oldSuffix + context);
|
||||
const hunkNewEnd = Math.min(newLines.length - 1, newSuffix + context);
|
||||
const oldCount = Math.max(0, hunkOldEnd - hunkStart + 1);
|
||||
const newCount = Math.max(0, hunkNewEnd - hunkStart + 1);
|
||||
const lines = [
|
||||
`--- ${fsPath}`,
|
||||
`+++ ${fsPath}`,
|
||||
`@@ -${hunkStart + 1},${oldCount} +${hunkStart + 1},${newCount} @@`,
|
||||
];
|
||||
|
||||
for (let i = hunkStart; i < prefix; i++) {
|
||||
lines.push(` ${oldLines[i] ?? ""}`);
|
||||
}
|
||||
for (let i = prefix; i <= oldSuffix; i++) {
|
||||
lines.push(`-${oldLines[i] ?? ""}`);
|
||||
}
|
||||
for (let i = prefix; i <= newSuffix; i++) {
|
||||
lines.push(`+${newLines[i] ?? ""}`);
|
||||
}
|
||||
for (let i = Math.max(oldSuffix + 1, prefix); i <= hunkOldEnd; i++) {
|
||||
lines.push(` ${oldLines[i] ?? ""}`);
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
// ---------- Commands -----------------------------------------------------
|
||||
|
||||
export async function pathResolveCommand(
|
||||
@@ -200,9 +268,13 @@ export async function pathResolveCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(pathStr, "resolve: missing <oc-path> argument", runtime, mode)) {return;}
|
||||
if (!requireArg(pathStr, "resolve: missing <oc-path> argument", runtime, mode)) {
|
||||
return;
|
||||
}
|
||||
const ocPath = tryParse(pathStr, runtime, mode);
|
||||
if (ocPath === null) {return;}
|
||||
if (ocPath === null) {
|
||||
return;
|
||||
}
|
||||
const ast = await loadAst(resolveFsPath(ocPath, options), ocPath.file);
|
||||
let match: OcMatch | null;
|
||||
try {
|
||||
@@ -231,15 +303,34 @@ export async function pathSetCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(pathStr, "set: requires <oc-path> <value>", runtime, mode)) {return;}
|
||||
if (!requireArg(value, "set: requires <oc-path> <value>", runtime, mode)) {return;}
|
||||
if (!requireArg(pathStr, "set: requires <oc-path> <value>", runtime, mode)) {
|
||||
return;
|
||||
}
|
||||
if (!requireArg(value, "set: requires <oc-path> <value>", runtime, mode)) {
|
||||
return;
|
||||
}
|
||||
if (options.diff === true && options.dryRun !== true) {
|
||||
emit(
|
||||
runtime,
|
||||
mode,
|
||||
{ ok: false, reason: "--diff requires --dry-run" },
|
||||
() => "set failed: --diff requires --dry-run",
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const ocPath = tryParse(pathStr, runtime, mode);
|
||||
if (ocPath === null) {return;}
|
||||
if (ocPath === null) {
|
||||
return;
|
||||
}
|
||||
const fsPath = resolveFsPath(ocPath, options);
|
||||
const oldBytes = await fs.readFile(fsPath, "utf-8");
|
||||
const ast = await loadAst(fsPath, ocPath.file);
|
||||
|
||||
const result = catchSentinel("set", runtime, mode, () => setOcPath(ast, ocPath, value));
|
||||
if (result === null) {return;}
|
||||
if (result === null) {
|
||||
return;
|
||||
}
|
||||
if (!result.ok) {
|
||||
const detail = "detail" in result ? result.detail : undefined;
|
||||
emit(
|
||||
@@ -252,17 +343,21 @@ export async function pathSetCommand(
|
||||
return;
|
||||
}
|
||||
// Per-kind emit can still refuse the sentinel even after set succeeds.
|
||||
const newBytes = catchSentinel("emit", runtime, mode, () =>
|
||||
emitForKind(result.ast, ocPath.file),
|
||||
);
|
||||
if (newBytes === null) {return;}
|
||||
const newBytes = catchSentinel("emit", runtime, mode, () => emitForKind(result.ast, ocPath.file));
|
||||
if (newBytes === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun === true) {
|
||||
const diff = options.diff === true ? formatUnifiedDiff(oldBytes, newBytes, fsPath) : undefined;
|
||||
emit(
|
||||
runtime,
|
||||
mode,
|
||||
{ ok: true, dryRun: true, bytes: newBytes },
|
||||
() => `--dry-run: would write ${newBytes.length} bytes to ${fsPath}\n${newBytes}`,
|
||||
{ ok: true, dryRun: true, bytes: newBytes, ...(diff !== undefined ? { diff } : {}) },
|
||||
() =>
|
||||
diff !== undefined
|
||||
? diff || `--dry-run: no byte changes for ${fsPath}`
|
||||
: `--dry-run: would write ${newBytes.length} bytes to ${fsPath}\n${newBytes}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -281,9 +376,13 @@ export async function pathFindCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(patternStr, "find: missing <pattern> argument", runtime, mode)) {return;}
|
||||
if (!requireArg(patternStr, "find: missing <pattern> argument", runtime, mode)) {
|
||||
return;
|
||||
}
|
||||
const pattern = tryParse(patternStr, runtime, mode);
|
||||
if (pattern === null) {return;}
|
||||
if (pattern === null) {
|
||||
return;
|
||||
}
|
||||
// File-slot wildcards would silently ENOENT during readFile; reject.
|
||||
if (/[*?]/.test(pattern.file)) {
|
||||
emitError(
|
||||
@@ -307,7 +406,9 @@ export async function pathFindCommand(
|
||||
matches: matches.map((m) => ({ path: formatOcPath(m.path), match: m.match })),
|
||||
},
|
||||
() => {
|
||||
if (matches.length === 0) {return `0 matches for ${patternStr}`;}
|
||||
if (matches.length === 0) {
|
||||
return `0 matches for ${patternStr}`;
|
||||
}
|
||||
const plural = matches.length === 1 ? "" : "es";
|
||||
const lines = [`${matches.length} match${plural} for ${patternStr}:`];
|
||||
for (const m of matches) {
|
||||
@@ -316,7 +417,9 @@ export async function pathFindCommand(
|
||||
return lines.join("\n");
|
||||
},
|
||||
);
|
||||
if (matches.length === 0) {runtime.exit(1);}
|
||||
if (matches.length === 0) {
|
||||
runtime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function pathValidateCommand(
|
||||
@@ -325,7 +428,9 @@ export function pathValidateCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): void {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(pathStr, "validate: missing <oc-path> argument", runtime, mode)) {return;}
|
||||
if (!requireArg(pathStr, "validate: missing <oc-path> argument", runtime, mode)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ocPath = parseOcPath(pathStr);
|
||||
emit(
|
||||
@@ -345,10 +450,18 @@ export function pathValidateCommand(
|
||||
},
|
||||
() => {
|
||||
const lines = [`valid: ${pathStr}`, ` file: ${ocPath.file}`];
|
||||
if (ocPath.section !== undefined) {lines.push(` section: ${ocPath.section}`);}
|
||||
if (ocPath.item !== undefined) {lines.push(` item: ${ocPath.item}`);}
|
||||
if (ocPath.field !== undefined) {lines.push(` field: ${ocPath.field}`);}
|
||||
if (ocPath.session !== undefined) {lines.push(` session: ${ocPath.session}`);}
|
||||
if (ocPath.section !== undefined) {
|
||||
lines.push(` section: ${ocPath.section}`);
|
||||
}
|
||||
if (ocPath.item !== undefined) {
|
||||
lines.push(` item: ${ocPath.item}`);
|
||||
}
|
||||
if (ocPath.field !== undefined) {
|
||||
lines.push(` field: ${ocPath.field}`);
|
||||
}
|
||||
if (ocPath.session !== undefined) {
|
||||
lines.push(` session: ${ocPath.session}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
},
|
||||
);
|
||||
@@ -373,7 +486,9 @@ export async function pathEmitCommand(
|
||||
runtime: OutputRuntimeEnv,
|
||||
): Promise<void> {
|
||||
const mode = detectMode(options);
|
||||
if (!requireArg(fileArg, "emit: missing <file> argument", runtime, mode)) {return;}
|
||||
if (!requireArg(fileArg, "emit: missing <file> argument", runtime, mode)) {
|
||||
return;
|
||||
}
|
||||
const fsPath =
|
||||
options.file !== undefined
|
||||
? resolvePath(options.file)
|
||||
@@ -381,7 +496,9 @@ export async function pathEmitCommand(
|
||||
const fileName = fsPath.split(/[\\/]/).pop() ?? fileArg;
|
||||
const ast = await loadAst(fsPath, fileName);
|
||||
const bytes = catchSentinel("emit", runtime, mode, () => emitForKind(ast, fileName));
|
||||
if (bytes === null) {return;}
|
||||
if (bytes === null) {
|
||||
return;
|
||||
}
|
||||
if (mode === "json") {
|
||||
runtime.writeStdout(scrubSentinel(JSON.stringify({ ok: true, kind: ast.kind, bytes })));
|
||||
return;
|
||||
@@ -429,7 +546,8 @@ export function registerPathCli(program: Command): void {
|
||||
.description("Write a leaf value at an oc:// path")
|
||||
.argument("<oc-path>", "oc:// path to write")
|
||||
.argument("<value>", "string value to write")
|
||||
.option("--dry-run", "Print bytes without writing"),
|
||||
.option("--dry-run", "Print bytes without writing")
|
||||
.option("--diff", "With --dry-run, print a unified diff instead of full bytes"),
|
||||
).action(async (pathStr: string, value: string, opts: PathCommandOptions) => {
|
||||
await pathSetCommand(pathStr, value, opts, defaultRuntime);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user