omgbase 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { changesSince } from "@omgbase/core";
3
- import { CliUsageError, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
3
+ import { CliUsageError, truncationFooter, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { remoteCall } from "./_remote.js";
5
5
  // `omg log` (11 §5.5) — changes_since, one digest summary per line. Relative
6
6
  // --since (24h/7d) is resolved to a literal ISO timestamp client-side, then to
@@ -23,9 +23,12 @@ function relativeToIso(since) {
23
23
  const ms = unit === "h" ? 3600e3 : unit === "d" ? 86400e3 : unit === "w" ? 7 * 86400e3 : 30 * 86400e3;
24
24
  return new Date(Date.now() - n * ms).toISOString();
25
25
  }
26
- // Absolute timestamp: pass through (Date normalizes).
26
+ // Absolute timestamp: pass through (Date normalizes). Anything else is the
27
+ // user's mistake, not "the epoch" (spec/cli §6 `log`).
27
28
  const t = Date.parse(since);
28
- return Number.isNaN(t) ? new Date(0).toISOString() : new Date(t).toISOString();
29
+ if (Number.isNaN(t))
30
+ throw new CliUsageError(`bad --since '${since}' (use a relative age like 24h or 7d, or an ISO timestamp)`);
31
+ return new Date(t).toISOString();
29
32
  }
30
33
  async function runLog(cli, args) {
31
34
  const { values } = parseArgs({
@@ -76,21 +79,8 @@ async function runLog(cli, args) {
76
79
  opts.origin = values.origin;
77
80
  result = changesSince(ws.store, repo.repoId, opts);
78
81
  }
79
- if (cli.flags.mode === "json") {
80
- cli.io.out(JSON.stringify(result));
81
- return EXIT_OK;
82
- }
83
- if (cli.flags.mode === "jsonl") {
84
- for (const d of result.digests)
85
- cli.io.out(JSON.stringify(d));
86
- if (result.truncated)
87
- truncationFooter(cli.io, cli.style, result.cursor);
88
- return EXIT_OK;
89
- }
90
- if (cli.flags.mode === "ids") {
91
- for (const d of result.digests)
92
- cli.io.out(d.commit);
93
- return EXIT_OK;
82
+ if (cli.flags.mode !== "human") {
83
+ return emitMachine(cli, result, { items: result.digests, ids: result.digests.map((d) => d.commit), cursor: result.truncated ? result.cursor : null });
94
84
  }
95
85
  const { style, io } = cli;
96
86
  if (result.digests.length === 0) {
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { docsList } from "@omgbase/core";
3
3
  import { columns } from "../render.js";
4
- import { EXIT_OK, renderHelp } from "../output.js";
4
+ import { EXIT_OK, emitMachine, renderHelp } from "../output.js";
5
5
  import { remoteCall } from "./_remote.js";
6
6
  // `omg ls [glob]` (11 §5.2) — live documents: path, block count, last-commit
7
7
  // time. glob is a simple prefix/suffix match on the path (SQL LIKE with * → %).
@@ -39,20 +39,8 @@ async function runLs(cli, args) {
39
39
  cursor = page.truncated ? page.cursor : null;
40
40
  } while (cursor);
41
41
  cli.capture?.(rows); // shell: docs become the addressable frame (ref = path)
42
- if (cli.flags.mode === "ids") {
43
- for (const r of rows)
44
- cli.io.out(r.path);
45
- return EXIT_OK;
46
- }
47
- if (cli.flags.mode === "json") {
48
- cli.io.out(JSON.stringify(rows));
49
- return EXIT_OK;
50
- }
51
- if (cli.flags.mode === "jsonl") {
52
- for (const r of rows)
53
- cli.io.out(JSON.stringify(r));
54
- return EXIT_OK;
55
- }
42
+ if (cli.flags.mode !== "human")
43
+ return emitMachine(cli, rows, { items: rows, ids: rows.map((r) => r.path) });
56
44
  const { style, io } = cli;
57
45
  if (rows.length === 0) {
58
46
  io.err(style.dim(" no documents"));
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
- import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError, pinClock, sequentialMinter, setIdMinter } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
2
+ import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError } from "@omgbase/core";
3
+ import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
+ import { installSpecSeams } from "../seams.js";
4
5
  import { loadEmbedding } from "./_embed.js";
5
6
  import { openRepoSource } from "./_source.js";
6
7
  // `omg mcp [--no-watch]` (11 §5.8) — MCP server on stdio; the host owns the
@@ -11,38 +12,18 @@ import { openRepoSource } from "./_source.js";
11
12
  // CRITICAL: on stdio, stdout is the MCP protocol channel. Nothing may write to
12
13
  // stdout here — every diagnostic goes to stderr (cli.io.err).
13
14
  /**
14
- * The two conformance seams of spec/surface §7.1, read from the environment
15
- * BEFORE the workspace opens (so no id is minted and no timestamp read without
16
- * them). For conformance runs only — never set in production:
17
- *
18
- * OMGBASE_SPEC_MINTER=sequential the fixture minter of spec/store §2.2 for
19
- * the whole process (`d_0, d_1, …`, each
20
- * prefix from 0); any other value is a
21
- * startup error
22
- * OMGBASE_SPEC_CLOCK=<RFC 3339> "now" is that instant for the whole
23
- * process: every commit a tool stamps, every
24
- * `ts` reported as current
25
- *
26
- * Both are announced on stderr (stdout is the protocol channel).
15
+ * Announce the two conformance seams of spec/surface §7.1 on stderr (stdout is
16
+ * the protocol channel). They are installed for every verb at the entry point
17
+ * (`seams.ts`, spec/cli §2.6) — `mcp` only reports them, so a conformance run
18
+ * is visibly not a production one.
27
19
  */
28
- function installSpecSeams(cli) {
29
- const minter = process.env.OMGBASE_SPEC_MINTER;
30
- if (minter !== undefined && minter !== "") {
31
- if (minter !== "sequential") {
32
- throw new CliUsageError(`OMGBASE_SPEC_MINTER=${JSON.stringify(minter)}: the only value is "sequential" (spec/surface §7.1)`, "unset it, or set OMGBASE_SPEC_MINTER=sequential for a conformance run");
33
- }
34
- setIdMinter(sequentialMinter());
20
+ function announceSpecSeams(cli) {
21
+ const seams = installSpecSeams();
22
+ if (seams.minter) {
35
23
  cli.io.err(cli.style.dim("[mcp] spec seam: sequential id minter (OMGBASE_SPEC_MINTER=sequential) — conformance run, not for production"));
36
24
  }
37
- const clock = process.env.OMGBASE_SPEC_CLOCK;
38
- if (clock !== undefined && clock !== "") {
39
- try {
40
- pinClock(clock);
41
- }
42
- catch {
43
- throw new CliUsageError(`OMGBASE_SPEC_CLOCK=${JSON.stringify(clock)}: not an RFC 3339 instant (spec/surface §7.1)`, "e.g. OMGBASE_SPEC_CLOCK=2026-09-27T00:00:00.000Z");
44
- }
45
- cli.io.err(cli.style.dim(`[mcp] spec seam: clock pinned to ${new Date().toISOString()} (OMGBASE_SPEC_CLOCK) — conformance run, not for production`));
25
+ if (seams.clock !== null) {
26
+ cli.io.err(cli.style.dim(`[mcp] spec seam: clock pinned to ${seams.clock} (OMGBASE_SPEC_CLOCK) — conformance run, not for production`));
46
27
  }
47
28
  }
48
29
  async function runMcp(cli, args) {
@@ -64,7 +45,7 @@ async function runMcp(cli, args) {
64
45
  notes: [`host config: {"command": "${cli.prog}", "args": ["mcp", "-C", "/path/to/notes"]}`],
65
46
  });
66
47
  }
67
- installSpecSeams(cli);
48
+ announceSpecSeams(cli);
68
49
  // The host launches us from its own cwd, so a missing workspace here almost
69
50
  // always means the host config lacks `-C <dir>` — say so, rather than the
70
51
  // generic "run init" hint (which would create an empty workspace in the wrong place).
@@ -13,6 +13,20 @@ function block(cli, ws, repoId, ref) {
13
13
  throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
14
14
  return r.blockId;
15
15
  }
16
+ // `done` addresses task blocks only (spec/cli §6): any other live block type is
17
+ // a type_mismatch before an op is built — the macro itself would record a no-op
18
+ // `edited` commit (its `attrs.checked` only rewrites a task's marker).
19
+ function taskBlock(ws, ref, blockId) {
20
+ const row = ws.store.db
21
+ .prepare("SELECT type FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
22
+ .get(blockId);
23
+ if (!row)
24
+ throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
25
+ if (row.type !== "task") {
26
+ throw new EngineErrorLike("type_mismatch", `not a task: ${ref} is a ${row.type}`, { data: { block: blockId, type: row.type } });
27
+ }
28
+ return blockId;
29
+ }
16
30
  function rawHashOf(ws, blockId) {
17
31
  const row = ws.store.db
18
32
  .prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
@@ -87,7 +101,7 @@ async function runInsert(cli, args) {
87
101
  }
88
102
  const to = positionals[0];
89
103
  if (!to)
90
- throw new CliUsageError("insert requires a <to> parent (block id, or a heading id for section append)");
104
+ throw new CliUsageError("insert requires a <to> parent (a container block id, or a doc id/path for the top level)");
91
105
  const markdown = readContent(content);
92
106
  // The destination-parent CAS (spec/mutate §1.2) rides as the op-level expect.
93
107
  const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
@@ -238,7 +252,7 @@ async function runDone(cli, args) {
238
252
  return runOpsRemote(cli, "tasks_complete", { blocks: refs, ...(values.undo ? { checked: false } : {}) });
239
253
  const ws = cli.workspace();
240
254
  const repo = cli.repo(ws);
241
- const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
255
+ const blocks = refs.map((r) => taskBlock(ws, r, block(cli, ws, repo.repoId, r)));
242
256
  const ops = values.undo
243
257
  ? blocks.map((b) => ({ op: "update", block: b, attrs: { checked: false }, expect: pinned(ws, b) }))
244
258
  : tasksComplete(ws.store, blocks);
@@ -301,7 +315,14 @@ async function runSplit(cli, args) {
301
315
  throw new CliUsageError("split requires a <block>");
302
316
  if (!values.at)
303
317
  throw new CliUsageError("split requires --at n[,n…]");
304
- const at = values.at.split(",").map((s) => Number(s.trim()));
318
+ // Offsets are integers (spec/mutate §2.5); anything else is a usage error
319
+ // here, not a NaN that reaches the kernel.
320
+ const at = values.at.split(",").map((s) => {
321
+ const n = Number(s.trim());
322
+ if (s.trim() === "" || !Number.isInteger(n))
323
+ throw new CliUsageError(`bad --at '${values.at}' (offsets must be integers: n[,n…])`);
324
+ return n;
325
+ });
305
326
  if (cli.flags.server)
306
327
  return runOpsRemote(cli, "blocks_split", { block: ref, at });
307
328
  const ws = cli.workspace();
@@ -28,8 +28,12 @@ async function runNode(cli, args) {
28
28
  });
29
29
  }
30
30
  if (sub === "set") {
31
- parseArgs({ args: rest, allowPositionals: true, options: { actor: { type: "string" } } });
32
- const [nodeId, prop, ...valueParts] = rest.filter((a) => !a.startsWith("--"));
31
+ // The value is every positional after <prop>, joined by a space; --actor
32
+ // (an option WITH a value) is taken out by the parser, never by a prefix
33
+ // filter — that filter used to leave the actor's value inside the property
34
+ // value (`checked "true human:spec"`), which the task editor read as false.
35
+ const { values, positionals } = parseArgs({ args: rest, allowPositionals: true, options: { actor: { type: "string" } } });
36
+ const [nodeId, prop, ...valueParts] = positionals;
33
37
  if (!nodeId || !prop || valueParts.length === 0)
34
38
  throw new CliUsageError("node set <nodeId> <prop> <value>");
35
39
  const value = valueParts.join(" ");
@@ -39,7 +43,7 @@ async function runNode(cli, args) {
39
43
  const ws = cli.workspace();
40
44
  const repo = cli.repo(ws);
41
45
  const ops = nodeSet(ws.store, nodeId, prop, value);
42
- return runOps(cli, ws, repo, ops, { reason: `node_set ${prop}` });
46
+ return runOps(cli, ws, repo, ops, { reason: `node_set ${prop}`, ...(values.actor ? { actor: values.actor } : {}) });
43
47
  }
44
48
  if (sub === "props") {
45
49
  // `props` is a local editability lookup with no remote tool (a niche read).
@@ -1,2 +1,12 @@
1
+ import { type OqxResult } from "@omgbase/core";
1
2
  import type { Command } from "../commands.js";
3
+ import type { Cli } from "../context.js";
4
+ /**
5
+ * Render an `OqxResult` in the current mode (shared with `run`). `--json` is the
6
+ * whole result verbatim (incl. consumer + any scalar), so count/exists round-trip
7
+ * without special-casing; `--jsonl` streams the hits (or the values, as JSON);
8
+ * `--ids` the hit ids — a scalar result has neither, so those modes print the
9
+ * document. The truncation footer prints in every mode but `--json`.
10
+ */
11
+ export declare function renderOqxResult(cli: Cli, result: OqxResult): number;
2
12
  export declare const cmdOqx: Command;
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
4
- import { truncationFooter, EngineErrorLike, EXIT_OK, renderHelp, renderHits } from "../output.js";
4
+ import { truncationFooter, CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp, renderHits } from "../output.js";
5
5
  import { loadEmbedding } from "./_embed.js";
6
6
  import { remoteCall } from "./_remote.js";
7
7
  import { readStdin } from "./_mutate.js";
@@ -85,10 +85,8 @@ async function runOqx(cli, args) {
85
85
  const source = values.file
86
86
  ? (values.file === "-" ? readStdin() : readFileSync(values.file, "utf8"))
87
87
  : positionals.join(" ").trim();
88
- if (!source) {
89
- cli.io.err(cli.style.dim(" usage: query <source> (or -f file|-)"));
90
- return EXIT_OK;
91
- }
88
+ if (!source)
89
+ throw new CliUsageError("query requires a <source> (or -f file|-)");
92
90
  const opts = {};
93
91
  if (values.n)
94
92
  opts.limit = Number(values.n);
@@ -135,11 +133,24 @@ async function runOqx(cli, args) {
135
133
  : result.consumer === "exists" ? result.exists
136
134
  : result.consumer === "none" ? result.none
137
135
  : result.values ?? result);
138
- // JSON emits the whole result verbatim (incl. consumer + any scalar), so
139
- // count/exists round-trip without special-casing.
140
- if (cli.flags.mode === "json") {
141
- cli.io.out(JSON.stringify(result));
142
- return EXIT_OK;
136
+ return renderOqxResult(cli, result);
137
+ }
138
+ /**
139
+ * Render an `OqxResult` in the current mode (shared with `run`). `--json` is the
140
+ * whole result verbatim (incl. consumer + any scalar), so count/exists round-trip
141
+ * without special-casing; `--jsonl` streams the hits (or the values, as JSON);
142
+ * `--ids` the hit ids — a scalar result has neither, so those modes print the
143
+ * document. The truncation footer prints in every mode but `--json`.
144
+ */
145
+ export function renderOqxResult(cli, result) {
146
+ const cursor = result.truncated ? result.cursor ?? "" : null;
147
+ if (cli.flags.mode !== "human") {
148
+ const list = result.values ?? (result.consumer === "collect" || result.consumer === "first" || result.consumer === "single" ? result.hits : undefined);
149
+ return emitMachine(cli, result, {
150
+ ...(list ? { items: list } : {}),
151
+ ...(list && !result.values ? { ids: result.hits.map((h) => h.id) } : {}),
152
+ cursor,
153
+ });
143
154
  }
144
155
  // Scalar consumers (count/exists/none) have no hits — render the reduction itself.
145
156
  if (result.consumer === "count" || result.consumer === "exists" || result.consumer === "none") {
@@ -148,26 +159,12 @@ async function runOqx(cli, args) {
148
159
  return EXIT_OK;
149
160
  }
150
161
  // A `values` projection has no hits — one bare value per line (strings
151
- // verbatim, anything else as JSON; `--jsonl` is JSON throughout).
162
+ // verbatim, anything else as JSON).
152
163
  if (result.values) {
153
164
  for (const v of result.values)
154
- cli.io.out(typeof v === "string" && cli.flags.mode !== "jsonl" ? v : JSON.stringify(v));
155
- if (result.truncated)
156
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
157
- return EXIT_OK;
158
- }
159
- if (cli.flags.mode === "ids") {
160
- for (const h of result.hits)
161
- cli.io.out(h.id);
162
- if (result.truncated)
163
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
164
- return EXIT_OK;
165
- }
166
- if (cli.flags.mode === "jsonl") {
167
- for (const h of result.hits)
168
- cli.io.out(JSON.stringify(h));
169
- if (result.truncated)
170
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
165
+ cli.io.out(typeof v === "string" ? v : JSON.stringify(v));
166
+ if (cursor !== null)
167
+ truncationFooter(cli.io, cli.style, cursor);
171
168
  return EXIT_OK;
172
169
  }
173
170
  const { style, io } = cli;
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { findDoc, docsOutline } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
3
+ import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { remoteCall } from "./_remote.js";
5
5
  // `omg outline <doc|path>` (alias ol) — the wire format (06 §6). Human output
6
6
  // prints the outline text with full block ids inline; --json emits the
@@ -11,7 +11,6 @@ async function runOutline(cli, args) {
11
11
  allowPositionals: true,
12
12
  options: {
13
13
  depth: { type: "string" },
14
- section: { type: "string" },
15
14
  skeleton: { type: "boolean" },
16
15
  help: { type: "boolean" },
17
16
  },
@@ -20,10 +19,9 @@ async function runOutline(cli, args) {
20
19
  return renderHelp(cli, {
21
20
  name: "outline",
22
21
  summary: "A document's outline with block ids inline (frozen wire format) — the orientation view",
23
- usage: "outline <doc|path> [--depth <n>] [--section <locator>] [--skeleton]",
22
+ usage: "outline <doc|path> [--depth <n>] [--skeleton]",
24
23
  options: [
25
24
  ["--depth <n>", "limit heading depth"],
26
- ["--section <locator>", "outline only the section at a locator"],
27
25
  ["--skeleton", "structure only, no text"],
28
26
  ],
29
27
  });
@@ -57,10 +55,9 @@ async function runOutline(cli, args) {
57
55
  result = docsOutline(ws.store, info.docId, opts);
58
56
  header = info.path;
59
57
  }
60
- if (cli.flags.mode !== "human") {
61
- cli.io.out(JSON.stringify(result));
62
- return EXIT_OK;
63
- }
58
+ // `{ text, truncated }` carries neither a list nor ids: every machine mode is the document.
59
+ if (cli.flags.mode !== "human")
60
+ return emitMachine(cli, result, { cursor: result.truncated ? "budget" : null });
64
61
  const { render, style, io } = cli;
65
62
  io.out(render.wordmark(header));
66
63
  io.out(render.rule(40));
@@ -1,11 +1,12 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { linksRetarget } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
- import { runOps } from "./_mutate.js";
4
+ import { runOps, unifiedDiffLines } from "./_mutate.js";
5
5
  import { remoteCall } from "./_remote.js";
6
6
  // `omg retarget <from> <to> [--scope glob] [--apply]` (11 §5.6) — plan-by-default.
7
- // Without --apply it runs the dry-run and prints the per-block diffs (the 06 §4
8
- // "always dry-run first" contract, encoded as the default). --apply commits.
7
+ // Without --apply it runs the dry-run and prints, per hit block, the unified
8
+ // diff of its raw (the same diff every other preview prints — the 06 §4 "always
9
+ // dry-run first" contract, encoded as the default). --apply commits.
9
10
  async function runRetarget(cli, args) {
10
11
  const { values, positionals } = parseArgs({
11
12
  args,
@@ -65,27 +66,11 @@ function renderPlan(cli, from, to, hits) {
65
66
  io.err(style.dim(` plan — ${hits.length} block(s) would change; re-run with --apply to commit`));
66
67
  for (const h of hits) {
67
68
  io.out(style.id(h.block));
68
- for (const line of lineDiff(h.oldRaw, h.newRaw)) {
69
- io.out(line.startsWith("+") ? style.ok(line) : line.startsWith("-") ? style.err(line) : style.dim(line));
70
- }
69
+ for (const line of unifiedDiffLines(cli, h.oldRaw, h.newRaw))
70
+ io.out(line);
71
71
  io.out("");
72
72
  }
73
73
  return EXIT_OK;
74
74
  }
75
- function lineDiff(before, after) {
76
- const a = before.split("\n");
77
- const b = after.split("\n");
78
- const out = [];
79
- const max = Math.max(a.length, b.length);
80
- for (let i = 0; i < max; i++) {
81
- if (a[i] === b[i])
82
- continue;
83
- if (a[i] !== undefined)
84
- out.push(`- ${a[i]}`);
85
- if (b[i] !== undefined)
86
- out.push(`+ ${b[i]}`);
87
- }
88
- return out;
89
- }
90
75
  export const cmdRetarget = { name: "retarget", summary: "Rewrite a link target (plan-by-default)", run: (c, a) => runRetarget(c, a) };
91
76
  //# sourceMappingURL=retarget.js.map
@@ -1,7 +1,8 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { resolveRef, findDoc, loadDocBlocks, blockRaw, oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp, renderHits } from "../output.js";
3
+ import { CliUsageError, EngineErrorLike, renderHelp } from "../output.js";
4
4
  import { loadEmbedding } from "./_embed.js";
5
+ import { renderOqxResult } from "./oqx.js";
5
6
  // `omg run <locator|path>` (11 §5.3) — evaluate the first ```omg fence in a doc
6
7
  // (or the fence at a block locator) and print its results. The fence body is an
7
8
  // OQX source string (the same language `omg query` runs). Strictly read-and-print:
@@ -98,37 +99,9 @@ async function runRun(cli, args) {
98
99
  else {
99
100
  result = oqxRun(ws.store, repo.repoId, source);
100
101
  }
101
- if (cli.flags.mode === "json") {
102
- cli.io.out(JSON.stringify(result));
103
- return EXIT_OK;
104
- }
105
- if (result.consumer === "count" || result.consumer === "exists" || result.consumer === "none") {
106
- cli.io.out(result.consumer === "count" ? String(result.count) : result.consumer === "exists" ? String(result.exists) : String(result.none));
107
- return EXIT_OK;
108
- }
109
- if (result.values) {
110
- for (const v of result.values)
111
- cli.io.out(typeof v === "string" ? v : JSON.stringify(v));
112
- if (result.truncated)
113
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
114
- return EXIT_OK;
115
- }
116
- if (cli.flags.mode === "ids") {
117
- for (const h of result.hits)
118
- cli.io.out(h.id);
119
- return EXIT_OK;
120
- }
121
- const { style, io } = cli;
122
- if (result.hits.length === 0) {
123
- io.err(style.dim(" no hits"));
124
- return EXIT_OK;
125
- }
126
- // Same renderer as `omg query`: `<id> <path>` lines, or a column table when
127
- // the fence projects (a selected `text` shows as its first line).
128
- renderHits(cli, result.hits);
129
- if (result.truncated)
130
- truncationFooter(io, style, result.cursor ?? "");
131
- return EXIT_OK;
102
+ // Same renderer as `omg query`, in every mode: `<id> <path>` lines, or a
103
+ // column table when the fence projects (a selected `text` shows as its first line).
104
+ return renderOqxResult(cli, result);
132
105
  }
133
106
  export const cmdRun = { name: "run", summary: "Evaluate an ```omg fence (OQX, inert, read-only)", run: (c, a) => runRun(c, a) };
134
107
  //# sourceMappingURL=run.js.map
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
- import { resolveRef, nodesGet, findDoc, docLinks, historyNode, docPropertiesMerged } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
2
+ import { resolveRef, nodesGet, docsRead, docLinks, historyNode, docPropertiesMerged } from "@omgbase/core";
3
+ import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { expandBlockArgs } from "./_mutate.js";
5
5
  function showOne(cli, ref, include) {
6
6
  const ws = cli.workspace();
@@ -9,11 +9,15 @@ function showOne(cli, ref, include) {
9
9
  if (!resolved)
10
10
  throw new EngineErrorLike("doc_missing", `no node ${ref}`);
11
11
  if (resolved.kind === "document") {
12
- const info = findDoc(ws.store, { docId: resolved.docId });
12
+ // The card reads the merged properties + open edges; `--json` is the
13
+ // `read_ref` result for a document (`{ kind: "document", ...docs_read }`).
14
+ const res = docsRead(ws.store, resolved.docId);
15
+ if (!res)
16
+ throw new EngineErrorLike("doc_missing", `no document ${ref}`);
13
17
  const links = docLinks(ws.store, resolved.docId, { direction: "both" });
14
18
  const properties = docPropertiesMerged(ws.store.db, resolved.docId);
15
- const payload = { kind: "document", id: info.docId, path: info.path, properties, edges: links };
16
- return { payload, render: () => renderDocCard(cli, info.path, properties, links) };
19
+ const payload = { kind: "document", ...res };
20
+ return { payload, render: () => renderDocCard(cli, res.path, properties, links) };
17
21
  }
18
22
  const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution: "full" });
19
23
  if (!node)
@@ -47,14 +51,9 @@ function runShow(cli, args) {
47
51
  // One ref keeps the single-entity shape (→ @_, frame intact); several become a list.
48
52
  const single = cards.length === 1 ? cards[0] : null;
49
53
  cli.capture?.(single ? single.payload : cards.map((c) => c.payload));
50
- if (cli.flags.mode === "json") {
51
- cli.io.out(JSON.stringify(single ? single.payload : cards.map((c) => c.payload)));
52
- return EXIT_OK;
53
- }
54
54
  if (cli.flags.mode !== "human") {
55
- for (const c of cards)
56
- cli.io.out(JSON.stringify(c.payload));
57
- return EXIT_OK;
55
+ const docs = cards.map((c) => c.payload);
56
+ return single ? emitMachine(cli, single.payload) : emitMachine(cli, docs, { items: docs });
58
57
  }
59
58
  for (const c of cards)
60
59
  c.render();
@@ -6,7 +6,7 @@ import { createSource, deleteSource, listSources, getSourceByName, attachSourceT
6
6
  import { columns } from "../render.js";
7
7
  import { drainEmbeddings } from "./_embed.js";
8
8
  import { ensureFsAdapter, FS_ADAPTER } from "./_source.js";
9
- import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
9
+ import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
10
10
  // `omg source` (ADR-014) — the source registry: where a repo's bytes come from
11
11
  // (a filesystem directory today; git/S3/etc. via adapters later). A repo owns
12
12
  // identity + history; adding a filesystem source points it at a directory and
@@ -101,14 +101,16 @@ async function runAdd(cli, args) {
101
101
  if (getSourceByName(ws.store, sourceName))
102
102
  throw new EngineErrorLike("path_taken", `a source named '${sourceName}' already exists`);
103
103
  // Consent gate: -y proceeds; a TTY prompts with a live file count; a non-TTY
104
- // without -y refuses rather than absorbing the tree silently.
104
+ // without -y is a usage error (the script forgot the flag; exit 2) rather
105
+ // than a silent absorption of the tree — or a silent success.
105
106
  if (!values.yes) {
107
+ if (!process.stdin.isTTY || !cli.io.stdoutTTY) {
108
+ throw new CliUsageError(`refusing without -y (would ingest files under ${shortenHome(abs)})`, "there is no TTY to confirm on; pass -y to consent to the ingest");
109
+ }
106
110
  const { ok } = await confirmIngestWithCount(cli, abs, slug);
107
- if (cli.io.stdoutTTY)
108
- process.stderr.write("\n");
111
+ process.stderr.write("\n");
109
112
  if (!ok) {
110
- const why = !process.stdin.isTTY || !cli.io.stdoutTTY ? `refusing without -y (would ingest files under ${shortenHome(abs)})` : "cancelled";
111
- cli.io.err(cli.style.dim(` ${why}`));
113
+ cli.io.err(cli.style.dim(" cancelled"));
112
114
  return EXIT_OK;
113
115
  }
114
116
  }
@@ -133,10 +135,8 @@ async function runAdd(cli, args) {
133
135
  return confirmTTY(cli, `embed ${pending} block(s) now via ${where}?`);
134
136
  },
135
137
  });
136
- if (cli.flags.mode !== "human") {
137
- cli.io.out(JSON.stringify({ repo: slug, repoId, source: sourceName, root: abs, ingested: swept.ingested.length, ...(drained ? { embedded: drained.embedded } : {}) }));
138
- return EXIT_OK;
139
- }
138
+ if (cli.flags.mode !== "human")
139
+ return emitMachine(cli, { repo: slug, repoId, source: sourceName, root: abs, ingested: swept.ingested.length, ...(drained ? { embedded: drained.embedded } : {}) });
140
140
  const { render, style, io } = cli;
141
141
  io.out(` ${render.g.diamond} ${style.accent(slug)} ← ${style.path(shortenHome(abs))} ${style.dim(`${swept.ingested.length} files`)}`);
142
142
  if (drained)
@@ -146,23 +146,13 @@ async function runAdd(cli, args) {
146
146
  function runList(cli) {
147
147
  const ws = cli.workspace();
148
148
  const sources = listSources(ws.store).map((s) => ({ ...s, repos: attachedRepoSlugs(cli, ws, s.sourceId) }));
149
- if (cli.flags.mode === "json") {
150
- cli.io.out(JSON.stringify(sources.map((s) => ({ name: s.name, adapter: s.adapter, config: s.config, repos: s.repos }))));
151
- return EXIT_OK;
152
- }
153
- if (cli.flags.mode === "jsonl") {
154
- for (const s of sources)
155
- cli.io.out(JSON.stringify({ name: s.name, adapter: s.adapter, config: s.config, repos: s.repos }));
156
- return EXIT_OK;
157
- }
158
- if (cli.flags.mode === "ids") {
159
- for (const s of sources)
160
- cli.io.out(s.name);
161
- return EXIT_OK;
149
+ if (cli.flags.mode !== "human") {
150
+ const rows = sources.map((s) => ({ name: s.name, adapter: s.adapter, config: s.config, repos: s.repos }));
151
+ return emitMachine(cli, rows, { items: rows, ids: rows.map((r) => r.name) });
162
152
  }
163
153
  const { style, io } = cli;
164
154
  if (sources.length === 0) {
165
- io.out(style.dim(" no sources registered"));
155
+ io.err(style.dim(" no sources registered"));
166
156
  return EXIT_OK;
167
157
  }
168
158
  const rows = sources.map((s) => [
@@ -189,10 +179,8 @@ function runAttachDetach(cli, args, verb) {
189
179
  attachSourceToRepo(ws.store, repo.repoId, source.sourceId);
190
180
  else
191
181
  detachSourceFromRepo(ws.store, repo.repoId, source.sourceId);
192
- if (cli.flags.mode !== "human") {
193
- cli.io.out(JSON.stringify({ source: name, repo: repo.slug, attached: verb === "attach" }));
194
- return EXIT_OK;
195
- }
182
+ if (cli.flags.mode !== "human")
183
+ return emitMachine(cli, { source: name, repo: repo.slug, attached: verb === "attach" });
196
184
  cli.io.out(` ${cli.render.g.ok} ${verb === "attach" ? "attached" : "detached"} ${cli.style.accent(name)} ${verb === "attach" ? "→" : "⇸"} ${cli.style.accent(repo.slug)}`);
197
185
  return EXIT_OK;
198
186
  }
@@ -206,10 +194,8 @@ function runRm(cli, args) {
206
194
  if (!source)
207
195
  throw new EngineErrorLike("target_missing", `no source named '${name}'`);
208
196
  deleteSource(ws.store, source.sourceId);
209
- if (cli.flags.mode !== "human") {
210
- cli.io.out(JSON.stringify({ source: name, deleted: true }));
211
- return EXIT_OK;
212
- }
197
+ if (cli.flags.mode !== "human")
198
+ return emitMachine(cli, { source: name, deleted: true });
213
199
  cli.io.out(` ${cli.render.g.ok} deleted source ${cli.style.accent(name)}`);
214
200
  return EXIT_OK;
215
201
  }