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,87 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { findDoc, docsOutline } from "@omgbase/core";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { remoteCall } from "./_remote.js";
|
|
5
|
+
// `omg outline <doc|path>` (alias ol) — the wire format (06 §6). Human output
|
|
6
|
+
// prints the outline text with full block ids inline; --json emits the
|
|
7
|
+
// OutlineResult verbatim.
|
|
8
|
+
async function runOutline(cli, args) {
|
|
9
|
+
const { values, positionals } = parseArgs({
|
|
10
|
+
args,
|
|
11
|
+
allowPositionals: true,
|
|
12
|
+
options: {
|
|
13
|
+
depth: { type: "string" },
|
|
14
|
+
section: { type: "string" },
|
|
15
|
+
skeleton: { type: "boolean" },
|
|
16
|
+
help: { type: "boolean" },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (values.help) {
|
|
20
|
+
return renderHelp(cli, {
|
|
21
|
+
name: "outline",
|
|
22
|
+
summary: "A document's outline with block ids inline (frozen wire format) — the orientation view",
|
|
23
|
+
usage: "outline <doc|path> [--depth <n>] [--section <locator>] [--skeleton]",
|
|
24
|
+
options: [
|
|
25
|
+
["--depth <n>", "limit heading depth"],
|
|
26
|
+
["--section <locator>", "outline only the section at a locator"],
|
|
27
|
+
["--skeleton", "structure only, no text"],
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const ref = positionals[0];
|
|
32
|
+
if (!ref)
|
|
33
|
+
throw new CliUsageError("outline requires a <doc|path>");
|
|
34
|
+
let result;
|
|
35
|
+
let header;
|
|
36
|
+
if (cli.flags.server) {
|
|
37
|
+
// Remote: the docs_outline tool resolves the ref + returns the same
|
|
38
|
+
// OutlineResult. The header shows the ref you passed (no local path lookup).
|
|
39
|
+
result = await remoteCall(cli, "docs_outline", {
|
|
40
|
+
doc: ref,
|
|
41
|
+
...(values.depth ? { depth: Number(values.depth) } : {}),
|
|
42
|
+
...(values.skeleton ? { resolution: "skeleton" } : {}),
|
|
43
|
+
});
|
|
44
|
+
header = ref;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const ws = cli.workspace();
|
|
48
|
+
const repo = cli.repo(ws);
|
|
49
|
+
const info = ref.startsWith("d_") ? findDoc(ws.store, { docId: ref }) : findDoc(ws.store, { repoId: repo.repoId, path: ref });
|
|
50
|
+
if (!info)
|
|
51
|
+
throw new EngineErrorLike("doc_missing", `no document ${ref}`);
|
|
52
|
+
const opts = {};
|
|
53
|
+
if (values.depth)
|
|
54
|
+
opts.depth = Number(values.depth);
|
|
55
|
+
if (values.skeleton)
|
|
56
|
+
opts.resolution = "skeleton";
|
|
57
|
+
result = docsOutline(ws.store, info.docId, opts);
|
|
58
|
+
header = info.path;
|
|
59
|
+
}
|
|
60
|
+
if (cli.flags.mode !== "human") {
|
|
61
|
+
cli.io.out(JSON.stringify(result));
|
|
62
|
+
return EXIT_OK;
|
|
63
|
+
}
|
|
64
|
+
const { render, style, io } = cli;
|
|
65
|
+
io.out(render.wordmark(header));
|
|
66
|
+
io.out(render.rule(40));
|
|
67
|
+
// Colorize the inline block ids + § marks.
|
|
68
|
+
for (const line of result.text.split("\n")) {
|
|
69
|
+
io.out(colorizeOutline(line, style));
|
|
70
|
+
}
|
|
71
|
+
if (result.truncated)
|
|
72
|
+
truncationFooter(io, style, "budget");
|
|
73
|
+
return EXIT_OK;
|
|
74
|
+
}
|
|
75
|
+
function colorizeOutline(line, style) {
|
|
76
|
+
// leading block id (b_…) → id style; trailing ` §` → accent.
|
|
77
|
+
return line
|
|
78
|
+
.replace(/\bb_[0-9a-z]+\b/, (m) => style.id(m))
|
|
79
|
+
.replace(/§/g, style.accent("§"));
|
|
80
|
+
}
|
|
81
|
+
export const cmdOutline = {
|
|
82
|
+
name: "outline",
|
|
83
|
+
aliases: ["ol"],
|
|
84
|
+
summary: "Document outline (frozen wire format)",
|
|
85
|
+
run: (cli, a) => runOutline(cli, a),
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=outline.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { linksRetarget } from "@omgbase/core";
|
|
3
|
+
import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { runOps } from "./_mutate.js";
|
|
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 the per-block diffs (the 06 §4
|
|
8
|
+
// "always dry-run first" contract, encoded as the default). --apply commits.
|
|
9
|
+
async function runRetarget(cli, args) {
|
|
10
|
+
const { values, positionals } = parseArgs({
|
|
11
|
+
args,
|
|
12
|
+
allowPositionals: true,
|
|
13
|
+
options: { scope: { type: "string" }, apply: { type: "boolean" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
14
|
+
});
|
|
15
|
+
if (values.help) {
|
|
16
|
+
return renderHelp(cli, {
|
|
17
|
+
name: "retarget",
|
|
18
|
+
summary: "Rewrite every link that points at <from> to point at <to> — plan-by-default, --apply commits",
|
|
19
|
+
usage: "retarget <from> <to> [--scope <glob>] [--apply] [--actor <s>] [--dry-run]",
|
|
20
|
+
options: [
|
|
21
|
+
["--scope <glob>", "only rewrite links in documents matching the path glob"],
|
|
22
|
+
["--apply", "commit the rewrite (default: print the plan only)"],
|
|
23
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const from = positionals[0];
|
|
28
|
+
const to = positionals[1];
|
|
29
|
+
if (!from || !to)
|
|
30
|
+
throw new CliUsageError("retarget requires <from> and <to> targets");
|
|
31
|
+
// Remote: links_retarget resolves + (optionally) applies server-side.
|
|
32
|
+
if (cli.flags.server) {
|
|
33
|
+
const r = await remoteCall(cli, "links_retarget", {
|
|
34
|
+
from_target: from, to_target: to, dry_run: !values.apply, ...(values.scope ? { path_glob: values.scope } : {}),
|
|
35
|
+
});
|
|
36
|
+
if (values.apply) {
|
|
37
|
+
if (cli.flags.mode !== "human")
|
|
38
|
+
cli.io.out(JSON.stringify(r));
|
|
39
|
+
else
|
|
40
|
+
cli.io.err(cli.style.dim(` ${cli.style.ok(cli.render.g.ok)} retargeted ${r.hits.length} block(s)`));
|
|
41
|
+
return EXIT_OK;
|
|
42
|
+
}
|
|
43
|
+
return renderPlan(cli, from, to, r.hits);
|
|
44
|
+
}
|
|
45
|
+
const ws = cli.workspace();
|
|
46
|
+
const repo = cli.repo(ws);
|
|
47
|
+
const { ops, hits } = linksRetarget(ws.store, repo.repoId, from, to, values.scope ? { pathGlob: values.scope } : {});
|
|
48
|
+
// --apply → commit via the shared helper (honors --dry-run too, if combined).
|
|
49
|
+
if (values.apply) {
|
|
50
|
+
return runOps(cli, ws, repo, ops, values.actor ? { actor: values.actor } : {});
|
|
51
|
+
}
|
|
52
|
+
return renderPlan(cli, from, to, hits);
|
|
53
|
+
}
|
|
54
|
+
// Plan (default): show what WOULD change, commit nothing.
|
|
55
|
+
function renderPlan(cli, from, to, hits) {
|
|
56
|
+
if (cli.flags.mode === "json") {
|
|
57
|
+
cli.io.out(JSON.stringify({ from, to, hits }));
|
|
58
|
+
return EXIT_OK;
|
|
59
|
+
}
|
|
60
|
+
const { style, io } = cli;
|
|
61
|
+
if (hits.length === 0) {
|
|
62
|
+
io.err(style.dim(` no blocks reference ${from}`));
|
|
63
|
+
return EXIT_OK;
|
|
64
|
+
}
|
|
65
|
+
io.err(style.dim(` plan — ${hits.length} block(s) would change; re-run with --apply to commit`));
|
|
66
|
+
for (const h of hits) {
|
|
67
|
+
io.out(style.id(h.block));
|
|
68
|
+
for (const line of lineDiff(h.oldRaw, h.newRaw)) {
|
|
69
|
+
io.out(line.startsWith("+") ? style.ok(line) : line.startsWith("-") ? style.err(line) : style.dim(line));
|
|
70
|
+
}
|
|
71
|
+
io.out("");
|
|
72
|
+
}
|
|
73
|
+
return EXIT_OK;
|
|
74
|
+
}
|
|
75
|
+
function lineDiff(before, after) {
|
|
76
|
+
const a = before.split("\n");
|
|
77
|
+
const b = after.split("\n");
|
|
78
|
+
const out = [];
|
|
79
|
+
const max = Math.max(a.length, b.length);
|
|
80
|
+
for (let i = 0; i < max; i++) {
|
|
81
|
+
if (a[i] === b[i])
|
|
82
|
+
continue;
|
|
83
|
+
if (a[i] !== undefined)
|
|
84
|
+
out.push(`- ${a[i]}`);
|
|
85
|
+
if (b[i] !== undefined)
|
|
86
|
+
out.push(`+ ${b[i]}`);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
export const cmdRetarget = { name: "retarget", summary: "Rewrite a link target (plan-by-default)", run: (c, a) => runRetarget(c, a) };
|
|
91
|
+
//# sourceMappingURL=retarget.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { resolveRef, findDoc, loadDocBlocks, blockRaw, oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { loadEmbedding } from "./_embed.js";
|
|
5
|
+
// `omg run <locator|path>` (11 §5.3) — evaluate the first ```omg fence in a doc
|
|
6
|
+
// (or the fence at a block locator) and print its results. The fence body is an
|
|
7
|
+
// OQX source string (the same language `omg query` runs). Strictly read-and-print:
|
|
8
|
+
// fences stay INERT in the corpus (ADR-011 §8) — nothing is projected or written.
|
|
9
|
+
function firstOmgFence(roots) {
|
|
10
|
+
for (const n of roots) {
|
|
11
|
+
if (n.type === "code_fence" && n.attrs.lang === "omg")
|
|
12
|
+
return n;
|
|
13
|
+
const inner = firstOmgFence(n.children);
|
|
14
|
+
if (inner)
|
|
15
|
+
return inner;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
// Strip the ``` fences from a code-fence block's raw, returning the body.
|
|
20
|
+
function fenceBody(raw) {
|
|
21
|
+
const lines = raw.split("\n");
|
|
22
|
+
if (lines[0]?.trimStart().startsWith("```"))
|
|
23
|
+
lines.shift();
|
|
24
|
+
while (lines.length && lines[lines.length - 1].trim() === "")
|
|
25
|
+
lines.pop();
|
|
26
|
+
if (lines[lines.length - 1]?.trimStart().startsWith("```"))
|
|
27
|
+
lines.pop();
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
async function runRun(cli, args) {
|
|
31
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
|
|
32
|
+
if (values.help) {
|
|
33
|
+
return renderHelp(cli, {
|
|
34
|
+
name: "run",
|
|
35
|
+
summary: "Evaluate the ```omg fence (an OQX query) at a locator — or the first fence in a document — and print its results",
|
|
36
|
+
usage: "run <locator|path> [--ids|--json|--jsonl]",
|
|
37
|
+
options: [["<locator|path>", "a fence block's locator, or a document whose first ```omg fence is run"]],
|
|
38
|
+
notes: ["Strictly read-and-print: fences stay inert in the corpus; nothing is projected or written."],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const ref = positionals[0];
|
|
42
|
+
if (!ref)
|
|
43
|
+
throw new CliUsageError("run requires a <locator|path>");
|
|
44
|
+
const ws = cli.workspace();
|
|
45
|
+
const repo = cli.repo(ws);
|
|
46
|
+
// A block ref runs that fence; a doc/path ref runs the doc's first omg fence.
|
|
47
|
+
const resolved = resolveRef(ws.store, repo.repoId, ref);
|
|
48
|
+
if (!resolved)
|
|
49
|
+
throw new EngineErrorLike("doc_missing", `no node ${ref}`);
|
|
50
|
+
let fenceRaw = null;
|
|
51
|
+
if (resolved.kind === "block") {
|
|
52
|
+
const roots = loadDocBlocks(ws.store, resolved.docId);
|
|
53
|
+
const find = (nodes) => {
|
|
54
|
+
for (const n of nodes) {
|
|
55
|
+
if (n.blockId === resolved.blockId)
|
|
56
|
+
return n;
|
|
57
|
+
const inner = find(n.children);
|
|
58
|
+
if (inner)
|
|
59
|
+
return inner;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
const node = find(roots);
|
|
64
|
+
if (!node || node.type !== "code_fence")
|
|
65
|
+
throw new EngineErrorLike("opaque_block", `block ${ref} is not an omg fence`);
|
|
66
|
+
fenceRaw = blockRaw(ws.store, node.rawHashHex);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const info = findDoc(ws.store, { docId: resolved.docId });
|
|
70
|
+
void info;
|
|
71
|
+
const fence = firstOmgFence(loadDocBlocks(ws.store, resolved.docId));
|
|
72
|
+
if (!fence)
|
|
73
|
+
throw new EngineErrorLike("target_missing", `no \`\`\`omg fence in ${ref}`);
|
|
74
|
+
fenceRaw = blockRaw(ws.store, fence.rawHashHex);
|
|
75
|
+
}
|
|
76
|
+
const source = fenceBody(fenceRaw).trim();
|
|
77
|
+
if (!source)
|
|
78
|
+
throw new EngineErrorLike("target_missing", `the omg fence in ${ref} is empty`);
|
|
79
|
+
// Same execution path as `omg query`: load the embedder only when the fence uses
|
|
80
|
+
// semantic(...); everything else runs on the sync core.
|
|
81
|
+
const phrases = collectSemanticPhrases(source);
|
|
82
|
+
let result;
|
|
83
|
+
if (phrases.length > 0) {
|
|
84
|
+
const loaded = await loadEmbedding(ws, repo.repoId);
|
|
85
|
+
if (!loaded) {
|
|
86
|
+
throw new EngineErrorLike("semantic_unavailable", "semantic(...) needs an embedding provider", {
|
|
87
|
+
hint: "omg config set embedding.provider <command|url>",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const embed = async (t) => ({ model: loaded.provider.model, vec: await loaded.worker.embedQuery(t) });
|
|
92
|
+
result = await oqxRunAsync(ws.store, repo.repoId, source, {}, embed);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await loaded.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
result = oqxRun(ws.store, repo.repoId, source);
|
|
100
|
+
}
|
|
101
|
+
if (cli.flags.mode === "json") {
|
|
102
|
+
cli.io.out(JSON.stringify(result));
|
|
103
|
+
return EXIT_OK;
|
|
104
|
+
}
|
|
105
|
+
if (result.consumer === "count" || result.consumer === "exists" || result.consumer === "none") {
|
|
106
|
+
cli.io.out(result.consumer === "count" ? String(result.count) : result.consumer === "exists" ? String(result.exists) : String(result.none));
|
|
107
|
+
return EXIT_OK;
|
|
108
|
+
}
|
|
109
|
+
if (result.values) {
|
|
110
|
+
for (const v of result.values)
|
|
111
|
+
cli.io.out(typeof v === "string" ? v : JSON.stringify(v));
|
|
112
|
+
if (result.truncated)
|
|
113
|
+
truncationFooter(cli.io, cli.style, result.cursor ?? "");
|
|
114
|
+
return EXIT_OK;
|
|
115
|
+
}
|
|
116
|
+
if (cli.flags.mode === "ids") {
|
|
117
|
+
for (const h of result.hits)
|
|
118
|
+
cli.io.out(h.id);
|
|
119
|
+
return EXIT_OK;
|
|
120
|
+
}
|
|
121
|
+
const { style, io } = cli;
|
|
122
|
+
if (result.hits.length === 0) {
|
|
123
|
+
io.err(style.dim(" no hits"));
|
|
124
|
+
return EXIT_OK;
|
|
125
|
+
}
|
|
126
|
+
for (const h of result.hits) {
|
|
127
|
+
const preview = typeof h.text === "string" ? String(h.text).split("\n")[0] : "";
|
|
128
|
+
io.out(`${style.id(h.id)} ${style.accent(h.path)} ${preview}`.trimEnd());
|
|
129
|
+
}
|
|
130
|
+
if (result.truncated)
|
|
131
|
+
truncationFooter(io, style, result.cursor ?? "");
|
|
132
|
+
return EXIT_OK;
|
|
133
|
+
}
|
|
134
|
+
export const cmdRun = { name: "run", summary: "Evaluate an ```omg fence (OQX, inert, read-only)", run: (c, a) => runRun(c, a) };
|
|
135
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { ShellSession } from "../shell/session.js";
|
|
5
|
+
// `omg shell` (11 shell) — a persistent in-process session. One open
|
|
6
|
+
// Workspace/store is reused across every command (no per-invocation startup
|
|
7
|
+
// cost), and typed command results become ephemeral bindings (@1/@_/@name). Two
|
|
8
|
+
// drive modes share one ShellSession runtime: an interactive readline REPL on a
|
|
9
|
+
// TTY, and a script runner when stdin is piped (one line per command) — the
|
|
10
|
+
// latter is also what a Markdown CLI-session test would feed.
|
|
11
|
+
async function runShell(cli, args) {
|
|
12
|
+
const { values } = parseArgs({
|
|
13
|
+
args,
|
|
14
|
+
allowPositionals: true,
|
|
15
|
+
options: { help: { type: "boolean" }, prompt: { type: "string" } },
|
|
16
|
+
});
|
|
17
|
+
if (values.help) {
|
|
18
|
+
return renderHelp(cli, {
|
|
19
|
+
name: "shell",
|
|
20
|
+
summary: "A persistent session: one open workspace across commands, and results become typed bindings (@1/@_/@name)",
|
|
21
|
+
usage: "shell [--prompt <str>] [--server <cmd|url>]",
|
|
22
|
+
options: [
|
|
23
|
+
["--prompt <str>", "emit <str> before reading each piped line so a driver (e.g. recital) can sync on it; also $OMG_SHELL_PROMPT (--prompt wins)"],
|
|
24
|
+
["--server <cmd|url>", "run every line against a remote engine over MCP"],
|
|
25
|
+
],
|
|
26
|
+
notes: ["Interactive on a TTY (type `?` for binding help, `exit` to leave); a script runner when stdin is piped (one command per line)."],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// An explicit prompt string, if any: --prompt wins, else $OMG_SHELL_PROMPT.
|
|
30
|
+
// In piped mode this switches on the prompt-emitting line runner; on a TTY it
|
|
31
|
+
// overrides the interactive prompt. (An empty env value is treated as unset.)
|
|
32
|
+
const promptOpt = values.prompt ?? (process.env.OMG_SHELL_PROMPT || undefined);
|
|
33
|
+
// Local mode: resolve the workspace once and hold it open for the session's
|
|
34
|
+
// lifetime. Remote mode (`--server`): no local store — every line routes over
|
|
35
|
+
// MCP through the shared connection, so we hold no workspace.
|
|
36
|
+
const ws = cli.flags.server ? undefined : cli.workspace();
|
|
37
|
+
const session = new ShellSession({
|
|
38
|
+
...(ws ? { workspace: ws } : {}),
|
|
39
|
+
cwd: cli.cwd,
|
|
40
|
+
io: cli.io,
|
|
41
|
+
noColor: cli.style.tier === "plain",
|
|
42
|
+
...(cli.flags.server ? { server: cli.flags.server } : {}),
|
|
43
|
+
...(cli.flags.headers ? { headers: cli.flags.headers } : {}),
|
|
44
|
+
});
|
|
45
|
+
const interactive = cli.io.stdoutTTY && Boolean(process.stdin.isTTY);
|
|
46
|
+
if (!interactive) {
|
|
47
|
+
// Piped mode. With a prompt configured, act like the interactive REPL for a
|
|
48
|
+
// machine driver: emit the prompt (no trailing newline) before each line so
|
|
49
|
+
// a tool like recital can treat its reappearance as the end-of-command
|
|
50
|
+
// signal. Without one, keep the batch script runner (read all, run all).
|
|
51
|
+
if (promptOpt !== undefined)
|
|
52
|
+
return await runPromptedScript(cli, session, promptOpt);
|
|
53
|
+
const script = await readAll();
|
|
54
|
+
let code = EXIT_OK;
|
|
55
|
+
for (const line of script.split(/\r?\n/)) {
|
|
56
|
+
const c = await session.exec(line);
|
|
57
|
+
if (c !== EXIT_OK)
|
|
58
|
+
code = c;
|
|
59
|
+
if (session.exited)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
return code;
|
|
63
|
+
}
|
|
64
|
+
// Interactive REPL. A configured prompt overrides the default prompt string.
|
|
65
|
+
const prompt = promptOpt ?? (cli.style.tier === "plain" ? "omg> " : cli.style.accent("omg") + cli.style.dim("> "));
|
|
66
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt });
|
|
67
|
+
cli.io.err(cli.style.dim(" omg shell — type `?` for session-binding help, `exit` to leave"));
|
|
68
|
+
rl.prompt();
|
|
69
|
+
return await new Promise((resolve) => {
|
|
70
|
+
rl.on("line", (line) => {
|
|
71
|
+
// Pause while a command runs so the next prompt appears after its output.
|
|
72
|
+
rl.pause();
|
|
73
|
+
session
|
|
74
|
+
.exec(line)
|
|
75
|
+
.catch((err) => cli.io.err(cli.style.err(String(err?.message ?? err))))
|
|
76
|
+
.finally(() => {
|
|
77
|
+
if (session.exited) {
|
|
78
|
+
rl.close();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
rl.resume();
|
|
82
|
+
rl.prompt();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
rl.on("close", () => {
|
|
87
|
+
cli.io.err("");
|
|
88
|
+
resolve(EXIT_OK);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Piped session that emits `promptStr` before each line, so a machine driver
|
|
94
|
+
* (e.g. recital's prompt mode) can sync on the prompt reappearing. The prompt is
|
|
95
|
+
* written straight to stdout with no trailing newline; command output flows
|
|
96
|
+
* through the normal IO in between. Ends on stdin EOF or an `exit` command.
|
|
97
|
+
*/
|
|
98
|
+
function runPromptedScript(cli, session, promptStr) {
|
|
99
|
+
const rl = createInterface({ input: process.stdin });
|
|
100
|
+
const queue = [];
|
|
101
|
+
let processing = false;
|
|
102
|
+
let inputClosed = false;
|
|
103
|
+
let code = EXIT_OK;
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
// Serialize command execution: `readline` can emit several buffered `line`
|
|
106
|
+
// events before any pause takes hold, so run one command fully — output and
|
|
107
|
+
// the next prompt — before starting the next, whatever pace lines arrive at.
|
|
108
|
+
const pump = async () => {
|
|
109
|
+
if (processing)
|
|
110
|
+
return;
|
|
111
|
+
processing = true;
|
|
112
|
+
while (queue.length > 0) {
|
|
113
|
+
const line = queue.shift();
|
|
114
|
+
try {
|
|
115
|
+
const c = await session.exec(line);
|
|
116
|
+
if (c !== EXIT_OK)
|
|
117
|
+
code = c;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
cli.io.err(cli.style.err(String(err?.message ?? err)));
|
|
121
|
+
}
|
|
122
|
+
if (session.exited) {
|
|
123
|
+
rl.close();
|
|
124
|
+
resolve(code);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
process.stdout.write(promptStr);
|
|
128
|
+
}
|
|
129
|
+
processing = false;
|
|
130
|
+
if (inputClosed)
|
|
131
|
+
resolve(code);
|
|
132
|
+
};
|
|
133
|
+
process.stdout.write(promptStr);
|
|
134
|
+
rl.on("line", (line) => {
|
|
135
|
+
queue.push(line);
|
|
136
|
+
void pump();
|
|
137
|
+
});
|
|
138
|
+
rl.on("close", () => {
|
|
139
|
+
inputClosed = true;
|
|
140
|
+
if (!processing && queue.length === 0)
|
|
141
|
+
resolve(code);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function readAll() {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
let buf = "";
|
|
148
|
+
process.stdin.setEncoding("utf8");
|
|
149
|
+
process.stdin.on("data", (c) => (buf += c));
|
|
150
|
+
process.stdin.on("end", () => resolve(buf));
|
|
151
|
+
process.stdin.on("error", () => resolve(buf));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export const cmdShell = {
|
|
155
|
+
name: "shell",
|
|
156
|
+
summary: "Persistent session with typed bindings (@1/@_/@name)",
|
|
157
|
+
run: (cli, a) => runShell(cli, a),
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { resolveRef, nodesGet, findDoc, docLinks, historyNode, docPropertiesMerged } from "@omgbase/core";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { expandBlockArgs } from "./_mutate.js";
|
|
5
|
+
function showOne(cli, ref, include) {
|
|
6
|
+
const ws = cli.workspace();
|
|
7
|
+
const repo = cli.repo(ws);
|
|
8
|
+
const resolved = resolveRef(ws.store, repo.repoId, ref);
|
|
9
|
+
if (!resolved)
|
|
10
|
+
throw new EngineErrorLike("doc_missing", `no node ${ref}`);
|
|
11
|
+
if (resolved.kind === "document") {
|
|
12
|
+
const info = findDoc(ws.store, { docId: resolved.docId });
|
|
13
|
+
const links = docLinks(ws.store, resolved.docId, { direction: "both" });
|
|
14
|
+
const properties = docPropertiesMerged(ws.store.db, resolved.docId);
|
|
15
|
+
const payload = { kind: "document", id: info.docId, path: info.path, properties, edges: links };
|
|
16
|
+
return { payload, render: () => renderDocCard(cli, info.path, properties, links) };
|
|
17
|
+
}
|
|
18
|
+
const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution: "full" });
|
|
19
|
+
if (!node)
|
|
20
|
+
throw new EngineErrorLike("block_missing", `no block ${ref}`);
|
|
21
|
+
const history = include.includes("history") ? historyNode(ws.store, resolved.blockId, { limit: 5 }) : undefined;
|
|
22
|
+
return { payload: { ...node, ...(history ? { history } : {}) }, render: () => renderBlockCard(cli, node, history) };
|
|
23
|
+
}
|
|
24
|
+
function runShow(cli, args) {
|
|
25
|
+
const { values, positionals } = parseArgs({
|
|
26
|
+
args,
|
|
27
|
+
allowPositionals: true,
|
|
28
|
+
options: { include: { type: "string" }, help: { type: "boolean" } },
|
|
29
|
+
});
|
|
30
|
+
if (values.help) {
|
|
31
|
+
return renderHelp(cli, {
|
|
32
|
+
name: "show",
|
|
33
|
+
summary: "Metadata card for a node: attrs, placement, open edges, last change (`cat` is the bytes)",
|
|
34
|
+
usage: "show <node…|-> [--include history]",
|
|
35
|
+
options: [
|
|
36
|
+
["<node…>", "one or more refs: a doc/block id, a path, or a locator"],
|
|
37
|
+
["-", `read refs from stdin, one per line (\`${cli.prog} q … --ids | ${cli.prog} show -\`)`],
|
|
38
|
+
["--include <list>", "comma-separated extras; `history` adds the block's last 5 changes"],
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const refs = expandBlockArgs(positionals);
|
|
43
|
+
if (refs.length === 0)
|
|
44
|
+
throw new CliUsageError("show requires a <node> (or - to read refs from stdin)");
|
|
45
|
+
const include = (values.include ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
46
|
+
const cards = refs.map((ref) => showOne(cli, ref, include));
|
|
47
|
+
// One ref keeps the single-entity shape (→ @_, frame intact); several become a list.
|
|
48
|
+
const single = cards.length === 1 ? cards[0] : null;
|
|
49
|
+
cli.capture?.(single ? single.payload : cards.map((c) => c.payload));
|
|
50
|
+
if (cli.flags.mode === "json") {
|
|
51
|
+
cli.io.out(JSON.stringify(single ? single.payload : cards.map((c) => c.payload)));
|
|
52
|
+
return EXIT_OK;
|
|
53
|
+
}
|
|
54
|
+
if (cli.flags.mode !== "human") {
|
|
55
|
+
for (const c of cards)
|
|
56
|
+
cli.io.out(JSON.stringify(c.payload));
|
|
57
|
+
return EXIT_OK;
|
|
58
|
+
}
|
|
59
|
+
for (const c of cards)
|
|
60
|
+
c.render();
|
|
61
|
+
return EXIT_OK;
|
|
62
|
+
}
|
|
63
|
+
function renderDocCard(cli, path, fm, links) {
|
|
64
|
+
const { render, style, io } = cli;
|
|
65
|
+
io.out(render.wordmark(path));
|
|
66
|
+
io.out(render.rule(40));
|
|
67
|
+
const keys = Object.keys(fm);
|
|
68
|
+
if (keys.length > 0) {
|
|
69
|
+
io.out(` ${style.dim("properties")}`);
|
|
70
|
+
for (const k of keys)
|
|
71
|
+
io.out(` ${style.accent(k)} ${style.dim("=")} ${fmt(fm[k])}`);
|
|
72
|
+
}
|
|
73
|
+
if (links.out.length > 0) {
|
|
74
|
+
io.out(` ${style.dim("out edges")}`);
|
|
75
|
+
for (const e of links.out)
|
|
76
|
+
io.out(` ${style.accent(e.predicate)} ${style.dim(render.g.arrow)} ${style.id(e.node)} ${style.dim(`×${e.count}`)}`);
|
|
77
|
+
}
|
|
78
|
+
if (links.in.length > 0) {
|
|
79
|
+
io.out(` ${style.dim("backlinks")}`);
|
|
80
|
+
for (const e of links.in)
|
|
81
|
+
io.out(` ${style.id(e.node)} ${style.dim(render.g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function renderBlockCard(cli, node, history) {
|
|
85
|
+
const { render, style, io } = cli;
|
|
86
|
+
io.out(` ${render.typeGlyph(node.type)} ${style.bold(node.type)} ${style.id(node.id)}`);
|
|
87
|
+
io.out(render.rule(40));
|
|
88
|
+
if (node.placement)
|
|
89
|
+
io.out(` ${style.dim("placement")} parent=${style.id(node.placement.parent ?? "—")} ordinal=${node.placement.ordinal} depth=${node.placement.depth}`);
|
|
90
|
+
if (node.attrs && Object.keys(node.attrs).length > 0) {
|
|
91
|
+
io.out(` ${style.dim("attrs")}`);
|
|
92
|
+
for (const [k, v] of Object.entries(node.attrs))
|
|
93
|
+
io.out(` ${style.accent(k)} ${style.dim("=")} ${fmt(v)}`);
|
|
94
|
+
}
|
|
95
|
+
if (node.text)
|
|
96
|
+
io.out(` ${style.dim("text")} ${node.text}`);
|
|
97
|
+
if (history && history.length > 0) {
|
|
98
|
+
io.out(` ${style.dim("history")}`);
|
|
99
|
+
for (const h of history)
|
|
100
|
+
io.out(` ${style.dim(`#${h.seq}`)} ${style.accent(h.kind)} ${style.dim(h.origin)} ${style.dim(h.ts)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function fmt(v) {
|
|
104
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
105
|
+
}
|
|
106
|
+
export const cmdShow = { name: "show", summary: "Metadata card for a node", run: (cli, a) => runShow(cli, a) };
|
|
107
|
+
//# sourceMappingURL=show.js.map
|