omgbase 0.11.0 → 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/oqx.js +1 -1
- 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/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 } from "./_mutate.js";
|
|
5
5
|
import { cmdUpdate as cmdUpdateBlock } from "./mutate.js";
|
|
6
6
|
import { remoteCall, 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,63 @@ 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
|
-
content: bytes
|
|
62
|
-
|
|
63
|
-
...(
|
|
64
|
-
});
|
|
65
|
-
opset = r.opset;
|
|
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
|
+
if (dryRun)
|
|
63
|
+
plan = await remoteCall(cli, "plan", { effects: [{ op: "set", target: { kind: "document", ...docLocator(doc) }, field: "content", value: bytes }] });
|
|
64
|
+
else
|
|
65
|
+
receipt = await remoteCall(cli, "docs_update", { ...docLocator(doc), content: bytes, ...(values.reason ? { reason: values.reason } : {}) });
|
|
67
66
|
}
|
|
68
67
|
else {
|
|
69
68
|
const ws = cli.workspace();
|
|
70
69
|
const repo = cli.repo(ws);
|
|
71
70
|
if (!repo.rootPath)
|
|
72
71
|
throw new CliUsageError(`repo '${repo.slug}' has no filesystem source; 'update' needs a working tree`);
|
|
73
|
-
const
|
|
72
|
+
const effects = [{ op: "set", target: { kind: "document", id: requireDoc(ws.store, repo.repoId, doc) }, field: "content", value: bytes }];
|
|
73
|
+
const req = {
|
|
74
74
|
repoId: repo.repoId,
|
|
75
75
|
rootPath: repo.rootPath,
|
|
76
|
+
effects,
|
|
77
|
+
origin: { actor: values.actor ?? defaultActor(), ...(values.reason ? { reason: values.reason } : {}) },
|
|
76
78
|
...(ws.omgbaseDir ? { omgbaseDir: ws.omgbaseDir } : {}),
|
|
77
|
-
...(values.actor ? { actor: values.actor } : {}),
|
|
78
79
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
if (dryRun) {
|
|
81
|
+
plan = surfacePlan(planEffects(ws.store, req));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const res = applyEffectsDetailed(ws.store, req);
|
|
85
|
+
plan = surfacePlan(res.plan);
|
|
86
|
+
receipt = res.receipt === undefined ? undefined : surfaceReceipt(res.receipt);
|
|
87
|
+
}
|
|
83
88
|
}
|
|
84
89
|
if (cli.flags.mode !== "human") {
|
|
85
|
-
cli.io.out(JSON.stringify(
|
|
90
|
+
cli.io.out(JSON.stringify(receipt ?? plan));
|
|
86
91
|
return EXIT_OK;
|
|
87
92
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
if (receipt === undefined) {
|
|
94
|
+
// The plan: its one effect names the document; `preview.identity[<id>]` is
|
|
95
|
+
// the opset plan (one line per op, an empty line, the summary).
|
|
96
|
+
const target = plan.effects[0] && "target" in plan.effects[0] ? plan.effects[0].target : undefined;
|
|
97
|
+
const docId = target && target.kind === "document" ? target.id : undefined;
|
|
98
|
+
const path = target && target.kind === "document" ? referencePath(target.path) : doc;
|
|
99
|
+
const identity = docId !== undefined ? plan.preview?.identity?.[docId] : undefined;
|
|
100
|
+
cli.io.err(cli.style.dim(` plan for ${cli.style.accent(path)}`));
|
|
101
|
+
if (identity)
|
|
102
|
+
cli.io.out(renderIdentityPlan(identity));
|
|
93
103
|
return EXIT_OK;
|
|
94
104
|
}
|
|
105
|
+
// The Receipt confirmation (§4): one revision per touched document; every id
|
|
106
|
+
// the plan's results name, once each (a block appears in several ops' results
|
|
107
|
+
// — an update then a retile of the same block — and is listed a single time).
|
|
95
108
|
const g = cli.render.g;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// lists it a single time.
|
|
100
|
-
for (const id of new Set(result.results.flatMap((r) => r.ids)))
|
|
109
|
+
const n = receipt.revisions.length;
|
|
110
|
+
cli.io.err(cli.style.dim(` ${cli.style.ok(g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
|
|
111
|
+
for (const id of new Set(receiptIds(receipt, plan)))
|
|
101
112
|
cli.io.out(id);
|
|
102
113
|
return EXIT_OK;
|
|
103
114
|
}
|
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/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.13.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.15.0",
|
|
49
49
|
"@omgbase/fs-adapter": "^0.2.0",
|
|
50
|
-
"@omgbase/sync": "^0.5.
|
|
50
|
+
"@omgbase/sync": "^0.5.8",
|
|
51
51
|
"yaml": "^2.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|