omgbase 0.11.1 → 0.16.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 +57 -16
- package/dist/src/cmd/_mutate.js +144 -53
- package/dist/src/cmd/_remote.d.ts +14 -0
- package/dist/src/cmd/_remote.js +28 -0
- package/dist/src/cmd/admin.js +74 -9
- 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 +44 -7
- package/dist/src/cmd/mutate.d.ts +1 -0
- package/dist/src/cmd/mutate.js +123 -64
- package/dist/src/cmd/node.js +7 -7
- package/dist/src/cmd/oqx.d.ts +3 -2
- package/dist/src/cmd/oqx.js +21 -8
- package/dist/src/cmd/retarget.js +29 -25
- package/dist/src/cmd/source.js +4 -2
- package/dist/src/cmd/sync.js +12 -6
- package/dist/src/cmd/update.js +55 -41
- package/dist/src/cmd/version.js +4 -1
- package/dist/src/commands.js +2 -1
- package/dist/src/context.js +3 -2
- package/dist/src/dispatch.js +9 -4
- package/dist/src/output.d.ts +9 -0
- package/dist/src/output.js +23 -0
- 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/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 {
|
|
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.
|
|
4
|
+
import { defaultActor, renderPlan as renderPlanDiffs, renderReceipt, unifiedDiffLines } from "./_mutate.js";
|
|
5
|
+
import { remoteCall, remoteSugar } from "./_remote.js";
|
|
10
6
|
async function runRetarget(cli, args) {
|
|
11
7
|
const { values, positionals } = parseArgs({
|
|
12
8
|
args,
|
|
@@ -29,32 +25,40 @@ 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
|
+
// `links_retarget` where listed (its result adds `hits`/`pairs`), else `apply { retarget }` (spec/cli §2.3, 4.2).
|
|
36
|
+
const r = await remoteSugar(cli, "links_retarget", { from_target: from, to_target: to, ...(values.scope ? { path_glob: values.scope } : {}) }, () => sugar.effects);
|
|
37
|
+
return renderReceipt(cli, r);
|
|
45
38
|
}
|
|
46
39
|
const ws = cli.workspace();
|
|
47
40
|
const repo = cli.repo(ws);
|
|
48
|
-
const {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
41
|
+
const request = { repoId: repo.repoId, ...(repo.rootPath ? { rootPath: repo.rootPath } : {}), omgbaseDir: ws.omgbaseDir };
|
|
42
|
+
if (!values.apply) {
|
|
43
|
+
// The plan (default): what WOULD change, commit nothing, mint nothing.
|
|
44
|
+
const plan = surfacePlan(planEffects(ws.store, { ...request, effects: sugar.effects }));
|
|
45
|
+
return renderHits(cli, from, to, retargetOf(plan).hits);
|
|
53
46
|
}
|
|
54
|
-
|
|
47
|
+
// --apply → commit (the global --dry-run plans and renders the file diffs instead, §3.6).
|
|
48
|
+
const { plan, receipt } = applySugar(ws.store, { ...request, actor: values.actor ?? defaultActor(), ...(cli.flags.dryRun ? { dryRun: true } : {}) }, sugar);
|
|
49
|
+
const rooted = surfacePlan(plan);
|
|
50
|
+
if (!receipt)
|
|
51
|
+
return renderPlanDiffs(cli, rooted);
|
|
52
|
+
const { hits, pairs } = retargetOf(rooted);
|
|
53
|
+
return renderReceipt(cli, { hits, pairs, ...surfaceReceipt(receipt) }, plan);
|
|
54
|
+
}
|
|
55
|
+
/** The one retarget effect's preview; a plan over no hit still carries it (empty hits, a zero count). */
|
|
56
|
+
function retargetOf(plan) {
|
|
57
|
+
const r = plan.preview?.retarget?.[0];
|
|
58
|
+
return { hits: r?.hits ?? [], pairs: r?.pairs ?? [] };
|
|
55
59
|
}
|
|
56
60
|
// Plan (default): show what WOULD change, commit nothing.
|
|
57
|
-
function
|
|
61
|
+
function renderHits(cli, from, to, hits) {
|
|
58
62
|
if (cli.flags.mode === "json") {
|
|
59
63
|
cli.io.out(JSON.stringify({ from, to, hits }));
|
|
60
64
|
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) => {
|
package/dist/src/cmd/update.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import {
|
|
2
|
+
import { applyEffectsDetailed, planEffects, referencePath, renderIdentityPlan, surfacePlan, surfaceReceipt } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
-
import { readContent, extractContentOpts } from "./_mutate.js";
|
|
4
|
+
import { readContent, extractContentOpts, receiptIds, defaultActor, renderSummary } from "./_mutate.js";
|
|
5
5
|
import { cmdUpdate as cmdUpdateBlock } from "./mutate.js";
|
|
6
|
-
import { remoteCall, docLocator, requireDoc } from "./_remote.js";
|
|
6
|
+
import { remoteCall, remoteSugar, docLocator, requireDoc } from "./_remote.js";
|
|
7
7
|
// `omg update <target> (-f file | -)`: a polymorphic-target command (the omgbase
|
|
8
8
|
// north-star — every command accepts any entity ref). A block id (b_…) replaces
|
|
9
9
|
// that block's markdown (CAS-pinned, the low-level op). A doc id or path is the
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// whole-document update: the complete proposed document is ONE `set content`
|
|
11
|
+
// effect (spec/mutate §11.3; the `docs_update` tool's sugar, spec/surface 4.0) —
|
|
12
|
+
// the engine reconciles it against the current tree, preserves stable block ids
|
|
13
|
+
// for recognizably-same structure, and commits the derived ops as one
|
|
14
|
+
// transaction. The global --dry-run is the plan: it prints the identity
|
|
15
|
+
// consequences from the Plan's `preview.identity` (spec/cli §3.6, 4.0: every dry
|
|
16
|
+
// run is a plan and burns no id) and commits nothing.
|
|
15
17
|
function runUpdate(cli, args) {
|
|
16
18
|
// Peek at the target to dispatch: a `b_` token is a block replace — a token
|
|
17
19
|
// that names no live block fails there as block_missing (spec/cli §6), never
|
|
@@ -28,18 +30,18 @@ async function runDocUpdate(cli, args) {
|
|
|
28
30
|
const { values, positionals } = parseArgs({
|
|
29
31
|
args: content.rest,
|
|
30
32
|
allowPositionals: true,
|
|
31
|
-
options: { actor: { type: "string" }, reason: { type: "string" },
|
|
33
|
+
options: { actor: { type: "string" }, reason: { type: "string" }, help: { type: "boolean" } },
|
|
32
34
|
});
|
|
33
35
|
if (values.help) {
|
|
34
36
|
return renderHelp(cli, {
|
|
35
37
|
name: "update",
|
|
36
38
|
summary: "Replace a block's markdown (b_… target), or reconcile a whole document from complete new bytes (doc id/path)",
|
|
37
|
-
usage: "update <block|doc> (-m <markdown> | -f <file> | -) [--
|
|
39
|
+
usage: "update <block|doc> (-m <markdown> | -f <file> | -) [--expect <hash>] [--reason <s>] [--actor <s>] [--dry-run]",
|
|
38
40
|
options: [
|
|
39
41
|
["-m <markdown>", "content inline"],
|
|
40
42
|
["-f <file>", "content from a file"],
|
|
41
43
|
["-", "content from stdin"],
|
|
42
|
-
["--
|
|
44
|
+
["--dry-run", "(doc) print the reconciliation plan — identity effects + summary — and commit nothing"],
|
|
43
45
|
["--expect <hash>", "(block) fail with stale_expectation unless the block's hash still matches"],
|
|
44
46
|
["--reason <s>", "commit reason recorded in history"],
|
|
45
47
|
["--actor <s>", "commit actor (default human:$USER)"],
|
|
@@ -50,54 +52,66 @@ async function runDocUpdate(cli, args) {
|
|
|
50
52
|
if (!doc)
|
|
51
53
|
throw new CliUsageError("update requires a <doc> (id or path)");
|
|
52
54
|
const bytes = readContent(content);
|
|
53
|
-
const dryRun =
|
|
54
|
-
let
|
|
55
|
-
let
|
|
55
|
+
const dryRun = cli.flags.dryRun;
|
|
56
|
+
let plan;
|
|
57
|
+
let receipt;
|
|
56
58
|
if (cli.flags.server) {
|
|
57
|
-
// Remote: docs_update
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
result = r.result;
|
|
59
|
+
// Remote: the `docs_update` tool commits the `set content` server-side and
|
|
60
|
+
// returns the Receipt (the confirmation then lists the ids it minted, as a
|
|
61
|
+
// remote `apply` does); the dry run is the `plan` tool over the same effect.
|
|
62
|
+
// A server without `docs_update` (a minimal profile) gets `apply` over the same effect (spec/cli §2.3, 4.2).
|
|
63
|
+
const setContent = { op: "set", target: { kind: "document", ...docLocator(doc) }, field: "content", value: bytes };
|
|
64
|
+
if (dryRun)
|
|
65
|
+
plan = await remoteCall(cli, "plan", { effects: [setContent] });
|
|
66
|
+
else
|
|
67
|
+
receipt = await remoteSugar(cli, "docs_update", { ...docLocator(doc), content: bytes, ...(values.reason ? { reason: values.reason } : {}) }, () => [setContent]);
|
|
67
68
|
}
|
|
68
69
|
else {
|
|
69
70
|
const ws = cli.workspace();
|
|
70
71
|
const repo = cli.repo(ws);
|
|
71
72
|
if (!repo.rootPath)
|
|
72
73
|
throw new CliUsageError(`repo '${repo.slug}' has no filesystem source; 'update' needs a working tree`);
|
|
73
|
-
const
|
|
74
|
+
const effects = [{ op: "set", target: { kind: "document", id: requireDoc(ws.store, repo.repoId, doc) }, field: "content", value: bytes }];
|
|
75
|
+
const req = {
|
|
74
76
|
repoId: repo.repoId,
|
|
75
77
|
rootPath: repo.rootPath,
|
|
78
|
+
effects,
|
|
79
|
+
origin: { actor: values.actor ?? defaultActor(), ...(values.reason ? { reason: values.reason } : {}) },
|
|
76
80
|
...(ws.omgbaseDir ? { omgbaseDir: ws.omgbaseDir } : {}),
|
|
77
|
-
...(values.actor ? { actor: values.actor } : {}),
|
|
78
81
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
if (dryRun) {
|
|
83
|
+
plan = surfacePlan(planEffects(ws.store, req));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const res = applyEffectsDetailed(ws.store, req);
|
|
87
|
+
plan = surfacePlan(res.plan);
|
|
88
|
+
receipt = res.receipt === undefined ? undefined : surfaceReceipt(res.receipt);
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
if (cli.flags.mode !== "human") {
|
|
85
|
-
cli.io.out(JSON.stringify(
|
|
92
|
+
cli.io.out(JSON.stringify(receipt ?? plan));
|
|
86
93
|
return EXIT_OK;
|
|
87
94
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
if (receipt === undefined) {
|
|
96
|
+
// The plan: its one effect names the document; `preview.identity[<id>]` is
|
|
97
|
+
// the opset plan (one line per op, an empty line, the summary).
|
|
98
|
+
const target = plan.effects[0] && "target" in plan.effects[0] ? plan.effects[0].target : undefined;
|
|
99
|
+
const docId = target && target.kind === "document" ? target.id : undefined;
|
|
100
|
+
const path = target && target.kind === "document" ? referencePath(target.path) : doc;
|
|
101
|
+
const identity = docId !== undefined ? plan.preview?.identity?.[docId] : undefined;
|
|
102
|
+
cli.io.err(cli.style.dim(` plan for ${cli.style.accent(path)}`));
|
|
103
|
+
if (identity)
|
|
104
|
+
cli.io.out(renderIdentityPlan(identity));
|
|
93
105
|
return EXIT_OK;
|
|
94
106
|
}
|
|
107
|
+
// The Receipt confirmation (§4): one revision per touched document; every id
|
|
108
|
+
// the plan's results name, once each (a block appears in several ops' results
|
|
109
|
+
// — an update then a retile of the same block — and is listed a single time).
|
|
95
110
|
const g = cli.render.g;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const id of new Set(result.results.flatMap((r) => r.ids)))
|
|
111
|
+
const n = receipt.revisions.length;
|
|
112
|
+
cli.io.err(cli.style.dim(` ${cli.style.ok(g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
|
|
113
|
+
renderSummary(cli, receipt.summary);
|
|
114
|
+
for (const id of new Set(receiptIds(receipt, plan)))
|
|
101
115
|
cli.io.out(id);
|
|
102
116
|
return EXIT_OK;
|
|
103
117
|
}
|
package/dist/src/cmd/version.js
CHANGED
|
@@ -41,7 +41,7 @@ async function runVersion(cli, args) {
|
|
|
41
41
|
return emitMachine(cli, info);
|
|
42
42
|
const { io, style } = cli;
|
|
43
43
|
const dash = "—";
|
|
44
|
-
const keys = ["engine", "version", "specs", "schema", "mcp", "runtime", "commit", "built"];
|
|
44
|
+
const keys = ["engine", "version", "specs", "schema", "mcp", "runtime", "commit", "built", "profile"];
|
|
45
45
|
const width = Math.max(...keys.map((k) => k.length));
|
|
46
46
|
const line = (key, value) => {
|
|
47
47
|
io.out(`${style.dim(key.padEnd(width))} ${value === null ? style.dim(dash) : String(value)}`);
|
|
@@ -58,6 +58,9 @@ async function runVersion(cli, args) {
|
|
|
58
58
|
line("runtime", info.runtime);
|
|
59
59
|
line("commit", info.commit);
|
|
60
60
|
line("built", info.built);
|
|
61
|
+
// The catalog profile served (spec/surface §4 "Profiles", 4.3): `full` for
|
|
62
|
+
// the library itself; over --server, whatever the remote serves.
|
|
63
|
+
line("profile", info.profile);
|
|
61
64
|
return EXIT_OK;
|
|
62
65
|
}
|
|
63
66
|
export const cmdVersion = {
|
package/dist/src/commands.js
CHANGED
|
@@ -13,7 +13,7 @@ import { cmdDiff } from "./cmd/diff.js";
|
|
|
13
13
|
import { cmdLinks } from "./cmd/links.js";
|
|
14
14
|
import { cmdSync } from "./cmd/sync.js";
|
|
15
15
|
import { cmdMcp } from "./cmd/mcp.js";
|
|
16
|
-
import { cmdApply, cmdInsert, cmdMove, cmdRm, cmdDone, cmdAppend, cmdSplit, cmdMerge } from "./cmd/mutate.js";
|
|
16
|
+
import { cmdApply, cmdExecute, cmdInsert, cmdMove, cmdRm, cmdDone, cmdAppend, cmdSplit, cmdMerge } from "./cmd/mutate.js";
|
|
17
17
|
import { cmdEdit } from "./cmd/edit.js";
|
|
18
18
|
import { cmdRetarget } from "./cmd/retarget.js";
|
|
19
19
|
import { cmdNode } from "./cmd/node.js";
|
|
@@ -40,6 +40,7 @@ export const COMMANDS = [
|
|
|
40
40
|
cmdDiff,
|
|
41
41
|
cmdLinks,
|
|
42
42
|
cmdApply,
|
|
43
|
+
cmdExecute,
|
|
43
44
|
cmdInsert,
|
|
44
45
|
cmdUpdateDoc,
|
|
45
46
|
cmdEdit,
|
package/dist/src/context.js
CHANGED
|
@@ -85,8 +85,9 @@ export const REMOTE_OK = new Set([
|
|
|
85
85
|
// doc-level mutators
|
|
86
86
|
"new", "mv", "meta", "rm", "update", "retarget",
|
|
87
87
|
// block-level sugar (server-side ref resolution + CAS via the blocks_* /
|
|
88
|
-
// blocks_check / blocks_append / nodes_set / apply tools)
|
|
89
|
-
|
|
88
|
+
// blocks_check / blocks_append / nodes_set / apply tools); `execute` is the
|
|
89
|
+
// `execute` tool's plan form
|
|
90
|
+
"apply", "execute", "insert", "move", "split", "merge", "done", "append", "node",
|
|
90
91
|
// shell accepts --server itself, then threads it into every line it runs.
|
|
91
92
|
"shell",
|
|
92
93
|
// version asks the REMOTE engine which engine/versions it is (spec/cli §6).
|
package/dist/src/dispatch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { freshnessSweep, watchLeaseLive } from "@omgbase/core";
|
|
1
|
+
import { freshnessSweep, watchLeaseLive, withWriterLock } from "@omgbase/core";
|
|
2
2
|
import { NO_WORKSPACE_OK, SKIP_FRESHNESS, REMOTE_OK, asksForHelp } from "./context.js";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, renderError, usageFromParseArgs } from "./output.js";
|
|
4
4
|
import { resolveCommand, unknownCommandError } from "./commands.js";
|
|
@@ -135,7 +135,11 @@ export async function runCommand(cli, command, rest) {
|
|
|
135
135
|
try {
|
|
136
136
|
// Freshness sweep (11 §3.3): current-by-default, unless --stale, a live
|
|
137
137
|
// watcher holds the lease, remote (`--server`) mode (no local tree to sweep),
|
|
138
|
-
// or the command manages sync itself.
|
|
138
|
+
// or the command manages sync itself. The sweep is a checkpoint ingest, so it
|
|
139
|
+
// runs under the workspace writer lock (spec/sync §7) like every other
|
|
140
|
+
// host's; a writer that follows re-enters the lock. A lock held past the
|
|
141
|
+
// wait fails the command as the catch-all engine error (exit 1), like any
|
|
142
|
+
// write would — `--stale` skips the sweep and so never contends.
|
|
139
143
|
if (!help && !cli.flags.stale && cli.flags.server === undefined && !SKIP_FRESHNESS.has(resolved.name) && !NO_WORKSPACE_OK.has(resolved.name)) {
|
|
140
144
|
const ws = cli.workspace();
|
|
141
145
|
if (!watchLeaseLive(ws.omgbaseDir)) {
|
|
@@ -145,8 +149,9 @@ export async function runCommand(cli, command, rest) {
|
|
|
145
149
|
// repo_not_found if it truly needs a repo.
|
|
146
150
|
try {
|
|
147
151
|
const repo = cli.repo(ws);
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
const root = repo.rootPath;
|
|
153
|
+
if (root)
|
|
154
|
+
withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root));
|
|
150
155
|
}
|
|
151
156
|
catch (err) {
|
|
152
157
|
if (!(err instanceof EngineErrorLike && err.code === "repo_not_found"))
|
package/dist/src/output.d.ts
CHANGED
|
@@ -103,6 +103,15 @@ export declare function renderHits(cli: {
|
|
|
103
103
|
io: IO;
|
|
104
104
|
style: Style;
|
|
105
105
|
}, hits: HitLike[]): void;
|
|
106
|
+
/** The bare hit table (`query --bare`, spec/cli §4, 4.1): a hit is its
|
|
107
|
+
* projection alone (`spec/surface` §1.4, 4.2), so the columns are every key in
|
|
108
|
+
* first-seen order across the hits — a projected `id`/`path` is an ordinary
|
|
109
|
+
* column, rendered as any other cell — with no envelope columns before them.
|
|
110
|
+
* Header row, then one row per hit; cells as the enveloped table's. */
|
|
111
|
+
export declare function renderBareHits(cli: {
|
|
112
|
+
io: IO;
|
|
113
|
+
style: Style;
|
|
114
|
+
}, hits: Record<string, unknown>[]): void;
|
|
106
115
|
/** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
|
|
107
116
|
export declare function truncationFooter(io: IO, style: Style, cursor: number | string): void;
|
|
108
117
|
export {};
|
package/dist/src/output.js
CHANGED
|
@@ -231,6 +231,29 @@ export function renderHits(cli, hits) {
|
|
|
231
231
|
for (const line of columns(rows))
|
|
232
232
|
io.out(line);
|
|
233
233
|
}
|
|
234
|
+
/** The bare hit table (`query --bare`, spec/cli §4, 4.1): a hit is its
|
|
235
|
+
* projection alone (`spec/surface` §1.4, 4.2), so the columns are every key in
|
|
236
|
+
* first-seen order across the hits — a projected `id`/`path` is an ordinary
|
|
237
|
+
* column, rendered as any other cell — with no envelope columns before them.
|
|
238
|
+
* Header row, then one row per hit; cells as the enveloped table's. */
|
|
239
|
+
export function renderBareHits(cli, hits) {
|
|
240
|
+
const { io, style } = cli;
|
|
241
|
+
const cols = [];
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
for (const h of hits) {
|
|
244
|
+
for (const k of Object.keys(h)) {
|
|
245
|
+
if (seen.has(k))
|
|
246
|
+
continue;
|
|
247
|
+
seen.add(k);
|
|
248
|
+
cols.push(k);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const rows = [cols.map((c) => style.dim(c))];
|
|
252
|
+
for (const h of hits)
|
|
253
|
+
rows.push(cols.map((c) => hitCell(h[c])));
|
|
254
|
+
for (const line of columns(rows))
|
|
255
|
+
io.out(line);
|
|
256
|
+
}
|
|
234
257
|
/** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
|
|
235
258
|
export function truncationFooter(io, style, cursor) {
|
|
236
259
|
io.err(style.dim(`… truncated; continue with --cursor ${cursor}`));
|
package/dist/src/seams.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
export declare const WRITER_LOCK_TIMEOUT_ENV = "OMGBASE_WRITER_LOCK_TIMEOUT_MS";
|
|
1
2
|
export interface SpecSeams {
|
|
2
3
|
/** the sequential minter is installed */
|
|
3
4
|
minter: boolean;
|
|
4
5
|
/** the pinned instant (canonical ISO form), when the clock is pinned */
|
|
5
6
|
clock: string | null;
|
|
7
|
+
/** the writer lock's wait in ms, when overridden */
|
|
8
|
+
writerLockTimeoutMs: number | null;
|
|
6
9
|
}
|
|
7
10
|
/**
|
|
8
11
|
* Read + install the seams (idempotent: a second call returns what the first
|
package/dist/src/seams.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
|
-
import { pinClock, sequentialMinter, setIdMinter } from "@omgbase/core";
|
|
1
|
+
import { pinClock, sequentialMinter, setIdMinter, setWriterLockDefaults } from "@omgbase/core";
|
|
2
2
|
import { CliUsageError } from "./output.js";
|
|
3
|
+
// The two conformance seams of spec/surface §7.1 (and spec/cli §2.6), read
|
|
4
|
+
// from the environment at process start — BEFORE any workspace opens, so no id
|
|
5
|
+
// is minted and no timestamp read without them. Every verb honors them (the
|
|
6
|
+
// CLI fixtures of spec/cli run each verb under them); `omg mcp` additionally
|
|
7
|
+
// announces them on stderr. For conformance runs only — never set in production:
|
|
8
|
+
//
|
|
9
|
+
// OMGBASE_SPEC_MINTER=sequential the fixture minter of spec/store §2.2 for
|
|
10
|
+
// the whole process (`d_0, d_1, …`, each
|
|
11
|
+
// prefix from 0); any other value is a
|
|
12
|
+
// usage error (exit 2)
|
|
13
|
+
// OMGBASE_SPEC_CLOCK=<RFC 3339> "now" is that instant for the whole
|
|
14
|
+
// process: every commit a verb stamps, every
|
|
15
|
+
// `ts` reported as current; not an instant →
|
|
16
|
+
// usage error (exit 2)
|
|
17
|
+
//
|
|
18
|
+
// A third seam is this engine's own, not the spec's (the Rust binary reads the
|
|
19
|
+
// same variable):
|
|
20
|
+
//
|
|
21
|
+
// OMGBASE_WRITER_LOCK_TIMEOUT_MS=<n> how long a write waits for the workspace
|
|
22
|
+
// writer lock (spec/sync §7: 5 s when unset)
|
|
23
|
+
// — for a test that proves a held lock fails
|
|
24
|
+
// a write without sitting out the full wait.
|
|
25
|
+
// Honored by every lock this process takes
|
|
26
|
+
// (a verb's kernel-level lock, `omg mcp`'s
|
|
27
|
+
// tool boundary, the watcher's checkpoints);
|
|
28
|
+
// empty = unset, not an integer → usage error
|
|
29
|
+
export const WRITER_LOCK_TIMEOUT_ENV = "OMGBASE_WRITER_LOCK_TIMEOUT_MS";
|
|
3
30
|
let installed = null;
|
|
4
31
|
/**
|
|
5
32
|
* Read + install the seams (idempotent: a second call returns what the first
|
|
@@ -8,7 +35,7 @@ let installed = null;
|
|
|
8
35
|
export function installSpecSeams(env = process.env) {
|
|
9
36
|
if (installed)
|
|
10
37
|
return installed;
|
|
11
|
-
const seams = { minter: false, clock: null };
|
|
38
|
+
const seams = { minter: false, clock: null, writerLockTimeoutMs: null };
|
|
12
39
|
const minter = env.OMGBASE_SPEC_MINTER;
|
|
13
40
|
if (minter !== undefined && minter !== "") {
|
|
14
41
|
if (minter !== "sequential") {
|
|
@@ -27,11 +54,20 @@ export function installSpecSeams(env = process.env) {
|
|
|
27
54
|
}
|
|
28
55
|
seams.clock = new Date().toISOString();
|
|
29
56
|
}
|
|
57
|
+
const lockWait = env[WRITER_LOCK_TIMEOUT_ENV];
|
|
58
|
+
if (lockWait !== undefined && lockWait !== "") {
|
|
59
|
+
const ms = /^\s*\d+\s*$/.test(lockWait) ? Number(lockWait) : NaN;
|
|
60
|
+
if (!Number.isSafeInteger(ms)) {
|
|
61
|
+
throw new CliUsageError(`${WRITER_LOCK_TIMEOUT_ENV}=${JSON.stringify(lockWait)}: not a number of milliseconds`, `unset it, or e.g. ${WRITER_LOCK_TIMEOUT_ENV}=200 for a test run`);
|
|
62
|
+
}
|
|
63
|
+
setWriterLockDefaults({ timeoutMs: ms });
|
|
64
|
+
seams.writerLockTimeoutMs = ms;
|
|
65
|
+
}
|
|
30
66
|
installed = seams;
|
|
31
67
|
return seams;
|
|
32
68
|
}
|
|
33
69
|
/** The seams installed by `installSpecSeams` (none when it has not run). */
|
|
34
70
|
export function activeSpecSeams() {
|
|
35
|
-
return installed ?? { minter: false, clock: null };
|
|
71
|
+
return installed ?? { minter: false, clock: null, writerLockTimeoutMs: null };
|
|
36
72
|
}
|
|
37
73
|
//# sourceMappingURL=seams.js.map
|
package/dist/src/shell/refs.js
CHANGED
|
@@ -43,16 +43,9 @@ export function deriveRows(value) {
|
|
|
43
43
|
return o.hits.map(rowOf); // query/oqx
|
|
44
44
|
if (Array.isArray(o.digests))
|
|
45
45
|
return o.digests.map(rowOf); // log
|
|
46
|
-
if (
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
for (const r of o.results) {
|
|
50
|
-
if (Array.isArray(r.ids))
|
|
51
|
-
for (const id of r.ids)
|
|
52
|
-
if (typeof id === "string")
|
|
53
|
-
ids.push(id);
|
|
54
|
-
}
|
|
55
|
-
return ids.map((id) => ({ ref: id, label: id, value: id }));
|
|
46
|
+
if (typeof o.plan_id === "string" && Array.isArray(o.ids)) {
|
|
47
|
+
// A write's Receipt as the verbs capture it: the ids the write names (one row per id).
|
|
48
|
+
return o.ids.filter((id) => typeof id === "string").map((id) => ({ ref: id, label: id, value: id }));
|
|
56
49
|
}
|
|
57
50
|
if (Array.isArray(o.in) || Array.isArray(o.out)) {
|
|
58
51
|
// links: both directions, each edge's far node is the selectable ref.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omgbase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Open Markdown Graph Base — CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"omg": "./dist/src/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@omgbase/core": "^0.
|
|
48
|
+
"@omgbase/core": "^0.19.0",
|
|
49
49
|
"@omgbase/fs-adapter": "^0.2.0",
|
|
50
|
-
"@omgbase/sync": "^0.5.
|
|
50
|
+
"@omgbase/sync": "^0.5.12",
|
|
51
51
|
"yaml": "^2.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|