omgbase 0.3.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.
- package/dist/src/cmd/_mutate.d.ts +12 -0
- package/dist/src/cmd/_mutate.js +26 -13
- package/dist/src/cmd/_source.d.ts +3 -3
- package/dist/src/cmd/_source.js +14 -11
- package/dist/src/cmd/admin.js +25 -22
- package/dist/src/cmd/bootstrap.js +14 -22
- package/dist/src/cmd/cat.js +10 -12
- package/dist/src/cmd/diff.js +39 -27
- package/dist/src/cmd/docs.js +44 -6
- package/dist/src/cmd/find.js +3 -15
- package/dist/src/cmd/hist.js +4 -10
- package/dist/src/cmd/links.js +21 -16
- package/dist/src/cmd/log.js +8 -18
- package/dist/src/cmd/ls.js +3 -15
- package/dist/src/cmd/mcp.js +22 -36
- package/dist/src/cmd/mutate.js +38 -11
- package/dist/src/cmd/node.js +7 -3
- package/dist/src/cmd/oqx.d.ts +10 -0
- package/dist/src/cmd/oqx.js +25 -28
- package/dist/src/cmd/outline.js +5 -8
- package/dist/src/cmd/retarget.js +6 -21
- package/dist/src/cmd/run.js +5 -32
- package/dist/src/cmd/show.js +11 -12
- package/dist/src/cmd/source.js +18 -32
- package/dist/src/cmd/status.js +13 -25
- package/dist/src/cmd/sync.js +19 -14
- package/dist/src/cmd/update.js +10 -5
- package/dist/src/context.d.ts +4 -0
- package/dist/src/context.js +14 -0
- package/dist/src/dispatch.d.ts +3 -0
- package/dist/src/dispatch.js +35 -18
- package/dist/src/main.d.ts +7 -0
- package/dist/src/main.js +46 -12
- package/dist/src/output.d.ts +28 -1
- package/dist/src/output.js +66 -20
- package/dist/src/seams.d.ts +13 -0
- package/dist/src/seams.js +37 -0
- package/dist/src/shell/session.js +4 -2
- package/package.json +4 -4
package/dist/src/cmd/hist.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { resolveRef, historyNode } from "@omgbase/core";
|
|
3
|
-
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
import { remoteCall } from "./_remote.js";
|
|
5
5
|
// `omg hist <node>` (11 §5.5) — history_node: a block's biography.
|
|
6
6
|
async function runHist(cli, args) {
|
|
@@ -34,15 +34,9 @@ async function runHist(cli, args) {
|
|
|
34
34
|
throw new EngineErrorLike("block_missing", `hist needs a block id; got ${ref}`);
|
|
35
35
|
changes = historyNode(ws.store, resolved.blockId, values.n ? { limit: Number(values.n) } : {});
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
if (cli.flags.mode === "jsonl") {
|
|
42
|
-
for (const c of changes)
|
|
43
|
-
cli.io.out(JSON.stringify(c));
|
|
44
|
-
return EXIT_OK;
|
|
45
|
-
}
|
|
37
|
+
// A change carries no id of its own, so `--ids` is the `--json` document.
|
|
38
|
+
if (cli.flags.mode !== "human")
|
|
39
|
+
return emitMachine(cli, changes, { items: changes });
|
|
46
40
|
const { style, io } = cli;
|
|
47
41
|
if (changes.length === 0) {
|
|
48
42
|
io.err(style.dim(" no history"));
|
package/dist/src/cmd/links.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { resolveRef, docLinks } from "@omgbase/core";
|
|
3
|
-
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
// `omg links <node>` (11 §5.4) — open edges touching the node; default both
|
|
5
|
-
// directions, grouped; doc-grain by default, --blocks for block-grain
|
|
6
|
-
// Backlinks = --in.
|
|
5
|
+
// directions, grouped; doc-grain by default, --blocks for block-grain (one row
|
|
6
|
+
// per edge, naming the source block). Backlinks = --in.
|
|
7
7
|
function runLinks(cli, args) {
|
|
8
8
|
const { values, positionals } = parseArgs({
|
|
9
9
|
args,
|
|
@@ -45,30 +45,35 @@ function runLinks(cli, args) {
|
|
|
45
45
|
opts.blocks = true;
|
|
46
46
|
const result = docLinks(ws.store, resolved.docId, opts);
|
|
47
47
|
cli.capture?.(result); // shell: edges (out+in) become the addressable frame
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
if (cli.flags.mode === "ids") {
|
|
53
|
-
for (const e of [...result.out, ...result.in])
|
|
54
|
-
cli.io.out(e.node);
|
|
55
|
-
return EXIT_OK;
|
|
56
|
-
}
|
|
48
|
+
// `{ out, in }` is not a list: `--jsonl` prints the document; `--ids` the far nodes.
|
|
49
|
+
if (cli.flags.mode !== "human")
|
|
50
|
+
return emitMachine(cli, result, { ids: [...result.out, ...result.in].map((e) => e.node) });
|
|
57
51
|
const { render, style, io } = cli;
|
|
58
52
|
const g = render.g;
|
|
59
53
|
if (result.out.length === 0 && result.in.length === 0) {
|
|
60
54
|
io.err(style.dim(" no links"));
|
|
61
55
|
return EXIT_OK;
|
|
62
56
|
}
|
|
57
|
+
// Doc-grain: one row per (predicate, node) with its rollup count. Block-grain:
|
|
58
|
+
// one row per edge, the source block beside the far node.
|
|
59
|
+
const src = (e) => style.dim(`(${e.block ?? "frontmatter"})`);
|
|
63
60
|
if (result.out.length > 0) {
|
|
64
61
|
io.out(style.dim(" out"));
|
|
65
|
-
for (const e of result.out)
|
|
66
|
-
|
|
62
|
+
for (const e of result.out) {
|
|
63
|
+
if (values.blocks)
|
|
64
|
+
io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${src(e)}`);
|
|
65
|
+
else
|
|
66
|
+
io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${style.dim(`×${e.count}`)}`);
|
|
67
|
+
}
|
|
67
68
|
}
|
|
68
69
|
if (result.in.length > 0) {
|
|
69
70
|
io.out(style.dim(" in (backlinks)"));
|
|
70
|
-
for (const e of result.in)
|
|
71
|
-
|
|
71
|
+
for (const e of result.in) {
|
|
72
|
+
if (values.blocks)
|
|
73
|
+
io.out(` ${style.id(e.node)} ${src(e)} ${style.dim(g.arrow)} ${style.accent(e.predicate)}`);
|
|
74
|
+
else
|
|
75
|
+
io.out(` ${style.id(e.node)} ${style.dim(g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
|
|
76
|
+
}
|
|
72
77
|
}
|
|
73
78
|
return EXIT_OK;
|
|
74
79
|
}
|
package/dist/src/cmd/log.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
80
|
-
cli.
|
|
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) {
|
package/dist/src/cmd/ls.js
CHANGED
|
@@ -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
|
|
43
|
-
|
|
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"));
|
package/dist/src/cmd/mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep,
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
29
|
-
const
|
|
30
|
-
if (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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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).
|
|
@@ -127,10 +108,10 @@ async function runMcp(cli, args) {
|
|
|
127
108
|
if (wantWatch) {
|
|
128
109
|
lease = WatchLease.tryAcquire(ws.omgbaseDir);
|
|
129
110
|
if (lease) {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
111
|
+
// spec/sync §5 order: open the source (the external fs-adapter process —
|
|
112
|
+
// chokidar lives there, not in-engine), `watch`, wait for `ready` (bounded),
|
|
113
|
+
// THEN the priming sweep, then report live. An edit landing before the feed
|
|
114
|
+
// is primed is caught by the sweep, one after it by the feed.
|
|
134
115
|
source = await openRepoSource(ws.store, repo);
|
|
135
116
|
if (source) {
|
|
136
117
|
watcher = new Watcher(ws.store, repo.repoId, source, {
|
|
@@ -144,6 +125,11 @@ async function runMcp(cli, args) {
|
|
|
144
125
|
onError: (err) => cli.io.err(cli.style.dim(`[watch] error: ${String(err)}`)),
|
|
145
126
|
});
|
|
146
127
|
await watcher.start();
|
|
128
|
+
if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
|
|
129
|
+
cli.io.err(cli.style.dim(`[watch] the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready`));
|
|
130
|
+
}
|
|
131
|
+
if (repo.rootPath)
|
|
132
|
+
freshnessSweep(ws.store, repo.repoId, repo.rootPath); // prime: anything that landed before the feed was ready
|
|
147
133
|
cli.io.err(cli.style.dim(`[mcp] serving ${repo.slug} on stdio · watcher live`));
|
|
148
134
|
}
|
|
149
135
|
else {
|
package/dist/src/cmd/mutate.js
CHANGED
|
@@ -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")
|
|
@@ -67,33 +81,36 @@ async function runInsert(cli, args) {
|
|
|
67
81
|
const { values, positionals } = parseArgs({
|
|
68
82
|
args: content.rest,
|
|
69
83
|
allowPositionals: true,
|
|
70
|
-
options: { at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
84
|
+
options: { at: { type: "string" }, expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
71
85
|
});
|
|
72
86
|
if (values.help) {
|
|
73
87
|
return renderHelp(cli, {
|
|
74
88
|
name: "insert",
|
|
75
89
|
summary: "Insert markdown as new block(s) under a parent block or heading",
|
|
76
|
-
usage: "insert <to> (-m <markdown> | -f <file> | -) [--at end|start|before <id>|after <id>] [--actor <s>] [--dry-run]",
|
|
90
|
+
usage: "insert <to> (-m <markdown> | -f <file> | -) [--at end|start|before <id>|after <id>] [--expect <parent_children_hash>] [--actor <s>] [--dry-run]",
|
|
77
91
|
options: [
|
|
78
92
|
["<to>", "parent block id, or a heading block id to append into its section"],
|
|
79
93
|
["-m <markdown>", "content inline"],
|
|
80
94
|
["-f <file>", "content from a file"],
|
|
81
95
|
["-", "content from stdin"],
|
|
82
96
|
["--at <pos>", "end (default) | start | before <id> | after <id>"],
|
|
97
|
+
["--expect <hash>", "destination CAS: fail with stale_expectation unless the parent's direct child ids (joined by \",\", sha256 hex) still hash to this"],
|
|
83
98
|
["--actor <s>", "commit actor (default human:$USER)"],
|
|
84
99
|
],
|
|
85
100
|
});
|
|
86
101
|
}
|
|
87
102
|
const to = positionals[0];
|
|
88
103
|
if (!to)
|
|
89
|
-
throw new CliUsageError("insert requires a <to> parent (block id, or a
|
|
104
|
+
throw new CliUsageError("insert requires a <to> parent (a container block id, or a doc id/path for the top level)");
|
|
90
105
|
const markdown = readContent(content);
|
|
106
|
+
// The destination-parent CAS (spec/mutate §1.2) rides as the op-level expect.
|
|
107
|
+
const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
|
|
91
108
|
if (cli.flags.server)
|
|
92
|
-
return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values) });
|
|
109
|
+
return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values), ...expect });
|
|
93
110
|
const ws = cli.workspace();
|
|
94
111
|
const repo = cli.repo(ws);
|
|
95
112
|
const parent = block(cli, ws, repo.repoId, to);
|
|
96
|
-
const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown };
|
|
113
|
+
const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown, ...expect };
|
|
97
114
|
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
98
115
|
}
|
|
99
116
|
// ---- update -----------------------------------------------------------------
|
|
@@ -143,17 +160,18 @@ async function runMove(cli, args) {
|
|
|
143
160
|
const { values, positionals } = parseArgs({
|
|
144
161
|
args,
|
|
145
162
|
allowPositionals: true,
|
|
146
|
-
options: { to: { type: "string" }, at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
163
|
+
options: { to: { type: "string" }, at: { type: "string" }, expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
147
164
|
});
|
|
148
165
|
if (values.help) {
|
|
149
166
|
return renderHelp(cli, {
|
|
150
167
|
name: "move",
|
|
151
168
|
summary: "Move block(s) under a new parent, identity preserved",
|
|
152
|
-
usage: "move <blocks…|-> --to <parent> [--at end|start|before <id>|after <id>] [--actor <s>] [--dry-run]",
|
|
169
|
+
usage: "move <blocks…|-> --to <parent> [--at end|start|before <id>|after <id>] [--expect <parent_children_hash>] [--actor <s>] [--dry-run]",
|
|
153
170
|
options: [
|
|
154
171
|
["<blocks…>", "block ids; `-` reads them from stdin, one per line"],
|
|
155
172
|
["--to <parent>", "destination parent block"],
|
|
156
173
|
["--at <pos>", "end (default) | start | before <id> | after <id>"],
|
|
174
|
+
["--expect <hash>", "destination CAS: fail with stale_expectation unless the destination's direct child ids (joined by \",\", sha256 hex) still hash to this"],
|
|
157
175
|
["--actor <s>", "commit actor (default human:$USER)"],
|
|
158
176
|
],
|
|
159
177
|
});
|
|
@@ -163,13 +181,15 @@ async function runMove(cli, args) {
|
|
|
163
181
|
const refs = expandBlockArgs(positionals);
|
|
164
182
|
if (refs.length === 0)
|
|
165
183
|
throw new CliUsageError("move requires one or more blocks (or - for stdin)");
|
|
184
|
+
// The destination-parent CAS (spec/mutate §1.2), checked once for the run.
|
|
185
|
+
const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
|
|
166
186
|
if (cli.flags.server)
|
|
167
|
-
return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values) });
|
|
187
|
+
return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values), ...expect });
|
|
168
188
|
const ws = cli.workspace();
|
|
169
189
|
const repo = cli.repo(ws);
|
|
170
190
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
171
191
|
const parent = block(cli, ws, repo.repoId, values.to);
|
|
172
|
-
const op = { op: "move", blocks, to: { parent, at: parseAt(values) } };
|
|
192
|
+
const op = { op: "move", blocks, to: { parent, at: parseAt(values) }, ...expect };
|
|
173
193
|
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
174
194
|
}
|
|
175
195
|
// ---- rm (blocks; --doc handled by the doc-level command) --------------------
|
|
@@ -232,7 +252,7 @@ async function runDone(cli, args) {
|
|
|
232
252
|
return runOpsRemote(cli, "tasks_complete", { blocks: refs, ...(values.undo ? { checked: false } : {}) });
|
|
233
253
|
const ws = cli.workspace();
|
|
234
254
|
const repo = cli.repo(ws);
|
|
235
|
-
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)));
|
|
236
256
|
const ops = values.undo
|
|
237
257
|
? blocks.map((b) => ({ op: "update", block: b, attrs: { checked: false }, expect: pinned(ws, b) }))
|
|
238
258
|
: tasksComplete(ws.store, blocks);
|
|
@@ -295,7 +315,14 @@ async function runSplit(cli, args) {
|
|
|
295
315
|
throw new CliUsageError("split requires a <block>");
|
|
296
316
|
if (!values.at)
|
|
297
317
|
throw new CliUsageError("split requires --at n[,n…]");
|
|
298
|
-
|
|
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
|
+
});
|
|
299
326
|
if (cli.flags.server)
|
|
300
327
|
return runOpsRemote(cli, "blocks_split", { block: ref, at });
|
|
301
328
|
const ws = cli.workspace();
|
package/dist/src/cmd/node.js
CHANGED
|
@@ -28,8 +28,12 @@ async function runNode(cli, args) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
if (sub === "set") {
|
|
31
|
-
|
|
32
|
-
|
|
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).
|
package/dist/src/cmd/oqx.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/cmd/oqx.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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"
|
|
155
|
-
if (
|
|
156
|
-
truncationFooter(cli.io, cli.style,
|
|
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;
|
package/dist/src/cmd/outline.js
CHANGED
|
@@ -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>] [--
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
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));
|
package/dist/src/cmd/retarget.js
CHANGED
|
@@ -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
|
|
8
|
-
//
|
|
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
|
|
69
|
-
io.out(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
|