omgbase 0.11.1 → 0.13.0
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/build-info.json +2 -2
- package/dist/src/cmd/_mutate.d.ts +51 -16
- package/dist/src/cmd/_mutate.js +133 -52
- package/dist/src/cmd/docs.js +0 -0
- package/dist/src/cmd/edit.js +7 -7
- package/dist/src/cmd/help.js +1 -1
- package/dist/src/cmd/mcp.js +16 -4
- package/dist/src/cmd/mutate.d.ts +1 -0
- package/dist/src/cmd/mutate.js +114 -65
- package/dist/src/cmd/node.js +7 -7
- package/dist/src/cmd/retarget.js +27 -24
- package/dist/src/cmd/source.js +4 -2
- package/dist/src/cmd/sync.js +12 -6
- package/dist/src/cmd/update.js +51 -40
- package/dist/src/commands.js +2 -1
- package/dist/src/context.js +3 -2
- package/dist/src/dispatch.js +9 -4
- package/dist/src/seams.d.ts +3 -0
- package/dist/src/seams.js +39 -3
- package/dist/src/shell/refs.js +3 -10
- package/package.json +3 -3
package/dist/src/cmd/mutate.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { resolveRef,
|
|
3
|
+
import { resolveRef, isContainerBlock, appendEffects, checkEffects, insertEffects, mergeEffects, moveBlocksEffects, removeBlocksEffects, splitEffects, updateBlockEffects, } from "@omgbase/core";
|
|
4
4
|
import { CliUsageError, EngineErrorLike, renderHelp } from "../output.js";
|
|
5
|
-
import {
|
|
5
|
+
import { runSugar, runSugarRemote, runEffects, runEffectsRemote, runExecutePlan, runExecutePlanRemote, readContent, readStdin, expandBlockArgs, extractContentOpts, targetOf } from "./_mutate.js";
|
|
6
6
|
import { runRmDoc } from "./docs.js";
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
import { remoteCall } from "./_remote.js";
|
|
8
|
+
// Write sugar (11 §5.6; cli 4.0): each command resolves its refs, builds its
|
|
9
|
+
// effects with the shared builders of `@omgbase/core` (the same table the MCP
|
|
10
|
+
// tools use — ADR-012, the CLI holds no business logic) and applies them as one
|
|
11
|
+
// request via `runSugar` (writer flock, actor default, `--dry-run` as a plan,
|
|
12
|
+
// conflict rendering). `apply` is the primitive — a changeset of `effects`
|
|
13
|
+
// (ADR-021, the intentional-write vocabulary; the kernel `ops` input is retired
|
|
14
|
+
// at cli 4.0) — and `execute` commits a frozen plan; the rest are sugar.
|
|
10
15
|
function block(cli, ws, repoId, ref) {
|
|
11
16
|
const r = resolveRef(ws.store, repoId, ref);
|
|
12
17
|
if (!r || r.kind !== "block")
|
|
13
18
|
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
|
|
14
19
|
return r.blockId;
|
|
15
20
|
}
|
|
21
|
+
// A parent ref for insert/move: a block ref names that block as the parent; a
|
|
22
|
+
// document ref (id or path) names the document's top level — the same reading
|
|
23
|
+
// the server gives `blocks_insert.to` / `blocks_move.to`.
|
|
24
|
+
function parent(ws, repoId, ref) {
|
|
25
|
+
const r = resolveRef(ws.store, repoId, ref);
|
|
26
|
+
if (!r)
|
|
27
|
+
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
|
|
28
|
+
if (r.kind === "document")
|
|
29
|
+
return { kind: "document", id: r.docId };
|
|
30
|
+
return { kind: "block", id: r.blockId };
|
|
31
|
+
}
|
|
16
32
|
// `done` addresses task blocks only (spec/cli §6): any other live block type is
|
|
17
33
|
// a type_mismatch before an op is built — the macro itself would record a no-op
|
|
18
34
|
// `edited` commit (its `attrs.checked` only rewrites a task's marker).
|
|
@@ -27,12 +43,6 @@ function taskBlock(ws, ref, blockId) {
|
|
|
27
43
|
}
|
|
28
44
|
return blockId;
|
|
29
45
|
}
|
|
30
|
-
function rawHashOf(ws, blockId) {
|
|
31
|
-
const row = ws.store.db
|
|
32
|
-
.prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
|
|
33
|
-
.get(blockId);
|
|
34
|
-
return row?.h ?? null;
|
|
35
|
-
}
|
|
36
46
|
// Parse an `--at` spec: end|start|before X|after X (default end).
|
|
37
47
|
function parseAt(values) {
|
|
38
48
|
const at = values.at?.trim();
|
|
@@ -55,25 +65,65 @@ async function runApply(cli, args) {
|
|
|
55
65
|
if (values.help) {
|
|
56
66
|
return renderHelp(cli, {
|
|
57
67
|
name: "apply",
|
|
58
|
-
summary: "Apply a
|
|
68
|
+
summary: "Apply a changeset — `{ effects: [...] }`, the intentional writes (create/insert/set/unset/move/remove/split/merge/retarget) planned and committed as one transaction — the primitive every other mutator expands to",
|
|
59
69
|
usage: "apply [-f <changeset.json|->] [--reason <s>] [--actor <s>] [--dry-run]",
|
|
60
70
|
options: [
|
|
61
71
|
["-f <file|->", "changeset JSON from a file or stdin (default: stdin)"],
|
|
62
72
|
["--reason <s>", "commit reason recorded in history"],
|
|
63
73
|
["--actor <s>", "commit actor (default human:$USER)"],
|
|
64
|
-
["--dry-run", "
|
|
74
|
+
["--dry-run", "plan only: render the diff, commit nothing (--json prints the Plan for `execute`)"],
|
|
65
75
|
],
|
|
66
76
|
});
|
|
67
77
|
}
|
|
68
78
|
const raw = values.f ? (values.f === "-" ? readStdin() : readFileSync(values.f, "utf8")) : readStdin();
|
|
69
79
|
const parsed = JSON.parse(raw);
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
// The effects are the one input (spec/cli §6 `apply`, 4.0): the kernel changeset
|
|
81
|
+
// `{ ops }` of 3.x is refused by name — the six ops are a library/fixture contract.
|
|
82
|
+
if (parsed.ops !== undefined)
|
|
83
|
+
throw new CliUsageError("`ops` is no longer an input — apply takes `effects` (the kernel ops are a library/fixture contract since spec/mutate 3.0)");
|
|
84
|
+
if (parsed.effects === undefined)
|
|
85
|
+
throw new CliUsageError("changeset must have an `effects` array");
|
|
86
|
+
if (!Array.isArray(parsed.effects))
|
|
87
|
+
throw new CliUsageError("changeset `effects` must be an array");
|
|
88
|
+
const opts = { ...(values.reason ? { reason: values.reason } : {}), ...(values.actor ? { actor: values.actor } : {}) };
|
|
72
89
|
if (cli.flags.server)
|
|
73
|
-
return
|
|
90
|
+
return runEffectsRemote(cli, parsed.effects, values.reason);
|
|
74
91
|
const ws = cli.workspace();
|
|
75
92
|
const repo = cli.repo(ws);
|
|
76
|
-
return
|
|
93
|
+
return runEffects(cli, ws, repo, parsed.effects, opts);
|
|
94
|
+
}
|
|
95
|
+
// ---- execute (the plan form of the Effect layer) ------------------------------
|
|
96
|
+
async function runExecute(cli, args) {
|
|
97
|
+
const { values, positionals } = parseArgs({
|
|
98
|
+
args,
|
|
99
|
+
allowPositionals: true,
|
|
100
|
+
options: { f: { type: "string", short: "f" }, reason: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
101
|
+
});
|
|
102
|
+
if (values.help) {
|
|
103
|
+
return renderHelp(cli, {
|
|
104
|
+
name: "execute",
|
|
105
|
+
summary: "Commit exactly a frozen Plan (what `apply -f <effects> --dry-run --json` printed) — one transaction, one commit; stale_plan if a document moved on",
|
|
106
|
+
usage: "execute (-f <plan.json> | -) [--reason <s>] [--actor <s>]",
|
|
107
|
+
options: [
|
|
108
|
+
["-f <file|->", "the Plan JSON from a file or stdin"],
|
|
109
|
+
["-", "the Plan JSON from stdin"],
|
|
110
|
+
["--reason <s>", "commit reason recorded in history"],
|
|
111
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// The plan IS the preview: a dry run of `execute` would be `apply --dry-run` again.
|
|
116
|
+
if (cli.flags.dryRun)
|
|
117
|
+
throw new CliUsageError("execute has no dry run — the plan is the preview (re-plan with apply --dry-run)");
|
|
118
|
+
const source = values.f ?? (positionals[0] === "-" ? "-" : undefined);
|
|
119
|
+
if (source === undefined)
|
|
120
|
+
throw new CliUsageError("execute requires -f <plan.json> (or - for stdin)");
|
|
121
|
+
const plan = JSON.parse(source === "-" ? readStdin() : readFileSync(source, "utf8"));
|
|
122
|
+
if (cli.flags.server)
|
|
123
|
+
return runExecutePlanRemote(cli, plan, values.reason);
|
|
124
|
+
const ws = cli.workspace();
|
|
125
|
+
const repo = cli.repo(ws);
|
|
126
|
+
return runExecutePlan(cli, ws, repo, plan, { ...(values.reason ? { reason: values.reason } : {}), ...(values.actor ? { actor: values.actor } : {}) });
|
|
77
127
|
}
|
|
78
128
|
// ---- insert -----------------------------------------------------------------
|
|
79
129
|
async function runInsert(cli, args) {
|
|
@@ -103,15 +153,15 @@ async function runInsert(cli, args) {
|
|
|
103
153
|
if (!to)
|
|
104
154
|
throw new CliUsageError("insert requires a <to> parent (a container block id, or a doc id/path for the top level)");
|
|
105
155
|
const markdown = readContent(content);
|
|
106
|
-
// The destination-parent CAS (spec/mutate §1.2) rides as the
|
|
107
|
-
const expect = values.expect ? {
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
// The destination-parent CAS (spec/mutate §1.2) rides as the effect's expect.
|
|
157
|
+
const expect = values.expect ? { parent_children_hash: values.expect } : undefined;
|
|
158
|
+
const at = parseAt(values);
|
|
159
|
+
if (cli.flags.server) {
|
|
160
|
+
return runSugarRemote(cli, "blocks_insert", { to, markdown, at, ...(expect ? { expect } : {}) }, () => insertEffects(targetOf(to), at, markdown, expect).effects);
|
|
161
|
+
}
|
|
110
162
|
const ws = cli.workspace();
|
|
111
163
|
const repo = cli.repo(ws);
|
|
112
|
-
|
|
113
|
-
const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown, ...expect };
|
|
114
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
164
|
+
return runSugar(cli, ws, repo, insertEffects(parent(ws, repo.repoId, to), at, markdown, expect), actorOf(values));
|
|
115
165
|
}
|
|
116
166
|
// ---- update -----------------------------------------------------------------
|
|
117
167
|
async function runUpdate(cli, args) {
|
|
@@ -139,21 +189,21 @@ async function runUpdate(cli, args) {
|
|
|
139
189
|
if (!ref)
|
|
140
190
|
throw new CliUsageError("update requires a <block>");
|
|
141
191
|
const markdown = readContent(content);
|
|
192
|
+
const expect = values.expect ? { content_hash: values.expect } : undefined;
|
|
142
193
|
// Remote: the server pins CAS from current bytes when --expect is absent.
|
|
143
|
-
if (cli.flags.server)
|
|
144
|
-
return
|
|
194
|
+
if (cli.flags.server) {
|
|
195
|
+
return runSugarRemote(cli, "blocks_update", { id: ref, markdown, ...(expect ? { expect } : {}) }, () => updateBlockEffects(ref, { markdown, expect }).effects);
|
|
196
|
+
}
|
|
145
197
|
const ws = cli.workspace();
|
|
146
198
|
const repo = cli.repo(ws);
|
|
147
199
|
const blockId = block(cli, ws, repo.repoId, ref);
|
|
148
200
|
// CAS (§5.7): scripted callers pass --expect; interactive callers omit it and
|
|
149
|
-
//
|
|
150
|
-
// edits since a read the caller never made).
|
|
151
|
-
const expect = values.expect ?? rawHashOf(ws, blockId);
|
|
201
|
+
// the planner pins the live hash (protects against concurrent edits, not
|
|
202
|
+
// against edits since a read the caller never made).
|
|
152
203
|
if (!values.expect) {
|
|
153
204
|
cli.io.err(cli.style.dim(` updating ${ref} (CAS pinned from current bytes)`));
|
|
154
205
|
}
|
|
155
|
-
|
|
156
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
206
|
+
return runSugar(cli, ws, repo, updateBlockEffects(blockId, { markdown, expect }), actorOf(values));
|
|
157
207
|
}
|
|
158
208
|
// ---- move -------------------------------------------------------------------
|
|
159
209
|
async function runMove(cli, args) {
|
|
@@ -182,15 +232,16 @@ async function runMove(cli, args) {
|
|
|
182
232
|
if (refs.length === 0)
|
|
183
233
|
throw new CliUsageError("move requires one or more blocks (or - for stdin)");
|
|
184
234
|
// The destination-parent CAS (spec/mutate §1.2), checked once for the run.
|
|
185
|
-
const expect = values.expect ? {
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
const expect = values.expect ? { parent_children_hash: values.expect } : undefined;
|
|
236
|
+
const at = parseAt(values);
|
|
237
|
+
const to = values.to;
|
|
238
|
+
if (cli.flags.server) {
|
|
239
|
+
return runSugarRemote(cli, "blocks_move", { blocks: refs, to, at, ...(expect ? { expect } : {}) }, () => moveBlocksEffects(refs, targetOf(to), at, expect).effects);
|
|
240
|
+
}
|
|
188
241
|
const ws = cli.workspace();
|
|
189
242
|
const repo = cli.repo(ws);
|
|
190
243
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
191
|
-
|
|
192
|
-
const op = { op: "move", blocks, to: { parent, at: parseAt(values) }, ...expect };
|
|
193
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
244
|
+
return runSugar(cli, ws, repo, moveBlocksEffects(blocks, parent(ws, repo.repoId, to), at, expect), actorOf(values));
|
|
194
245
|
}
|
|
195
246
|
// ---- rm (blocks; --doc handled by the doc-level command) --------------------
|
|
196
247
|
async function runRm(cli, args) {
|
|
@@ -219,12 +270,11 @@ async function runRm(cli, args) {
|
|
|
219
270
|
if (refs.length === 0)
|
|
220
271
|
throw new CliUsageError("rm requires one or more blocks (or - for stdin), or --doc");
|
|
221
272
|
if (cli.flags.server)
|
|
222
|
-
return
|
|
273
|
+
return runSugarRemote(cli, "blocks_remove", { blocks: refs }, () => removeBlocksEffects(refs).effects);
|
|
223
274
|
const ws = cli.workspace();
|
|
224
275
|
const repo = cli.repo(ws);
|
|
225
276
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
226
|
-
|
|
227
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
277
|
+
return runSugar(cli, ws, repo, removeBlocksEffects(blocks), actorOf(values));
|
|
228
278
|
}
|
|
229
279
|
// ---- done (blocks_check) --------------------------------------------------
|
|
230
280
|
async function runDone(cli, args) {
|
|
@@ -248,15 +298,13 @@ async function runDone(cli, args) {
|
|
|
248
298
|
const refs = expandBlockArgs(positionals);
|
|
249
299
|
if (refs.length === 0)
|
|
250
300
|
throw new CliUsageError("done requires one or more task blocks (or - for stdin)");
|
|
301
|
+
const checked = !values.undo;
|
|
251
302
|
if (cli.flags.server)
|
|
252
|
-
return
|
|
303
|
+
return runSugarRemote(cli, "blocks_check", { ids: refs, ...(values.undo ? { checked: false } : {}) }, () => checkEffects(refs, checked).effects);
|
|
253
304
|
const ws = cli.workspace();
|
|
254
305
|
const repo = cli.repo(ws);
|
|
255
306
|
const blocks = refs.map((r) => taskBlock(ws, r, block(cli, ws, repo.repoId, r)));
|
|
256
|
-
|
|
257
|
-
? blocks.map((b) => ({ op: "update", block: b, attrs: { checked: false }, expect: pinned(ws, b) }))
|
|
258
|
-
: tasksComplete(ws.store, blocks);
|
|
259
|
-
return runOps(cli, ws, repo, ops, actorOf(values));
|
|
307
|
+
return runSugar(cli, ws, repo, checkEffects(blocks, checked), actorOf(values));
|
|
260
308
|
}
|
|
261
309
|
// ---- append (blocks_append) --------------------------------------------------
|
|
262
310
|
async function runAppend(cli, args) {
|
|
@@ -285,27 +333,31 @@ async function runAppend(cli, args) {
|
|
|
285
333
|
throw new CliUsageError("append requires a <block>");
|
|
286
334
|
const markdown = readContent(content);
|
|
287
335
|
// Remote: blocks_append resolves the ref and decides by the block's kind
|
|
288
|
-
// server-side (spec/surface §4).
|
|
289
|
-
if (cli.flags.server)
|
|
290
|
-
return
|
|
336
|
+
// server-side (spec/surface §4); a remote dry run reads the kind first.
|
|
337
|
+
if (cli.flags.server) {
|
|
338
|
+
return runSugarRemote(cli, "blocks_append", { id: target, text: markdown }, async () => {
|
|
339
|
+
const { type } = await remoteCall(cli, "blocks_read", { id: target, resolution: "skeleton" });
|
|
340
|
+
return appendEffects(type === "heading" ? { kind: "section", heading_id: target } : { kind: "block", id: target }, markdown).effects;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
291
343
|
const ws = cli.workspace();
|
|
292
344
|
const repo = cli.repo(ws);
|
|
293
345
|
const blockId = block(cli, ws, repo.repoId, target);
|
|
294
|
-
return
|
|
346
|
+
return runSugar(cli, ws, repo, appendSugar(ws.store, blockId, markdown), actorOf(values));
|
|
295
347
|
}
|
|
296
|
-
/** The
|
|
297
|
-
* of its section
|
|
298
|
-
*
|
|
299
|
-
function
|
|
348
|
+
/** The effect `blocks_append` expands to, by what the block is: a heading → the
|
|
349
|
+
* end of its section; a container → the end of its children; a leaf →
|
|
350
|
+
* `block_not_container` (append means into, never a sibling insert). */
|
|
351
|
+
function appendSugar(store, blockId, markdown) {
|
|
300
352
|
const row = store.db
|
|
301
353
|
.prepare("SELECT b.type AS type, d.format AS format, (SELECT count(*) FROM blocks c WHERE c.parent_block = b.block_id AND c.deleted_commit IS NULL) AS children FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ? AND b.deleted_commit IS NULL")
|
|
302
354
|
.get(blockId);
|
|
303
355
|
if (!row)
|
|
304
356
|
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(blockId)}`, { data: { id: blockId } });
|
|
305
357
|
if (row.type === "heading")
|
|
306
|
-
return
|
|
358
|
+
return appendEffects({ kind: "section", heading_id: blockId }, markdown);
|
|
307
359
|
if (isContainerBlock(row.format ?? "markdown", { type: row.type, children: new Array(row.children) })) {
|
|
308
|
-
return
|
|
360
|
+
return appendEffects({ kind: "block", id: blockId }, markdown);
|
|
309
361
|
}
|
|
310
362
|
throw new EngineErrorLike("block_not_container", `block ${blockId} is a ${row.type}, not a heading or a container; append means into — insert after it with blocks_insert { to: <its parent>, at: { after: ${JSON.stringify(blockId)} } }`, { data: { id: blockId, type: row.type } });
|
|
311
363
|
}
|
|
@@ -341,12 +393,12 @@ async function runSplit(cli, args) {
|
|
|
341
393
|
return n;
|
|
342
394
|
});
|
|
343
395
|
if (cli.flags.server)
|
|
344
|
-
return
|
|
396
|
+
return runSugarRemote(cli, "blocks_split", { id: ref, at }, () => splitEffects(ref, at).effects);
|
|
345
397
|
const ws = cli.workspace();
|
|
346
398
|
const repo = cli.repo(ws);
|
|
347
399
|
const blockId = block(cli, ws, repo.repoId, ref);
|
|
348
|
-
|
|
349
|
-
return
|
|
400
|
+
// The planner pins the content CAS from the live row.
|
|
401
|
+
return runSugar(cli, ws, repo, splitEffects(blockId, at), actorOf(values));
|
|
350
402
|
}
|
|
351
403
|
async function runMerge(cli, args) {
|
|
352
404
|
const { values, positionals } = parseArgs({
|
|
@@ -370,21 +422,18 @@ async function runMerge(cli, args) {
|
|
|
370
422
|
if (refs.length < 2)
|
|
371
423
|
throw new CliUsageError("merge requires at least two blocks");
|
|
372
424
|
if (cli.flags.server)
|
|
373
|
-
return
|
|
425
|
+
return runSugarRemote(cli, "blocks_merge", { blocks: refs, ...(values.sep !== undefined ? { separator: values.sep } : {}) }, () => mergeEffects(refs, values.sep).effects);
|
|
374
426
|
const ws = cli.workspace();
|
|
375
427
|
const repo = cli.repo(ws);
|
|
376
428
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
377
|
-
|
|
378
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
429
|
+
return runSugar(cli, ws, repo, mergeEffects(blocks, values.sep), actorOf(values));
|
|
379
430
|
}
|
|
380
431
|
// helpers ---------------------------------------------------------------------
|
|
381
|
-
function pinned(ws, blockId) {
|
|
382
|
-
return { content_hash: rawHashOf(ws, blockId) ?? "" };
|
|
383
|
-
}
|
|
384
432
|
function actorOf(values) {
|
|
385
433
|
return values.actor ? { actor: values.actor } : {};
|
|
386
434
|
}
|
|
387
|
-
export const cmdApply = { name: "apply", summary: "Apply a
|
|
435
|
+
export const cmdApply = { name: "apply", summary: "Apply a changeset of effects (the primitive)", run: (c, a) => runApply(c, a) };
|
|
436
|
+
export const cmdExecute = { name: "execute", summary: "Commit a frozen plan (from apply --dry-run --json)", run: (c, a) => runExecute(c, a) };
|
|
388
437
|
export const cmdInsert = { name: "insert", summary: "Insert blocks under a parent", run: (c, a) => runInsert(c, a) };
|
|
389
438
|
export const cmdUpdate = { name: "update", summary: "Replace a block's markdown (CAS)", run: (c, a) => runUpdate(c, a) };
|
|
390
439
|
export const cmdMove = { name: "move", summary: "Move blocks under a new parent", run: (c, a) => runMove(c, a) };
|
package/dist/src/cmd/node.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import {
|
|
2
|
+
import { nodeSetEffects, editablePropsFor } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
4
|
import { requireNode } from "./_remote.js";
|
|
5
|
-
import {
|
|
5
|
+
import { runSugar, runSugarRemote } from "./_mutate.js";
|
|
6
6
|
// `omg node set <nodeId> <prop> <value>` (node-editability) — surgically set an
|
|
7
7
|
// editable property of a projected node (e.g. a link's text/target, a task's
|
|
8
|
-
// checked)
|
|
9
|
-
//
|
|
8
|
+
// checked): one `set { node, field: prop, value }` effect (the `nodes_set`
|
|
9
|
+
// builder; cli 4.0), lowered by the planner through the adapter's registered
|
|
10
|
+
// editor. `omg node props <nodeId>` reports what's editable.
|
|
10
11
|
function nodeKindFormat(cli, nodeId) {
|
|
11
12
|
const ws = cli.workspace();
|
|
12
13
|
const row = ws.store.db.prepare("SELECT n.kind AS kind, d.format AS format FROM nodes n JOIN docs d ON d.doc_id = n.doc_id WHERE n.node_id = ?").get(nodeId);
|
|
@@ -40,11 +41,10 @@ async function runNode(cli, args) {
|
|
|
40
41
|
const value = valueParts.join(" ");
|
|
41
42
|
// Remote: nodes_set resolves node → block + rewrites the span server-side.
|
|
42
43
|
if (cli.flags.server)
|
|
43
|
-
return
|
|
44
|
+
return runSugarRemote(cli, "nodes_set", { id: nodeId, prop, value }, () => nodeSetEffects(nodeId, prop, value).effects);
|
|
44
45
|
const ws = cli.workspace();
|
|
45
46
|
const repo = cli.repo(ws);
|
|
46
|
-
|
|
47
|
-
return runOps(cli, ws, repo, ops, { reason: `nodes_set ${prop}`, ...(values.actor ? { actor: values.actor } : {}) });
|
|
47
|
+
return runSugar(cli, ws, repo, nodeSetEffects(requireNode(ws.store, repo.repoId, nodeId), prop, value), values.actor ? { actor: values.actor } : {});
|
|
48
48
|
}
|
|
49
49
|
if (sub === "props") {
|
|
50
50
|
// `props` is a local editability lookup with no remote tool (a niche read).
|
package/dist/src/cmd/retarget.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import {
|
|
2
|
+
import { applySugar, planEffects, retargetEffects, surfacePlan, surfaceReceipt } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
-
import {
|
|
4
|
+
import { defaultActor, renderPlan as renderPlanDiffs, renderReceipt, unifiedDiffLines } from "./_mutate.js";
|
|
5
5
|
import { remoteCall } from "./_remote.js";
|
|
6
|
-
// `omg retarget <from> <to> [--scope glob] [--apply]` (11 §5.6) — plan-by-default.
|
|
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.
|
|
10
6
|
async function runRetarget(cli, args) {
|
|
11
7
|
const { values, positionals } = parseArgs({
|
|
12
8
|
args,
|
|
@@ -29,32 +25,39 @@ async function runRetarget(cli, args) {
|
|
|
29
25
|
const to = positionals[1];
|
|
30
26
|
if (!from || !to)
|
|
31
27
|
throw new CliUsageError("retarget requires <from> and <to> targets");
|
|
32
|
-
|
|
28
|
+
const sugar = retargetEffects([{ from, to }], values.scope, "links_retarget");
|
|
29
|
+
// Remote: the `plan` tool previews, `links_retarget` commits server-side.
|
|
33
30
|
if (cli.flags.server) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (values.apply) {
|
|
38
|
-
if (cli.flags.mode !== "human")
|
|
39
|
-
cli.io.out(JSON.stringify(r));
|
|
40
|
-
else
|
|
41
|
-
cli.io.err(cli.style.dim(` ${cli.style.ok(cli.render.g.ok)} retargeted ${r.hits.length} block(s)`));
|
|
42
|
-
return EXIT_OK;
|
|
31
|
+
if (!values.apply || cli.flags.dryRun) {
|
|
32
|
+
const plan = await remoteCall(cli, "plan", { effects: sugar.effects });
|
|
33
|
+
return values.apply ? renderPlanDiffs(cli, plan) : renderHits(cli, from, to, retargetOf(plan).hits);
|
|
43
34
|
}
|
|
44
|
-
|
|
35
|
+
const r = await remoteCall(cli, "links_retarget", { from_target: from, to_target: to, ...(values.scope ? { path_glob: values.scope } : {}) });
|
|
36
|
+
return renderReceipt(cli, r);
|
|
45
37
|
}
|
|
46
38
|
const ws = cli.workspace();
|
|
47
39
|
const repo = cli.repo(ws);
|
|
48
|
-
const {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
40
|
+
const request = { repoId: repo.repoId, ...(repo.rootPath ? { rootPath: repo.rootPath } : {}), omgbaseDir: ws.omgbaseDir };
|
|
41
|
+
if (!values.apply) {
|
|
42
|
+
// The plan (default): what WOULD change, commit nothing, mint nothing.
|
|
43
|
+
const plan = surfacePlan(planEffects(ws.store, { ...request, effects: sugar.effects }));
|
|
44
|
+
return renderHits(cli, from, to, retargetOf(plan).hits);
|
|
53
45
|
}
|
|
54
|
-
|
|
46
|
+
// --apply → commit (the global --dry-run plans and renders the file diffs instead, §3.6).
|
|
47
|
+
const { plan, receipt } = applySugar(ws.store, { ...request, actor: values.actor ?? defaultActor(), ...(cli.flags.dryRun ? { dryRun: true } : {}) }, sugar);
|
|
48
|
+
const rooted = surfacePlan(plan);
|
|
49
|
+
if (!receipt)
|
|
50
|
+
return renderPlanDiffs(cli, rooted);
|
|
51
|
+
const { hits, pairs } = retargetOf(rooted);
|
|
52
|
+
return renderReceipt(cli, { hits, pairs, ...surfaceReceipt(receipt) }, plan);
|
|
53
|
+
}
|
|
54
|
+
/** The one retarget effect's preview; a plan over no hit still carries it (empty hits, a zero count). */
|
|
55
|
+
function retargetOf(plan) {
|
|
56
|
+
const r = plan.preview?.retarget?.[0];
|
|
57
|
+
return { hits: r?.hits ?? [], pairs: r?.pairs ?? [] };
|
|
55
58
|
}
|
|
56
59
|
// Plan (default): show what WOULD change, commit nothing.
|
|
57
|
-
function
|
|
60
|
+
function renderHits(cli, from, to, hits) {
|
|
58
61
|
if (cli.flags.mode === "json") {
|
|
59
62
|
cli.io.out(JSON.stringify({ from, to, hits }));
|
|
60
63
|
return EXIT_OK;
|
package/dist/src/cmd/source.js
CHANGED
|
@@ -2,7 +2,7 @@ import { resolve, basename } from "node:path";
|
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
|
-
import { createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, ensureRepo, freshnessSweep, walkMarkdownAsync, rebuildFileStats, } from "@omgbase/core";
|
|
5
|
+
import { createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, ensureRepo, freshnessSweep, walkMarkdownAsync, rebuildFileStats, withWriterLock, } from "@omgbase/core";
|
|
6
6
|
import { columns } from "../render.js";
|
|
7
7
|
import { drainEmbeddings } from "./_embed.js";
|
|
8
8
|
import { ensureFsAdapter, FS_ADAPTER } from "./_source.js";
|
|
@@ -121,7 +121,9 @@ async function runAdd(cli, args) {
|
|
|
121
121
|
ensureFsAdapter(ws.store);
|
|
122
122
|
const sourceId = createSource(ws.store, { name: sourceName, adapter: FS_ADAPTER, config: { root: abs } });
|
|
123
123
|
attachSourceToRepo(ws.store, repoId, sourceId);
|
|
124
|
-
|
|
124
|
+
// The initial sweep is a checkpoint ingest: under the workspace writer lock
|
|
125
|
+
// (spec/sync §7), like every other sweep.
|
|
126
|
+
const swept = withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repoId, abs));
|
|
125
127
|
rebuildFileStats(ws.store, repoId, abs);
|
|
126
128
|
// Offer to embed freshly-ingested blocks now (consent-gated like the ingest).
|
|
127
129
|
const drained = await drainEmbeddings(cli, ws, repoId, {
|
package/dist/src/cmd/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EngineError, surfaceSweepResult } from "@omgbase/core";
|
|
3
|
+
import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EngineError, surfaceSweepResult } from "@omgbase/core";
|
|
4
4
|
import { runFsMirror } from "@omgbase/sync";
|
|
5
5
|
import { EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
6
6
|
import { loadEmbedding } from "./_embed.js";
|
|
@@ -64,12 +64,14 @@ function runOneShot(cli, ws, repo) {
|
|
|
64
64
|
if (!repo.rootPath) {
|
|
65
65
|
// Nothing to sweep: an empty-result note (stderr, §3.1), or the empty sweep shape.
|
|
66
66
|
if (cli.flags.mode !== "human")
|
|
67
|
-
return emitMachine(cli, { scanned: 0, ingested: [], deleted: [], conflicted: [], changed: false });
|
|
67
|
+
return emitMachine(cli, { scanned: 0, ingested: [], deleted: [], conflicted: [], changed: false, materialized: [] });
|
|
68
68
|
cli.io.err(cli.style.dim(` ${repo.slug} has no filesystem source — nothing to sync`));
|
|
69
69
|
return EXIT_OK;
|
|
70
70
|
}
|
|
71
|
-
// The
|
|
72
|
-
|
|
71
|
+
// The sweep is a checkpoint ingest: under the workspace writer lock (spec/sync
|
|
72
|
+
// §7). The paths it names are printed in the reference form (spec/surface §1 "Paths").
|
|
73
|
+
const root = repo.rootPath;
|
|
74
|
+
const result = surfaceSweepResult(withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root)));
|
|
73
75
|
// A `SweepResult` is a record (its lists are per-kind): every machine mode is the document.
|
|
74
76
|
if (cli.flags.mode !== "human")
|
|
75
77
|
return emitMachine(cli, result);
|
|
@@ -137,7 +139,10 @@ async function runLocalWatch(cli, ws, repo) {
|
|
|
137
139
|
await embedding.close();
|
|
138
140
|
return EXIT_OK;
|
|
139
141
|
}
|
|
142
|
+
// Checkpoints and the priming sweep run under the workspace writer lock
|
|
143
|
+
// (spec/sync §7), as the Rust watcher's do.
|
|
140
144
|
const watcher = new Watcher(ws.store, repo.repoId, source, {
|
|
145
|
+
omgbaseDir: ws.omgbaseDir,
|
|
141
146
|
onCheckpoint: (r) => {
|
|
142
147
|
if (r.ingested.length || r.deleted.length || r.conflicted.length) {
|
|
143
148
|
cli.io.err(`${cli.style.ok(cli.render.g.sync)} +${r.ingested.length} -${r.deleted.length}${r.conflicted.length ? ` !${r.conflicted.length}` : ""}`);
|
|
@@ -150,8 +155,9 @@ async function runLocalWatch(cli, ws, repo) {
|
|
|
150
155
|
if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
|
|
151
156
|
cli.io.err(cli.style.dim(` the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready (an edit made before now may be missed until the next sweep)`));
|
|
152
157
|
}
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
const root = repo.rootPath;
|
|
159
|
+
if (root)
|
|
160
|
+
withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root)); // prime: anything that landed before the feed was ready
|
|
155
161
|
drainer?.schedule(); // embed anything already stale at startup, in the background
|
|
156
162
|
cli.io.err(cli.style.dim(` watching ${repo.slug} — Ctrl-C to stop${drainer ? " · auto-embed on" : ""}`));
|
|
157
163
|
await new Promise((resolveWait) => {
|