omgbase 0.1.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/LICENSE +21 -0
- package/dist/src/cmd/_embed.d.ts +55 -0
- package/dist/src/cmd/_embed.js +102 -0
- package/dist/src/cmd/_mutate.d.ts +47 -0
- package/dist/src/cmd/_mutate.js +211 -0
- package/dist/src/cmd/_remote.d.ts +8 -0
- package/dist/src/cmd/_remote.js +61 -0
- package/dist/src/cmd/_source.d.ts +13 -0
- package/dist/src/cmd/_source.js +47 -0
- package/dist/src/cmd/admin.d.ts +6 -0
- package/dist/src/cmd/admin.js +289 -0
- package/dist/src/cmd/bootstrap.d.ts +3 -0
- package/dist/src/cmd/bootstrap.js +252 -0
- package/dist/src/cmd/cat.d.ts +2 -0
- package/dist/src/cmd/cat.js +80 -0
- package/dist/src/cmd/diff.d.ts +2 -0
- package/dist/src/cmd/diff.js +76 -0
- package/dist/src/cmd/docs.d.ts +6 -0
- package/dist/src/cmd/docs.js +154 -0
- package/dist/src/cmd/edit.d.ts +2 -0
- package/dist/src/cmd/edit.js +74 -0
- package/dist/src/cmd/find.d.ts +2 -0
- package/dist/src/cmd/find.js +98 -0
- package/dist/src/cmd/help.d.ts +2 -0
- package/dist/src/cmd/help.js +47 -0
- package/dist/src/cmd/hist.d.ts +2 -0
- package/dist/src/cmd/hist.js +58 -0
- package/dist/src/cmd/links.d.ts +2 -0
- package/dist/src/cmd/links.js +76 -0
- package/dist/src/cmd/log.d.ts +2 -0
- package/dist/src/cmd/log.js +109 -0
- package/dist/src/cmd/ls.d.ts +2 -0
- package/dist/src/cmd/ls.js +88 -0
- package/dist/src/cmd/mcp.d.ts +2 -0
- package/dist/src/cmd/mcp.js +192 -0
- package/dist/src/cmd/mutate.d.ts +10 -0
- package/dist/src/cmd/mutate.js +352 -0
- package/dist/src/cmd/node.d.ts +2 -0
- package/dist/src/cmd/node.js +66 -0
- package/dist/src/cmd/oqx.d.ts +2 -0
- package/dist/src/cmd/oqx.js +193 -0
- package/dist/src/cmd/outline.d.ts +2 -0
- package/dist/src/cmd/outline.js +87 -0
- package/dist/src/cmd/retarget.d.ts +2 -0
- package/dist/src/cmd/retarget.js +91 -0
- package/dist/src/cmd/run.d.ts +2 -0
- package/dist/src/cmd/run.js +135 -0
- package/dist/src/cmd/shell.d.ts +2 -0
- package/dist/src/cmd/shell.js +159 -0
- package/dist/src/cmd/show.d.ts +2 -0
- package/dist/src/cmd/show.js +107 -0
- package/dist/src/cmd/source.d.ts +2 -0
- package/dist/src/cmd/source.js +231 -0
- package/dist/src/cmd/status.d.ts +2 -0
- package/dist/src/cmd/status.js +95 -0
- package/dist/src/cmd/sync.d.ts +2 -0
- package/dist/src/cmd/sync.js +176 -0
- package/dist/src/cmd/update.d.ts +2 -0
- package/dist/src/cmd/update.js +100 -0
- package/dist/src/commands.d.ts +18 -0
- package/dist/src/commands.js +112 -0
- package/dist/src/context.d.ts +73 -0
- package/dist/src/context.js +80 -0
- package/dist/src/dispatch.d.ts +8 -0
- package/dist/src/dispatch.js +148 -0
- package/dist/src/main.d.ts +3 -0
- package/dist/src/main.js +67 -0
- package/dist/src/output.d.ts +60 -0
- package/dist/src/output.js +131 -0
- package/dist/src/render.d.ts +44 -0
- package/dist/src/render.js +116 -0
- package/dist/src/shell/refs.d.ts +37 -0
- package/dist/src/shell/refs.js +111 -0
- package/dist/src/shell/session.d.ts +71 -0
- package/dist/src/shell/session.js +317 -0
- package/dist/src/shell/tokenize.d.ts +3 -0
- package/dist/src/shell/tokenize.js +79 -0
- package/dist/src/style.d.ts +58 -0
- package/dist/src/style.js +159 -0
- package/package.json +63 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolveRef, tasksComplete, sectionsAppend, } from "@omgbase/core";
|
|
4
|
+
import { CliUsageError, EngineErrorLike, renderHelp } from "../output.js";
|
|
5
|
+
import { runOps, runOpsRemote, readContent, readStdin, expandBlockArgs, extractContentOpts } from "./_mutate.js";
|
|
6
|
+
import { runRmDoc } from "./docs.js";
|
|
7
|
+
// Write sugar (11 §5.6): each command builds kernel Ops and applies them as one
|
|
8
|
+
// changeset via the shared runOps helper (writer flock, actor default, dry-run,
|
|
9
|
+
// conflict rendering). `apply` is the primitive; the rest are sugar.
|
|
10
|
+
function block(cli, ws, repoId, ref) {
|
|
11
|
+
const r = resolveRef(ws.store, repoId, ref);
|
|
12
|
+
if (!r || r.kind !== "block")
|
|
13
|
+
throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
|
|
14
|
+
return r.blockId;
|
|
15
|
+
}
|
|
16
|
+
function rawHashOf(ws, blockId) {
|
|
17
|
+
const row = ws.store.db
|
|
18
|
+
.prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
|
|
19
|
+
.get(blockId);
|
|
20
|
+
return row?.h ?? null;
|
|
21
|
+
}
|
|
22
|
+
// Parse an `--at` spec: end|start|before X|after X (default end).
|
|
23
|
+
function parseAt(values) {
|
|
24
|
+
const at = values.at?.trim();
|
|
25
|
+
if (!at || at === "end")
|
|
26
|
+
return "end";
|
|
27
|
+
if (at === "start")
|
|
28
|
+
return "start";
|
|
29
|
+
const m = /^(before|after)\s+(.+)$/.exec(at);
|
|
30
|
+
if (m)
|
|
31
|
+
return m[1] === "before" ? { before: m[2] } : { after: m[2] };
|
|
32
|
+
throw new CliUsageError(`bad --at '${at}' (use end|start|before <id>|after <id>)`);
|
|
33
|
+
}
|
|
34
|
+
// ---- apply (primitive) ------------------------------------------------------
|
|
35
|
+
async function runApply(cli, args) {
|
|
36
|
+
const { values } = parseArgs({
|
|
37
|
+
args,
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
options: { f: { type: "string", short: "f" }, reason: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
40
|
+
});
|
|
41
|
+
if (values.help) {
|
|
42
|
+
return renderHelp(cli, {
|
|
43
|
+
name: "apply",
|
|
44
|
+
summary: "Apply a raw changeset (`{ ops: [...] }`, the six-op kernel) — the primitive every other mutator expands to",
|
|
45
|
+
usage: "apply [-f <changeset.json|->] [--reason <s>] [--actor <s>] [--dry-run]",
|
|
46
|
+
options: [
|
|
47
|
+
["-f <file|->", "changeset JSON from a file or stdin (default: stdin)"],
|
|
48
|
+
["--reason <s>", "commit reason recorded in history"],
|
|
49
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
50
|
+
["--dry-run", "validate + render the diff, commit nothing"],
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const raw = values.f ? (values.f === "-" ? readStdin() : readFileSync(values.f, "utf8")) : readStdin();
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!parsed.ops || !Array.isArray(parsed.ops))
|
|
57
|
+
throw new CliUsageError("changeset must have an `ops` array");
|
|
58
|
+
if (cli.flags.server)
|
|
59
|
+
return runOpsRemote(cli, "apply", { ops: parsed.ops, ...(values.reason ? { reason: values.reason } : {}) });
|
|
60
|
+
const ws = cli.workspace();
|
|
61
|
+
const repo = cli.repo(ws);
|
|
62
|
+
return runOps(cli, ws, repo, parsed.ops, { ...(values.reason ? { reason: values.reason } : {}), ...(values.actor ? { actor: values.actor } : {}) });
|
|
63
|
+
}
|
|
64
|
+
// ---- insert -----------------------------------------------------------------
|
|
65
|
+
async function runInsert(cli, args) {
|
|
66
|
+
const content = extractContentOpts(args);
|
|
67
|
+
const { values, positionals } = parseArgs({
|
|
68
|
+
args: content.rest,
|
|
69
|
+
allowPositionals: true,
|
|
70
|
+
options: { at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
71
|
+
});
|
|
72
|
+
if (values.help) {
|
|
73
|
+
return renderHelp(cli, {
|
|
74
|
+
name: "insert",
|
|
75
|
+
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]",
|
|
77
|
+
options: [
|
|
78
|
+
["<to>", "parent block id, or a heading block id to append into its section"],
|
|
79
|
+
["-m <markdown>", "content inline"],
|
|
80
|
+
["-f <file>", "content from a file"],
|
|
81
|
+
["-", "content from stdin"],
|
|
82
|
+
["--at <pos>", "end (default) | start | before <id> | after <id>"],
|
|
83
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const to = positionals[0];
|
|
88
|
+
if (!to)
|
|
89
|
+
throw new CliUsageError("insert requires a <to> parent (block id, or a heading id for section append)");
|
|
90
|
+
const markdown = readContent(content);
|
|
91
|
+
if (cli.flags.server)
|
|
92
|
+
return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values) });
|
|
93
|
+
const ws = cli.workspace();
|
|
94
|
+
const repo = cli.repo(ws);
|
|
95
|
+
const parent = block(cli, ws, repo.repoId, to);
|
|
96
|
+
const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown };
|
|
97
|
+
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
98
|
+
}
|
|
99
|
+
// ---- update -----------------------------------------------------------------
|
|
100
|
+
async function runUpdate(cli, args) {
|
|
101
|
+
const content = extractContentOpts(args);
|
|
102
|
+
const { values, positionals } = parseArgs({
|
|
103
|
+
args: content.rest,
|
|
104
|
+
allowPositionals: true,
|
|
105
|
+
options: { expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
106
|
+
});
|
|
107
|
+
if (values.help) {
|
|
108
|
+
return renderHelp(cli, {
|
|
109
|
+
name: "update",
|
|
110
|
+
summary: "Replace a block's markdown, with compare-and-swap against the block's current hash",
|
|
111
|
+
usage: "update <block> (-m <markdown> | -f <file> | -) [--expect <hash>] [--actor <s>] [--dry-run]",
|
|
112
|
+
options: [
|
|
113
|
+
["-m <markdown>", "content inline"],
|
|
114
|
+
["-f <file>", "content from a file"],
|
|
115
|
+
["-", "content from stdin"],
|
|
116
|
+
["--expect <hash>", "fail with stale_expectation unless the block's hash still matches"],
|
|
117
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const ref = positionals[0];
|
|
122
|
+
if (!ref)
|
|
123
|
+
throw new CliUsageError("update requires a <block>");
|
|
124
|
+
const markdown = readContent(content);
|
|
125
|
+
// Remote: the server pins CAS from current bytes when --expect is absent.
|
|
126
|
+
if (cli.flags.server)
|
|
127
|
+
return runOpsRemote(cli, "blocks_update", { block: ref, markdown, ...(values.expect ? { expect: { content_hash: values.expect } } : {}) });
|
|
128
|
+
const ws = cli.workspace();
|
|
129
|
+
const repo = cli.repo(ws);
|
|
130
|
+
const blockId = block(cli, ws, repo.repoId, ref);
|
|
131
|
+
// CAS (§5.7): scripted callers pass --expect; interactive callers omit it and
|
|
132
|
+
// we pin the just-read hash (protects against concurrent edits, not against
|
|
133
|
+
// edits since a read the caller never made).
|
|
134
|
+
const expect = values.expect ?? rawHashOf(ws, blockId);
|
|
135
|
+
if (!values.expect) {
|
|
136
|
+
cli.io.err(cli.style.dim(` updating ${ref} (CAS pinned from current bytes)`));
|
|
137
|
+
}
|
|
138
|
+
const op = { op: "update", block: blockId, markdown, ...(expect ? { expect: { content_hash: expect } } : {}) };
|
|
139
|
+
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
140
|
+
}
|
|
141
|
+
// ---- move -------------------------------------------------------------------
|
|
142
|
+
async function runMove(cli, args) {
|
|
143
|
+
const { values, positionals } = parseArgs({
|
|
144
|
+
args,
|
|
145
|
+
allowPositionals: true,
|
|
146
|
+
options: { to: { type: "string" }, at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
147
|
+
});
|
|
148
|
+
if (values.help) {
|
|
149
|
+
return renderHelp(cli, {
|
|
150
|
+
name: "move",
|
|
151
|
+
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]",
|
|
153
|
+
options: [
|
|
154
|
+
["<blocks…>", "block ids; `-` reads them from stdin, one per line"],
|
|
155
|
+
["--to <parent>", "destination parent block"],
|
|
156
|
+
["--at <pos>", "end (default) | start | before <id> | after <id>"],
|
|
157
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (!values.to)
|
|
162
|
+
throw new CliUsageError("move requires --to <parent>");
|
|
163
|
+
const refs = expandBlockArgs(positionals);
|
|
164
|
+
if (refs.length === 0)
|
|
165
|
+
throw new CliUsageError("move requires one or more blocks (or - for stdin)");
|
|
166
|
+
if (cli.flags.server)
|
|
167
|
+
return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values) });
|
|
168
|
+
const ws = cli.workspace();
|
|
169
|
+
const repo = cli.repo(ws);
|
|
170
|
+
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
171
|
+
const parent = block(cli, ws, repo.repoId, values.to);
|
|
172
|
+
const op = { op: "move", blocks, to: { parent, at: parseAt(values) } };
|
|
173
|
+
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
174
|
+
}
|
|
175
|
+
// ---- rm (blocks; --doc handled by the doc-level command) --------------------
|
|
176
|
+
async function runRm(cli, args) {
|
|
177
|
+
const { values, positionals } = parseArgs({
|
|
178
|
+
args,
|
|
179
|
+
allowPositionals: true,
|
|
180
|
+
options: { doc: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
181
|
+
});
|
|
182
|
+
if (values.help) {
|
|
183
|
+
return renderHelp(cli, {
|
|
184
|
+
name: "rm",
|
|
185
|
+
summary: "Remove block(s) (the resurrection pool catches regret), or delete a whole document with --doc",
|
|
186
|
+
usage: ["rm <blocks…|-> [--actor <s>] [--dry-run]", "rm --doc <doc> [--actor <s>] [--dry-run]"],
|
|
187
|
+
options: [
|
|
188
|
+
["<blocks…>", "block ids; `-` reads them from stdin, one per line"],
|
|
189
|
+
["--doc <doc>", "delete a whole document (id or path) — always explicit"],
|
|
190
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (values.doc !== undefined) {
|
|
195
|
+
// Doc deletion always requires the explicit --doc (11 §5.6).
|
|
196
|
+
return runRmDoc(cli, values.doc, values.actor);
|
|
197
|
+
}
|
|
198
|
+
const refs = expandBlockArgs(positionals);
|
|
199
|
+
if (refs.length === 0)
|
|
200
|
+
throw new CliUsageError("rm requires one or more blocks (or - for stdin), or --doc");
|
|
201
|
+
if (cli.flags.server)
|
|
202
|
+
return runOpsRemote(cli, "blocks_remove", { blocks: refs });
|
|
203
|
+
const ws = cli.workspace();
|
|
204
|
+
const repo = cli.repo(ws);
|
|
205
|
+
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
206
|
+
const op = { op: "remove", blocks };
|
|
207
|
+
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
208
|
+
}
|
|
209
|
+
// ---- done (tasks_complete) --------------------------------------------------
|
|
210
|
+
async function runDone(cli, args) {
|
|
211
|
+
const { values, positionals } = parseArgs({
|
|
212
|
+
args,
|
|
213
|
+
allowPositionals: true,
|
|
214
|
+
options: { undo: { type: "boolean" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
215
|
+
});
|
|
216
|
+
if (values.help) {
|
|
217
|
+
return renderHelp(cli, {
|
|
218
|
+
name: "done",
|
|
219
|
+
summary: "Check (or uncheck) task blocks",
|
|
220
|
+
usage: "done <blocks…|-> [--undo] [--actor <s>] [--dry-run]",
|
|
221
|
+
options: [
|
|
222
|
+
["<blocks…>", `task block ids; \`-\` reads them from stdin (\`${cli.prog} q … --ids | ${cli.prog} done -\`)`],
|
|
223
|
+
["--undo", "uncheck instead"],
|
|
224
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const refs = expandBlockArgs(positionals);
|
|
229
|
+
if (refs.length === 0)
|
|
230
|
+
throw new CliUsageError("done requires one or more task blocks (or - for stdin)");
|
|
231
|
+
if (cli.flags.server)
|
|
232
|
+
return runOpsRemote(cli, "tasks_complete", { blocks: refs, ...(values.undo ? { checked: false } : {}) });
|
|
233
|
+
const ws = cli.workspace();
|
|
234
|
+
const repo = cli.repo(ws);
|
|
235
|
+
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
236
|
+
const ops = values.undo
|
|
237
|
+
? blocks.map((b) => ({ op: "update", block: b, attrs: { checked: false }, expect: pinned(ws, b) }))
|
|
238
|
+
: tasksComplete(ws.store, blocks);
|
|
239
|
+
return runOps(cli, ws, repo, ops, actorOf(values));
|
|
240
|
+
}
|
|
241
|
+
// ---- append (sections_append) -----------------------------------------------
|
|
242
|
+
async function runAppend(cli, args) {
|
|
243
|
+
const content = extractContentOpts(args);
|
|
244
|
+
const { values, positionals } = parseArgs({
|
|
245
|
+
args: content.rest,
|
|
246
|
+
allowPositionals: true,
|
|
247
|
+
options: { actor: { type: "string" }, help: { type: "boolean" } },
|
|
248
|
+
});
|
|
249
|
+
if (values.help) {
|
|
250
|
+
return renderHelp(cli, {
|
|
251
|
+
name: "append",
|
|
252
|
+
summary: "Append markdown at the end of a heading's section",
|
|
253
|
+
usage: "append <heading> (-m <markdown> | -f <file> | -) [--actor <s>] [--dry-run]",
|
|
254
|
+
options: [
|
|
255
|
+
["<heading>", "the heading block (id or locator) whose section receives the content"],
|
|
256
|
+
["-m <markdown>", "content inline"],
|
|
257
|
+
["-f <file>", "content from a file"],
|
|
258
|
+
["-", "content from stdin"],
|
|
259
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const heading = positionals[0];
|
|
264
|
+
if (!heading)
|
|
265
|
+
throw new CliUsageError("append requires a <heading> block");
|
|
266
|
+
const markdown = readContent(content);
|
|
267
|
+
// Remote: sections_append resolves the heading ref (id OR text) server-side.
|
|
268
|
+
if (cli.flags.server)
|
|
269
|
+
return runOpsRemote(cli, "sections_append", { heading, markdown });
|
|
270
|
+
const ws = cli.workspace();
|
|
271
|
+
const repo = cli.repo(ws);
|
|
272
|
+
const headingId = block(cli, ws, repo.repoId, heading);
|
|
273
|
+
return runOps(cli, ws, repo, sectionsAppend(headingId, markdown), actorOf(values));
|
|
274
|
+
}
|
|
275
|
+
// ---- split / merge ----------------------------------------------------------
|
|
276
|
+
async function runSplit(cli, args) {
|
|
277
|
+
const { values, positionals } = parseArgs({
|
|
278
|
+
args,
|
|
279
|
+
allowPositionals: true,
|
|
280
|
+
options: { at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
281
|
+
});
|
|
282
|
+
if (values.help) {
|
|
283
|
+
return renderHelp(cli, {
|
|
284
|
+
name: "split",
|
|
285
|
+
summary: "Split a block at character offset(s) into sibling blocks",
|
|
286
|
+
usage: "split <block> --at <n[,n…]> [--actor <s>] [--dry-run]",
|
|
287
|
+
options: [
|
|
288
|
+
["--at <n[,n…]>", "character offset(s) within the block's markdown"],
|
|
289
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const ref = positionals[0];
|
|
294
|
+
if (!ref)
|
|
295
|
+
throw new CliUsageError("split requires a <block>");
|
|
296
|
+
if (!values.at)
|
|
297
|
+
throw new CliUsageError("split requires --at n[,n…]");
|
|
298
|
+
const at = values.at.split(",").map((s) => Number(s.trim()));
|
|
299
|
+
if (cli.flags.server)
|
|
300
|
+
return runOpsRemote(cli, "blocks_split", { block: ref, at });
|
|
301
|
+
const ws = cli.workspace();
|
|
302
|
+
const repo = cli.repo(ws);
|
|
303
|
+
const blockId = block(cli, ws, repo.repoId, ref);
|
|
304
|
+
const op = { op: "split", block: blockId, at, expect: { content_hash: rawHashOf(ws, blockId) ?? "" } };
|
|
305
|
+
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
306
|
+
}
|
|
307
|
+
async function runMerge(cli, args) {
|
|
308
|
+
const { values, positionals } = parseArgs({
|
|
309
|
+
args,
|
|
310
|
+
allowPositionals: true,
|
|
311
|
+
options: { sep: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
312
|
+
});
|
|
313
|
+
if (values.help) {
|
|
314
|
+
return renderHelp(cli, {
|
|
315
|
+
name: "merge",
|
|
316
|
+
summary: "Merge adjacent blocks into the first (the first block's identity survives)",
|
|
317
|
+
usage: "merge <blocks…|-> [--sep <s>] [--actor <s>] [--dry-run]",
|
|
318
|
+
options: [
|
|
319
|
+
["<blocks…>", "two or more adjacent block ids; `-` reads them from stdin"],
|
|
320
|
+
["--sep <s>", "separator placed between the merged texts"],
|
|
321
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const refs = expandBlockArgs(positionals);
|
|
326
|
+
if (refs.length < 2)
|
|
327
|
+
throw new CliUsageError("merge requires at least two blocks");
|
|
328
|
+
if (cli.flags.server)
|
|
329
|
+
return runOpsRemote(cli, "blocks_merge", { blocks: refs, ...(values.sep !== undefined ? { separator: values.sep } : {}) });
|
|
330
|
+
const ws = cli.workspace();
|
|
331
|
+
const repo = cli.repo(ws);
|
|
332
|
+
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
333
|
+
const op = { op: "merge", blocks, ...(values.sep !== undefined ? { separator: values.sep } : {}) };
|
|
334
|
+
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
335
|
+
}
|
|
336
|
+
// helpers ---------------------------------------------------------------------
|
|
337
|
+
function pinned(ws, blockId) {
|
|
338
|
+
return { content_hash: rawHashOf(ws, blockId) ?? "" };
|
|
339
|
+
}
|
|
340
|
+
function actorOf(values) {
|
|
341
|
+
return values.actor ? { actor: values.actor } : {};
|
|
342
|
+
}
|
|
343
|
+
export const cmdApply = { name: "apply", summary: "Apply a raw changeset (the primitive)", run: (c, a) => runApply(c, a) };
|
|
344
|
+
export const cmdInsert = { name: "insert", summary: "Insert blocks under a parent", run: (c, a) => runInsert(c, a) };
|
|
345
|
+
export const cmdUpdate = { name: "update", summary: "Replace a block's markdown (CAS)", run: (c, a) => runUpdate(c, a) };
|
|
346
|
+
export const cmdMove = { name: "move", summary: "Move blocks under a new parent", run: (c, a) => runMove(c, a) };
|
|
347
|
+
export const cmdRm = { name: "rm", summary: "Remove blocks (or --doc a document)", run: (c, a) => runRm(c, a) };
|
|
348
|
+
export const cmdDone = { name: "done", summary: "Check/uncheck task blocks", run: (c, a) => runDone(c, a) };
|
|
349
|
+
export const cmdAppend = { name: "append", summary: "Append into a heading's section", run: (c, a) => runAppend(c, a) };
|
|
350
|
+
export const cmdSplit = { name: "split", summary: "Split a block at offsets", run: (c, a) => runSplit(c, a) };
|
|
351
|
+
export const cmdMerge = { name: "merge", summary: "Merge adjacent blocks", run: (c, a) => runMerge(c, a) };
|
|
352
|
+
//# sourceMappingURL=mutate.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { nodeSet, editablePropsFor } from "@omgbase/core";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { runOps, runOpsRemote } from "./_mutate.js";
|
|
5
|
+
// `omg node set <nodeId> <prop> <value>` (node-editability) — surgically set an
|
|
6
|
+
// editable property of a projected node (e.g. a link's text/target, a task's
|
|
7
|
+
// checked). Resolves node → block via the adapter's registered editor and
|
|
8
|
+
// applies one update op. `omg node props <nodeId>` reports what's editable.
|
|
9
|
+
function nodeKindFormat(cli, nodeId) {
|
|
10
|
+
const ws = cli.workspace();
|
|
11
|
+
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);
|
|
12
|
+
if (!row)
|
|
13
|
+
throw new EngineErrorLike("block_missing", `no node ${nodeId}`);
|
|
14
|
+
return row;
|
|
15
|
+
}
|
|
16
|
+
async function runNode(cli, args) {
|
|
17
|
+
const [sub, ...rest] = args;
|
|
18
|
+
if (sub === undefined || sub === "help" || args.includes("--help")) {
|
|
19
|
+
return renderHelp(cli, {
|
|
20
|
+
name: "node",
|
|
21
|
+
summary: "Edit a projected node's editable properties surgically (a link's target, a task's checked, …)",
|
|
22
|
+
usage: ["node set <nodeId> <prop> <value> [--actor <s>] [--dry-run]", "node props <nodeId>"],
|
|
23
|
+
options: [
|
|
24
|
+
["set", "rewrite one property; the adapter maps it back onto the source block"],
|
|
25
|
+
["props", "list which properties of this node's kind are editable"],
|
|
26
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (sub === "set") {
|
|
31
|
+
parseArgs({ args: rest, allowPositionals: true, options: { actor: { type: "string" } } });
|
|
32
|
+
const [nodeId, prop, ...valueParts] = rest.filter((a) => !a.startsWith("--"));
|
|
33
|
+
if (!nodeId || !prop || valueParts.length === 0)
|
|
34
|
+
throw new CliUsageError("node set <nodeId> <prop> <value>");
|
|
35
|
+
const value = valueParts.join(" ");
|
|
36
|
+
// Remote: node_set resolves node → block + rewrites the span server-side.
|
|
37
|
+
if (cli.flags.server)
|
|
38
|
+
return runOpsRemote(cli, "node_set", { node: nodeId, prop, value });
|
|
39
|
+
const ws = cli.workspace();
|
|
40
|
+
const repo = cli.repo(ws);
|
|
41
|
+
const ops = nodeSet(ws.store, nodeId, prop, value);
|
|
42
|
+
return runOps(cli, ws, repo, ops, { reason: `node_set ${prop}` });
|
|
43
|
+
}
|
|
44
|
+
if (sub === "props") {
|
|
45
|
+
// `props` is a local editability lookup with no remote tool (a niche read).
|
|
46
|
+
if (cli.flags.server)
|
|
47
|
+
throw new CliUsageError("node props is local-only; run it against a local workspace");
|
|
48
|
+
const nodeId = rest[0];
|
|
49
|
+
if (!nodeId)
|
|
50
|
+
throw new CliUsageError("node props <nodeId>");
|
|
51
|
+
const { kind, format } = nodeKindFormat(cli, nodeId);
|
|
52
|
+
const props = editablePropsFor(format, kind);
|
|
53
|
+
if (cli.flags.mode !== "human") {
|
|
54
|
+
cli.io.out(JSON.stringify({ node: nodeId, kind, editable: props }));
|
|
55
|
+
return EXIT_OK;
|
|
56
|
+
}
|
|
57
|
+
if (props.length === 0)
|
|
58
|
+
cli.io.err(cli.style.dim(` ${kind} has no editable properties`));
|
|
59
|
+
else
|
|
60
|
+
cli.io.out(` ${cli.style.accent(kind)} ${cli.style.dim("editable:")} ${props.join(", ")}`);
|
|
61
|
+
return EXIT_OK;
|
|
62
|
+
}
|
|
63
|
+
throw new CliUsageError(`unknown node subcommand '${sub}' (set|props)`);
|
|
64
|
+
}
|
|
65
|
+
export const cmdNode = { name: "node", summary: "Edit a node's editable properties (surgical)", run: (c, a) => runNode(c, a) };
|
|
66
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
|
|
4
|
+
import { truncationFooter, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
5
|
+
import { loadEmbedding } from "./_embed.js";
|
|
6
|
+
import { remoteCall } from "./_remote.js";
|
|
7
|
+
import { readStdin } from "./_mutate.js";
|
|
8
|
+
// `omg query <source>` — OQX composable query. Dot navigation belongs to the host
|
|
9
|
+
// object model (`doc.layer`, `section.blocks`); whitespace query directives
|
|
10
|
+
// belong to OQX (`<receiver> collect|exists|count|first|single { <block> }`).
|
|
11
|
+
// `from E` selects + flattens a relation relative to the current source scope
|
|
12
|
+
// (top-level `from docs` = the repository's docs; `$repo.docs collect { from
|
|
13
|
+
// nodes … }` re-projects each doc through its nodes). Inside a block the
|
|
14
|
+
// `where`/`select` keyword may be omitted: a leading predicate-shaped expression
|
|
15
|
+
// is an implicit `where` (`nodes exists { kind == "md:task" }`), a bare
|
|
16
|
+
// reference / `name: value` list is an implicit `select` (`nodes collect
|
|
17
|
+
// { attrs.text }`); a bare boolean property still projects (filter with
|
|
18
|
+
// `where active` or `active == true`). Features: a boolean where
|
|
19
|
+
// tree over scalar predicates and consumer directives, count comparisons
|
|
20
|
+
// (`nodes count { … } >= 2`), nestable collect, one-scope lifts (^name:) that
|
|
21
|
+
// filter + capture, one-scope-outward references (^name) that correlate a nested
|
|
22
|
+
// query to a parent binding, explicit root relations $repo.docs/nodes/blocks for
|
|
23
|
+
// join-equivalents, first/single lookups, full-text text(...) + embedding-score
|
|
24
|
+
// semantic(...) predicates, `order by <expr> [asc|desc]` ranking, and recursive
|
|
25
|
+
// `follow [distinct] <rel> [{ where … frontier … depth n by … }]` traversal over
|
|
26
|
+
// a type-preserving relation with $depth/$stop/$ordinal recursion metadata +
|
|
27
|
+
// post-walk filtering. A top-level consumer directive changes the result shape:
|
|
28
|
+
// `$repo.docs count { … }` / `$repo.docs exists { … }` reduce to a scalar,
|
|
29
|
+
// `$repo.docs first { … }` / `$repo.docs single { … }` to zero-or-one row (bare
|
|
30
|
+
// `from …` = collect). Coexists with `omg q` (CEL). Source is a positional
|
|
31
|
+
// string or -f file|-. Human output: one hit per line (id + path), or the scalar
|
|
32
|
+
// for count/exists; --json/--jsonl/--ids.
|
|
33
|
+
async function runOqx(cli, args) {
|
|
34
|
+
const { values, positionals } = parseArgs({
|
|
35
|
+
args,
|
|
36
|
+
allowPositionals: true,
|
|
37
|
+
options: {
|
|
38
|
+
n: { type: "string", short: "n" },
|
|
39
|
+
cursor: { type: "string" },
|
|
40
|
+
file: { type: "string", short: "f" },
|
|
41
|
+
help: { type: "boolean" },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (values.help) {
|
|
45
|
+
renderHelp(cli, {
|
|
46
|
+
name: "query",
|
|
47
|
+
summary: "Composable OQX query: from <docs|blocks|nodes|edges> where … select … [order by …] [follow …] (alias: q)",
|
|
48
|
+
usage: ["query <source> [-n <N>] [--cursor <c>] [--ids|--json|--jsonl]", "query -f <file|-> [-n <N>] [--cursor <c>]"],
|
|
49
|
+
options: [
|
|
50
|
+
["<source>", "the OQX query text (quote it); `from` may be omitted inside blocks"],
|
|
51
|
+
["-f <file|->", "read the query from a file, or stdin with `-`"],
|
|
52
|
+
["-n <N>", "page size"],
|
|
53
|
+
["--cursor <c>", "continue a truncated result"],
|
|
54
|
+
["--ids", `bare ids, one per line — pipe fuel (\`… --ids | ${cli.prog} cat -\`)`],
|
|
55
|
+
],
|
|
56
|
+
notes: [
|
|
57
|
+
`semantic ranking: \`order by semantic("phrase") desc\` or \`where semantic("phrase") > 0.6\` (needs embedding.provider; see \`${cli.prog} embed --help\`).`,
|
|
58
|
+
"full syntax: the examples below, the MCP `query_syntax` tool, and docs/query-language.md.",
|
|
59
|
+
"examples:",
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
cli.io.out(" e.g. query 'from docs where nodes count { where kind == \"md:task\" } >= 2'");
|
|
63
|
+
cli.io.out(" query 'from nodes where kind == \"md:section\" select items: section.blocks collect { where type == \"list_item\" }'");
|
|
64
|
+
cli.io.out(" query 'from docs where nodes collect { ^open: value where kind == \"md:task\" && !attrs.checked } select $path, open'");
|
|
65
|
+
cli.io.out(" query 'from docs select owner_id, owner: $repo.nodes single { where kind == \"person\" && attrs.id == ^owner_id }'");
|
|
66
|
+
cli.io.out(" query '$repo.docs count { where layer == \"canon\" }' # scalar; also $repo.<target> exists/none/first/single { … }");
|
|
67
|
+
cli.io.out(" query 'from docs where nodes none { where kind == \"md:task\" && !checked }' # none = zero rows (≡ !exists; \"every\" = none over the complement)");
|
|
68
|
+
cli.io.out(" query 'from docs where type == \"practitioner\" order by era desc limit 2 offset 1' # limit/offset bound the set (after order/distinct, before the consumer)");
|
|
69
|
+
cli.io.out(" query 'from docs where $path == \"x.md\" select fm: entries(frontmatter) collect { k: $key, v: $value }' # a record as a collection ($key/$value); also entries(attrs), entries(inline)");
|
|
70
|
+
cli.io.out(" query 'from docs where layer == \"canon\" select $path values' # `values`: bare values, no {id,path} hits (one item only)");
|
|
71
|
+
cli.io.out(" query 'from docs select $path, tags: tags collect { $value values where $value != \"draft\" }' # $value = the current item (here: each tag)");
|
|
72
|
+
cli.io.out(" query '$repo.docs collect { from nodes where kind == \"md:task\" }' # `from E` re-projects the source (→ nodes)");
|
|
73
|
+
cli.io.out(" query 'from docs where text(\"philosophers stone\") && layer == \"canon\"' # full-text prune");
|
|
74
|
+
cli.io.out(" query 'from blocks where semantic(\"the great work\") > 0.6 select s: semantic(\"the great work\")' # embedding score (needs a provider)");
|
|
75
|
+
cli.io.out(" query 'from docs where type == \"practitioner\" order by era desc' # order by <expr> [asc|desc]");
|
|
76
|
+
cli.io.out(" query 'from blocks where $id == \"b_x\" select t: text, d: $depth, s: $stop follow block.children' # recursive walk ($depth/$stop metadata)");
|
|
77
|
+
cli.io.out(" query 'from nodes where name == \"Overview\" follow section.subsections { depth 3 }' # follow [distinct] <rel> [{ where … frontier … depth n by … }]");
|
|
78
|
+
cli.io.out(" query 'from docs where $path == \"index.md\" select p: $path, d: $depth, s: $stop follow doc.out' # citation graph (cyclic-safe: $stop=cycle)");
|
|
79
|
+
cli.io.out(" query 'from blocks where type == \"list_item\" && $leaf follow block.children' # $leaf/$depth/$stop filter the walk result post-walk");
|
|
80
|
+
cli.io.out(" query 'from edges where predicate == \"depends_on\" select src: $src, to: $dst_path' # the edges target: predicate/provenance/dst_kind + $src/$dst_path/$dst_uri");
|
|
81
|
+
cli.io.out(" query 'from docs where $path == \"a.md\" follow doc.out { via predicate == \"cites\" }' # predicate-filtered traversal (via = the licensing edge)");
|
|
82
|
+
return EXIT_OK;
|
|
83
|
+
}
|
|
84
|
+
const source = values.file
|
|
85
|
+
? (values.file === "-" ? readStdin() : readFileSync(values.file, "utf8"))
|
|
86
|
+
: positionals.join(" ").trim();
|
|
87
|
+
if (!source) {
|
|
88
|
+
cli.io.err(cli.style.dim(" usage: query <source> (or -f file|-)"));
|
|
89
|
+
return EXIT_OK;
|
|
90
|
+
}
|
|
91
|
+
const opts = {};
|
|
92
|
+
if (values.n)
|
|
93
|
+
opts.limit = Number(values.n);
|
|
94
|
+
if (values.cursor)
|
|
95
|
+
opts.cursor = values.cursor;
|
|
96
|
+
let result;
|
|
97
|
+
if (cli.flags.server) {
|
|
98
|
+
// Remote: the `query` MCP tool returns the same OqxResult; render unchanged.
|
|
99
|
+
result = await remoteCall(cli, "query", {
|
|
100
|
+
query: source,
|
|
101
|
+
...(opts.limit !== undefined ? { limit: opts.limit } : {}),
|
|
102
|
+
...(opts.cursor !== undefined ? { cursor: opts.cursor } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const ws = cli.workspace();
|
|
107
|
+
const repo = cli.repo(ws);
|
|
108
|
+
// A query using semantic("…") needs the embedding provider to turn each
|
|
109
|
+
// phrase into a query vector; everything else runs on the sync core with no
|
|
110
|
+
// provider loaded. loadEmbedding is only touched when a phrase is present.
|
|
111
|
+
const phrases = collectSemanticPhrases(source);
|
|
112
|
+
if (phrases.length > 0) {
|
|
113
|
+
const loaded = await loadEmbedding(ws, repo.repoId);
|
|
114
|
+
if (!loaded) {
|
|
115
|
+
throw new EngineErrorLike("semantic_unavailable", "semantic(...) needs an embedding provider", {
|
|
116
|
+
hint: "omg config set embedding.provider <command|url>",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const embed = async (t) => ({ model: loaded.provider.model, vec: await loaded.worker.embedQuery(t) });
|
|
121
|
+
result = await oqxRunAsync(ws.store, repo.repoId, source, opts, embed);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await loaded.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
result = oqxRun(ws.store, repo.repoId, source, opts);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Shell capture (typed result before formatting): a scalar for count/exists,
|
|
132
|
+
// else the whole result (its .hits become the addressable frame).
|
|
133
|
+
cli.capture?.(result.consumer === "count" ? result.count
|
|
134
|
+
: result.consumer === "exists" ? result.exists
|
|
135
|
+
: result.consumer === "none" ? result.none
|
|
136
|
+
: result.values ?? result);
|
|
137
|
+
// JSON emits the whole result verbatim (incl. consumer + any scalar), so
|
|
138
|
+
// count/exists round-trip without special-casing.
|
|
139
|
+
if (cli.flags.mode === "json") {
|
|
140
|
+
cli.io.out(JSON.stringify(result));
|
|
141
|
+
return EXIT_OK;
|
|
142
|
+
}
|
|
143
|
+
// Scalar consumers (count/exists/none) have no hits — render the reduction itself.
|
|
144
|
+
if (result.consumer === "count" || result.consumer === "exists" || result.consumer === "none") {
|
|
145
|
+
const scalar = result.consumer === "count" ? String(result.count) : result.consumer === "exists" ? String(result.exists) : String(result.none);
|
|
146
|
+
cli.io.out(scalar);
|
|
147
|
+
return EXIT_OK;
|
|
148
|
+
}
|
|
149
|
+
// A `values` projection has no hits — one bare value per line (strings
|
|
150
|
+
// verbatim, anything else as JSON; `--jsonl` is JSON throughout).
|
|
151
|
+
if (result.values) {
|
|
152
|
+
for (const v of result.values)
|
|
153
|
+
cli.io.out(typeof v === "string" && cli.flags.mode !== "jsonl" ? v : JSON.stringify(v));
|
|
154
|
+
if (result.truncated)
|
|
155
|
+
truncationFooter(cli.io, cli.style, result.cursor ?? "");
|
|
156
|
+
return EXIT_OK;
|
|
157
|
+
}
|
|
158
|
+
if (cli.flags.mode === "ids") {
|
|
159
|
+
for (const h of result.hits)
|
|
160
|
+
cli.io.out(h.id);
|
|
161
|
+
if (result.truncated)
|
|
162
|
+
truncationFooter(cli.io, cli.style, result.cursor ?? "");
|
|
163
|
+
return EXIT_OK;
|
|
164
|
+
}
|
|
165
|
+
if (cli.flags.mode === "jsonl") {
|
|
166
|
+
for (const h of result.hits)
|
|
167
|
+
cli.io.out(JSON.stringify(h));
|
|
168
|
+
if (result.truncated)
|
|
169
|
+
truncationFooter(cli.io, cli.style, result.cursor ?? "");
|
|
170
|
+
return EXIT_OK;
|
|
171
|
+
}
|
|
172
|
+
const { style, io } = cli;
|
|
173
|
+
if (result.hits.length === 0) {
|
|
174
|
+
io.err(style.dim(" no hits"));
|
|
175
|
+
return EXIT_OK;
|
|
176
|
+
}
|
|
177
|
+
for (const h of result.hits) {
|
|
178
|
+
io.out(`${style.id(h.id)} ${style.accent(h.path)}`.trimEnd());
|
|
179
|
+
}
|
|
180
|
+
if (result.truncated)
|
|
181
|
+
truncationFooter(io, style, result.cursor ?? "");
|
|
182
|
+
return EXIT_OK;
|
|
183
|
+
}
|
|
184
|
+
// `query` (alias `q`) is the single, default query surface — the OQX engine.
|
|
185
|
+
// (The `oqx` alias was removed: OQX *is* the query experience, so `query`/`q`
|
|
186
|
+
// name it. The impl lives in this file, still named oqx.ts after the engine.)
|
|
187
|
+
export const cmdOqx = {
|
|
188
|
+
name: "query",
|
|
189
|
+
aliases: ["q"],
|
|
190
|
+
summary: "Composable query (OQX: from/where/select + collection ops)",
|
|
191
|
+
run: (cli, a) => runOqx(cli, a),
|
|
192
|
+
};
|
|
193
|
+
//# sourceMappingURL=oqx.js.map
|