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,231 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, ensureRepo, freshnessSweep, walkMarkdownAsync, rebuildFileStats, } from "@omgbase/core";
|
|
6
|
+
import { columns } from "../render.js";
|
|
7
|
+
import { drainEmbeddings } from "./_embed.js";
|
|
8
|
+
import { ensureFsAdapter, FS_ADAPTER } from "./_source.js";
|
|
9
|
+
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
10
|
+
// `omg source` (ADR-014) — the source registry: where a repo's bytes come from
|
|
11
|
+
// (a filesystem directory today; git/S3/etc. via adapters later). A repo owns
|
|
12
|
+
// identity + history; adding a filesystem source points it at a directory and
|
|
13
|
+
// runs the initial sync. There is no separate "attach/ingest/load" verb —
|
|
14
|
+
// `source add <dir>` is how content enters, and the initial ingest is just that
|
|
15
|
+
// source's first sync (the same reconcile path every later sync uses).
|
|
16
|
+
function help(cli) {
|
|
17
|
+
return renderHelp(cli, {
|
|
18
|
+
name: "source",
|
|
19
|
+
summary: "Where a repo's bytes come from (a filesystem directory today; git/S3/… via adapters later)",
|
|
20
|
+
usage: "source <add|list|attach|detach|rm> …",
|
|
21
|
+
options: [
|
|
22
|
+
["add <dir> [--slug <s>] [--name <n>] [-y]", "point a repo at a directory: creates the repo + runs the initial sync (-y skips the consent prompt)"],
|
|
23
|
+
["list", "list sources and which repos they feed"],
|
|
24
|
+
["attach <name> [--repo <slug>]", "attach an existing source to a repo"],
|
|
25
|
+
["detach <name> [--repo <slug>]", "detach a source from a repo"],
|
|
26
|
+
["rm <name>", "delete a source (and its attachments)"],
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function attachedRepoSlugs(cli, ws, sourceId) {
|
|
31
|
+
return ws.repos().filter((r) => sourcesForRepo(ws.store, r.repoId).some((s) => s.sourceId === sourceId)).map((r) => r.slug);
|
|
32
|
+
}
|
|
33
|
+
async function runSource(cli, args) {
|
|
34
|
+
const sub = args[0];
|
|
35
|
+
if (!sub || sub === "--help" || sub === "help")
|
|
36
|
+
return help(cli);
|
|
37
|
+
const rest = args.slice(1);
|
|
38
|
+
switch (sub) {
|
|
39
|
+
case "add":
|
|
40
|
+
return runAdd(cli, rest);
|
|
41
|
+
case "list":
|
|
42
|
+
return runList(cli);
|
|
43
|
+
case "attach":
|
|
44
|
+
return runAttachDetach(cli, rest, "attach");
|
|
45
|
+
case "detach":
|
|
46
|
+
return runAttachDetach(cli, rest, "detach");
|
|
47
|
+
case "rm":
|
|
48
|
+
return runRm(cli, rest);
|
|
49
|
+
default:
|
|
50
|
+
throw new CliUsageError(`unknown source subcommand '${sub}'`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Walk `abs` for markdown while showing a growing count beside a [y/N] prompt.
|
|
54
|
+
// The scan runs concurrently with the question; the count carries a `+` suffix
|
|
55
|
+
// until the walk completes. Returns the answer plus whether the walk finished.
|
|
56
|
+
async function confirmIngestWithCount(cli, abs, slug) {
|
|
57
|
+
const signal = { aborted: false };
|
|
58
|
+
let count = 0;
|
|
59
|
+
let complete = false;
|
|
60
|
+
const isTTY = cli.io.stdoutTTY && process.stdin.isTTY;
|
|
61
|
+
const render = () => {
|
|
62
|
+
if (!isTTY)
|
|
63
|
+
return;
|
|
64
|
+
process.stderr.write(`\r add source for ${slug} — ${count}${complete ? "" : "+"} files [y/N] `);
|
|
65
|
+
};
|
|
66
|
+
const walk = walkMarkdownAsync(abs, (n) => { count = n; render(); }, signal).then(() => { complete = true; render(); });
|
|
67
|
+
if (!isTTY) {
|
|
68
|
+
// No TTY to prompt: refuse rather than silently ingesting the tree.
|
|
69
|
+
signal.aborted = true;
|
|
70
|
+
await walk;
|
|
71
|
+
return { ok: false, complete };
|
|
72
|
+
}
|
|
73
|
+
render();
|
|
74
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
75
|
+
const answer = await new Promise((res) => rl.question("", res));
|
|
76
|
+
rl.close();
|
|
77
|
+
signal.aborted = true;
|
|
78
|
+
await walk;
|
|
79
|
+
return { ok: /^y(es)?$/i.test(answer.trim()), complete };
|
|
80
|
+
}
|
|
81
|
+
async function runAdd(cli, args) {
|
|
82
|
+
const { values, positionals } = parseArgs({
|
|
83
|
+
args,
|
|
84
|
+
allowPositionals: true,
|
|
85
|
+
options: { slug: { type: "string" }, name: { type: "string" }, yes: { type: "boolean", short: "y" }, help: { type: "boolean" } },
|
|
86
|
+
});
|
|
87
|
+
if (values.help)
|
|
88
|
+
return help(cli);
|
|
89
|
+
const dir = positionals[0];
|
|
90
|
+
if (!dir)
|
|
91
|
+
throw new CliUsageError("source add requires a <dir>");
|
|
92
|
+
const abs = resolve(cli.cwd, dir);
|
|
93
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
94
|
+
throw new EngineErrorLike("target_missing", `no such directory: ${abs}`);
|
|
95
|
+
const slug = values.slug ?? (basename(abs) || "vault");
|
|
96
|
+
const sourceName = values.name ?? `${slug}-fs`;
|
|
97
|
+
const ws = cli.workspace();
|
|
98
|
+
if (getSourceByName(ws.store, sourceName))
|
|
99
|
+
throw new EngineErrorLike("path_taken", `a source named '${sourceName}' already exists`);
|
|
100
|
+
// Consent gate: -y proceeds; a TTY prompts with a live file count; a non-TTY
|
|
101
|
+
// without -y refuses rather than absorbing the tree silently.
|
|
102
|
+
if (!values.yes) {
|
|
103
|
+
const { ok } = await confirmIngestWithCount(cli, abs, slug);
|
|
104
|
+
if (cli.io.stdoutTTY)
|
|
105
|
+
process.stderr.write("\n");
|
|
106
|
+
if (!ok) {
|
|
107
|
+
const why = !process.stdin.isTTY || !cli.io.stdoutTTY ? `refusing without -y (would ingest files under ${shortenHome(abs)})` : "cancelled";
|
|
108
|
+
cli.io.err(cli.style.dim(` ${why}`));
|
|
109
|
+
return EXIT_OK;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// A repo owns identity; register its fs source, then run the initial sync
|
|
113
|
+
// through the ordinary freshness path (the same reconcile every later sync
|
|
114
|
+
// uses). ensureRepo(…, null) mints identity only — the source is explicit.
|
|
115
|
+
const repoId = ensureRepo(ws.store, slug, null);
|
|
116
|
+
ensureFsAdapter(ws.store);
|
|
117
|
+
const sourceId = createSource(ws.store, { name: sourceName, adapter: FS_ADAPTER, config: { root: abs } });
|
|
118
|
+
attachSourceToRepo(ws.store, repoId, sourceId);
|
|
119
|
+
const swept = freshnessSweep(ws.store, repoId, abs);
|
|
120
|
+
rebuildFileStats(ws.store, repoId, abs);
|
|
121
|
+
// Offer to embed freshly-ingested blocks now (consent-gated like the ingest).
|
|
122
|
+
const drained = await drainEmbeddings(cli, ws, repoId, {
|
|
123
|
+
verbose: true,
|
|
124
|
+
confirm: async ({ pending, remote, providerName }) => {
|
|
125
|
+
if (pending === 0)
|
|
126
|
+
return false;
|
|
127
|
+
if (values.yes)
|
|
128
|
+
return true;
|
|
129
|
+
const where = remote ? `${providerName} (remote — block text leaves your machine)` : `${providerName} (local)`;
|
|
130
|
+
return confirmTTY(cli, `embed ${pending} block(s) now via ${where}?`);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
if (cli.flags.mode !== "human") {
|
|
134
|
+
cli.io.out(JSON.stringify({ repo: slug, repoId, source: sourceName, root: abs, ingested: swept.ingested.length, ...(drained ? { embedded: drained.embedded } : {}) }));
|
|
135
|
+
return EXIT_OK;
|
|
136
|
+
}
|
|
137
|
+
const { render, style, io } = cli;
|
|
138
|
+
io.out(` ${render.g.diamond} ${style.accent(slug)} ← ${style.path(shortenHome(abs))} ${style.dim(`${swept.ingested.length} files`)}`);
|
|
139
|
+
if (drained)
|
|
140
|
+
io.err(` ${style.ok(render.g.ok)} embedded ${drained.embedded}, cached ${drained.cached}`);
|
|
141
|
+
return EXIT_OK;
|
|
142
|
+
}
|
|
143
|
+
function runList(cli) {
|
|
144
|
+
const ws = cli.workspace();
|
|
145
|
+
const sources = listSources(ws.store).map((s) => ({ ...s, repos: attachedRepoSlugs(cli, ws, s.sourceId) }));
|
|
146
|
+
if (cli.flags.mode === "json") {
|
|
147
|
+
cli.io.out(JSON.stringify(sources.map((s) => ({ name: s.name, adapter: s.adapter, config: s.config, repos: s.repos }))));
|
|
148
|
+
return EXIT_OK;
|
|
149
|
+
}
|
|
150
|
+
if (cli.flags.mode === "jsonl") {
|
|
151
|
+
for (const s of sources)
|
|
152
|
+
cli.io.out(JSON.stringify({ name: s.name, adapter: s.adapter, config: s.config, repos: s.repos }));
|
|
153
|
+
return EXIT_OK;
|
|
154
|
+
}
|
|
155
|
+
if (cli.flags.mode === "ids") {
|
|
156
|
+
for (const s of sources)
|
|
157
|
+
cli.io.out(s.name);
|
|
158
|
+
return EXIT_OK;
|
|
159
|
+
}
|
|
160
|
+
const { style, io } = cli;
|
|
161
|
+
if (sources.length === 0) {
|
|
162
|
+
io.out(style.dim(" no sources registered"));
|
|
163
|
+
return EXIT_OK;
|
|
164
|
+
}
|
|
165
|
+
const rows = sources.map((s) => [
|
|
166
|
+
` ${style.accent(s.name)}`,
|
|
167
|
+
style.dim(s.adapter),
|
|
168
|
+
style.path(String(s.config.root ?? "")),
|
|
169
|
+
style.dim(s.repos.length > 0 ? `→ ${s.repos.join(", ")}` : "(unattached)"),
|
|
170
|
+
]);
|
|
171
|
+
for (const line of columns(rows))
|
|
172
|
+
io.out(line);
|
|
173
|
+
return EXIT_OK;
|
|
174
|
+
}
|
|
175
|
+
function runAttachDetach(cli, args, verb) {
|
|
176
|
+
const { positionals } = parseArgs({ args, allowPositionals: true, options: {} });
|
|
177
|
+
const name = positionals[0];
|
|
178
|
+
if (!name)
|
|
179
|
+
throw new CliUsageError(`source ${verb} requires a <name>`);
|
|
180
|
+
const ws = cli.workspace();
|
|
181
|
+
const source = getSourceByName(ws.store, name);
|
|
182
|
+
if (!source)
|
|
183
|
+
throw new EngineErrorLike("target_missing", `no source named '${name}'`);
|
|
184
|
+
const repo = cli.repo(ws);
|
|
185
|
+
if (verb === "attach")
|
|
186
|
+
attachSourceToRepo(ws.store, repo.repoId, source.sourceId);
|
|
187
|
+
else
|
|
188
|
+
detachSourceFromRepo(ws.store, repo.repoId, source.sourceId);
|
|
189
|
+
if (cli.flags.mode !== "human") {
|
|
190
|
+
cli.io.out(JSON.stringify({ source: name, repo: repo.slug, attached: verb === "attach" }));
|
|
191
|
+
return EXIT_OK;
|
|
192
|
+
}
|
|
193
|
+
cli.io.out(` ${cli.render.g.ok} ${verb === "attach" ? "attached" : "detached"} ${cli.style.accent(name)} ${verb === "attach" ? "→" : "⇸"} ${cli.style.accent(repo.slug)}`);
|
|
194
|
+
return EXIT_OK;
|
|
195
|
+
}
|
|
196
|
+
function runRm(cli, args) {
|
|
197
|
+
const { positionals } = parseArgs({ args, allowPositionals: true, options: {} });
|
|
198
|
+
const name = positionals[0];
|
|
199
|
+
if (!name)
|
|
200
|
+
throw new CliUsageError("source rm requires a <name>");
|
|
201
|
+
const ws = cli.workspace();
|
|
202
|
+
const source = getSourceByName(ws.store, name);
|
|
203
|
+
if (!source)
|
|
204
|
+
throw new EngineErrorLike("target_missing", `no source named '${name}'`);
|
|
205
|
+
deleteSource(ws.store, source.sourceId);
|
|
206
|
+
if (cli.flags.mode !== "human") {
|
|
207
|
+
cli.io.out(JSON.stringify({ source: name, deleted: true }));
|
|
208
|
+
return EXIT_OK;
|
|
209
|
+
}
|
|
210
|
+
cli.io.out(` ${cli.render.g.ok} deleted source ${cli.style.accent(name)}`);
|
|
211
|
+
return EXIT_OK;
|
|
212
|
+
}
|
|
213
|
+
// small local helpers ---------------------------------------------------------
|
|
214
|
+
async function confirmTTY(cli, question) {
|
|
215
|
+
if (!cli.io.stdoutTTY || !process.stdin.isTTY)
|
|
216
|
+
return false;
|
|
217
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
218
|
+
try {
|
|
219
|
+
const answer = await new Promise((res) => rl.question(`${question} [y/N] `, res));
|
|
220
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
rl.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function shortenHome(p) {
|
|
227
|
+
const home = process.env.HOME;
|
|
228
|
+
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
229
|
+
}
|
|
230
|
+
export const cmdSource = { name: "source", summary: "Where a repo's bytes come from (add/list/attach/detach/rm)", run: (cli, a) => runSource(cli, a) };
|
|
231
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { reposStatus, syncStatus, watchLeaseLive } from "@omgbase/core";
|
|
3
|
+
import { EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
// `omg status` (11 §5.2): repos_status + sync_status + watch-lease probe +
|
|
5
|
+
// embedding queue depth. The "where am I" command — the visual showcase.
|
|
6
|
+
function runStatus(cli, args) {
|
|
7
|
+
const { values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
|
|
8
|
+
if (values.help) {
|
|
9
|
+
return renderHelp(cli, {
|
|
10
|
+
name: "status",
|
|
11
|
+
summary: "Where am I: the active repo, doc/block/commit counts, sync convergence, watcher, embed queue",
|
|
12
|
+
usage: "status [--repo <slug>] [--json]",
|
|
13
|
+
options: [["--json", "a flat object: repo, root, docs, blocks, commits, openEdges, convergent, disk, watcher, embedQueue"]],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const ws = cli.workspace();
|
|
17
|
+
const repo = cli.repo(ws);
|
|
18
|
+
const rs = reposStatus(ws.store, repo.repoId, repo.rootPath ?? undefined);
|
|
19
|
+
const ss = syncStatus(ws.store, repo.repoId, repo.rootPath ?? undefined);
|
|
20
|
+
const watcher = watchLeaseLive(ws.omgbaseDir);
|
|
21
|
+
// Embedding queue depth: count blocks whose embeddings are missing is a v1
|
|
22
|
+
// approximation; the queue table isn't wired yet, so report 0 (05 §6 stub).
|
|
23
|
+
const queued = 0;
|
|
24
|
+
const payload = {
|
|
25
|
+
repo: repo.slug,
|
|
26
|
+
root: repo.rootPath,
|
|
27
|
+
docs: rs.docs,
|
|
28
|
+
blocks: rs.blocks,
|
|
29
|
+
commits: rs.commits,
|
|
30
|
+
openEdges: rs.openEdges,
|
|
31
|
+
unconverged: rs.unconverged,
|
|
32
|
+
convergent: ss.convergent,
|
|
33
|
+
disk: ss.disk,
|
|
34
|
+
lastCommitSeq: ss.lastCommitSeq,
|
|
35
|
+
watcher: watcher ? "live" : "none",
|
|
36
|
+
embedQueue: queued,
|
|
37
|
+
};
|
|
38
|
+
if (cli.flags.mode !== "human") {
|
|
39
|
+
cli.io.out(JSON.stringify(payload));
|
|
40
|
+
return EXIT_OK;
|
|
41
|
+
}
|
|
42
|
+
const { render, style, io } = cli;
|
|
43
|
+
const g = render.g;
|
|
44
|
+
io.out(render.wordmark(repo.slug));
|
|
45
|
+
io.out(` ${style.path(repo.rootPath ? shortenHome(repo.rootPath) : "(no source — headless)")}`);
|
|
46
|
+
io.out(render.rule(40));
|
|
47
|
+
// Two-column figure grid.
|
|
48
|
+
const left = [
|
|
49
|
+
[`${style.path(g.doc)} docs`, String(rs.docs)],
|
|
50
|
+
[`${style.dim(g.block)} blocks`, String(rs.blocks)],
|
|
51
|
+
[`${g.diamond} commits`, String(rs.commits)],
|
|
52
|
+
[`${g.arrow} edges`, String(rs.openEdges)],
|
|
53
|
+
];
|
|
54
|
+
const right = [
|
|
55
|
+
["watcher", watcher ? render.statusDot("live", "live") : render.statusDot("none", "none")],
|
|
56
|
+
["synced", ss.convergent ? render.statusDot("ok", "converged") : render.statusDot("warn", syncedLabel(ss, rs.unconverged))],
|
|
57
|
+
["queue", queued === 0 ? style.dim("empty") : style.warn(`${queued} queued`)],
|
|
58
|
+
["commit#", style.dim(String(ss.lastCommitSeq))],
|
|
59
|
+
];
|
|
60
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
61
|
+
const l = left[i];
|
|
62
|
+
const r = right[i];
|
|
63
|
+
const lCell = l ? ` ${l[0]} ${style.bold(l[1])}` : " ";
|
|
64
|
+
const rCell = r ? `${style.dim(r[0].padEnd(8))}${r[1]}` : "";
|
|
65
|
+
io.out(padVisible(lCell, 26) + rCell);
|
|
66
|
+
}
|
|
67
|
+
return EXIT_OK;
|
|
68
|
+
}
|
|
69
|
+
// Summarize why the repo isn't convergent: DB-internal lag and/or on-disk drift.
|
|
70
|
+
function syncedLabel(ss, unconverged) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (unconverged > 0)
|
|
73
|
+
parts.push(`${unconverged} behind`);
|
|
74
|
+
if (ss.disk.deleted > 0)
|
|
75
|
+
parts.push(`${ss.disk.deleted} deleted`);
|
|
76
|
+
if (ss.disk.changed > 0)
|
|
77
|
+
parts.push(`${ss.disk.changed} drifted`);
|
|
78
|
+
if (ss.disk.untracked > 0)
|
|
79
|
+
parts.push(`${ss.disk.untracked} untracked`);
|
|
80
|
+
if (!ss.diskChecked)
|
|
81
|
+
parts.push("disk unverified");
|
|
82
|
+
return parts.length > 0 ? parts.join(", ") : "unconverged";
|
|
83
|
+
}
|
|
84
|
+
// Local width-aware pad (mirrors render.visibleWidth without importing the class).
|
|
85
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
86
|
+
function padVisible(s, w) {
|
|
87
|
+
const vis = s.replace(ANSI, "").length;
|
|
88
|
+
return vis >= w ? s : s + " ".repeat(w - vis);
|
|
89
|
+
}
|
|
90
|
+
function shortenHome(p) {
|
|
91
|
+
const home = process.env.HOME;
|
|
92
|
+
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
93
|
+
}
|
|
94
|
+
export const cmdStatus = { name: "status", summary: "Where am I: repo, sync, watcher, queue", run: (cli, a) => runStatus(cli, a) };
|
|
95
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, EngineError } from "@omgbase/core";
|
|
4
|
+
import { runFsMirror } from "@omgbase/sync";
|
|
5
|
+
import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
6
|
+
import { loadEmbedding } from "./_embed.js";
|
|
7
|
+
import { openRepoSource } from "./_source.js";
|
|
8
|
+
// `omg sync` — reconcile a repo with its filesystem source. One verb, three modes:
|
|
9
|
+
// omg sync one-shot local freshness sweep (fs → DB)
|
|
10
|
+
// omg sync --watch stay live locally (external fs-adapter watcher)
|
|
11
|
+
// omg sync --server <cmd> … the same, but against a remote engine over MCP
|
|
12
|
+
// (the coordinator — mirrors `omgbase-sync`)
|
|
13
|
+
// The `--server` form is the global remote flag (context REMOTE_OK); it shares
|
|
14
|
+
// `runFsMirror` with the standalone `omgbase-sync` bin. There is no separate
|
|
15
|
+
// `omg watch` — watching is `omg sync --watch`. The local one-shot IS the
|
|
16
|
+
// explicit form of the freshness sweep every read runs by default.
|
|
17
|
+
function help(cli) {
|
|
18
|
+
return renderHelp(cli, {
|
|
19
|
+
name: "sync",
|
|
20
|
+
summary: "Reconcile a repo with its filesystem source (one-shot by default; the explicit form of the freshness sweep every read runs)",
|
|
21
|
+
usage: ["sync [--watch]", 'sync --server <cmd|url> [-H "Name: value"]… [--root <dir>] [--out]'],
|
|
22
|
+
options: [
|
|
23
|
+
["--watch", "stay live and reconcile edits as they land"],
|
|
24
|
+
["--server <cmd|url>", "mirror a local directory into a remote engine over MCP (spawns <cmd>, or an http(s) url over Streamable HTTP)"],
|
|
25
|
+
['-H "Name: value"', "(--server url) extra HTTP header, repeatable"],
|
|
26
|
+
["--root <dir>", "(--server) the directory to mirror (default cwd)"],
|
|
27
|
+
["--out", "(--server) also export engine-authored changes back to disk"],
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function runSync(cli, args) {
|
|
32
|
+
const { values } = parseArgs({
|
|
33
|
+
args,
|
|
34
|
+
allowPositionals: true,
|
|
35
|
+
options: { watch: { type: "boolean" }, out: { type: "boolean" }, root: { type: "string" }, help: { type: "boolean" } },
|
|
36
|
+
});
|
|
37
|
+
if (values.help)
|
|
38
|
+
return help(cli);
|
|
39
|
+
// Remote mode: run the coordinator against an MCP server (shared with the
|
|
40
|
+
// omgbase-sync bin). No local workspace needed — the fs dir is the source.
|
|
41
|
+
if (cli.flags.server !== undefined) {
|
|
42
|
+
const root = resolve(cli.cwd, values.root ?? ".");
|
|
43
|
+
const server = cli.flags.server.split(/\s+/).filter(Boolean);
|
|
44
|
+
await runFsMirror({
|
|
45
|
+
server,
|
|
46
|
+
root,
|
|
47
|
+
...(values.watch ? { watch: true } : {}),
|
|
48
|
+
...(values.out ? { out: true } : {}),
|
|
49
|
+
log: (msg) => cli.io.err(cli.style.dim(` ${msg}`)),
|
|
50
|
+
});
|
|
51
|
+
return EXIT_OK;
|
|
52
|
+
}
|
|
53
|
+
// Local mode: resolve the workspace + repo and reconcile in-process.
|
|
54
|
+
const ws = cli.workspace();
|
|
55
|
+
const repo = cli.repo(ws);
|
|
56
|
+
return values.watch ? runLocalWatch(cli, ws, repo) : runOneShot(cli, ws, repo);
|
|
57
|
+
}
|
|
58
|
+
function runOneShot(cli, ws, repo) {
|
|
59
|
+
if (!repo.rootPath) {
|
|
60
|
+
if (cli.flags.mode === "human")
|
|
61
|
+
cli.io.out(cli.style.dim(` ${repo.slug} has no filesystem source — nothing to sync`));
|
|
62
|
+
else
|
|
63
|
+
cli.io.out(JSON.stringify({ scanned: 0, ingested: [], deleted: [], conflicted: [], changed: false }));
|
|
64
|
+
return EXIT_OK;
|
|
65
|
+
}
|
|
66
|
+
const result = freshnessSweep(ws.store, repo.repoId, repo.rootPath);
|
|
67
|
+
if (cli.flags.mode !== "human") {
|
|
68
|
+
cli.io.out(JSON.stringify(result));
|
|
69
|
+
return EXIT_OK;
|
|
70
|
+
}
|
|
71
|
+
const { render, style, io } = cli;
|
|
72
|
+
const g = render.g;
|
|
73
|
+
io.out(render.wordmark("sync"));
|
|
74
|
+
io.out(render.rule(40));
|
|
75
|
+
io.out(` ${style.dim("scanned")} ${result.scanned} files`);
|
|
76
|
+
if (result.ingested.length > 0) {
|
|
77
|
+
io.out(` ${style.ok(g.ok)} ingested ${result.ingested.length}`);
|
|
78
|
+
for (const p of result.ingested)
|
|
79
|
+
io.out(` ${style.accent(p)}`);
|
|
80
|
+
}
|
|
81
|
+
if (result.deleted.length > 0) {
|
|
82
|
+
io.out(` ${style.err(g.err)} deleted ${result.deleted.length}`);
|
|
83
|
+
for (const p of result.deleted)
|
|
84
|
+
io.out(` ${style.dim(p)}`);
|
|
85
|
+
}
|
|
86
|
+
if (result.conflicted.length > 0) {
|
|
87
|
+
io.out(` ${style.warn(g.warn)} conflicts ${result.conflicted.length}`);
|
|
88
|
+
for (const p of result.conflicted)
|
|
89
|
+
io.out(` ${style.warn(p)}`);
|
|
90
|
+
}
|
|
91
|
+
if (!result.changed)
|
|
92
|
+
io.out(` ${style.dim("already up to date")}`);
|
|
93
|
+
return EXIT_OK;
|
|
94
|
+
}
|
|
95
|
+
// Local live watcher (the former `omg watch`): prime with a sweep, then run the
|
|
96
|
+
// external fs-adapter watcher until a signal, keeping embeddings warm.
|
|
97
|
+
async function runLocalWatch(cli, ws, repo) {
|
|
98
|
+
const lease = WatchLease.tryAcquire(ws.omgbaseDir);
|
|
99
|
+
if (!lease)
|
|
100
|
+
throw new EngineErrorLike("target_missing", "another watcher already holds the lease for this workspace");
|
|
101
|
+
// A configured-but-broken embedder disables auto-embed but must not take the
|
|
102
|
+
// watcher down: warn loudly and keep watching (vectors just go stale until the
|
|
103
|
+
// embedder is fixed and a drain catches up). An unset provider is silent.
|
|
104
|
+
let embedding = null;
|
|
105
|
+
try {
|
|
106
|
+
embedding = await loadEmbedding(ws, repo.repoId);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err instanceof EngineError && err.code === "embedder_failed") {
|
|
110
|
+
const d = (err.data ?? {});
|
|
111
|
+
cli.io.err(cli.style.err(` ✖ EMBEDDER NONFUNCTIONAL — auto-embed disabled (provider: ${d.provider ?? "?"}; reason: ${d.reason ?? err.message})`));
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
const drainer = embedding
|
|
117
|
+
? new EmbedDrainer(ws.store, repo.repoId, embedding.worker, {
|
|
118
|
+
onDrain: ({ embedded }) => cli.io.err(cli.style.dim(` embedded ${embedded} block(s)`)),
|
|
119
|
+
onError: (err) => cli.io.err(cli.style.dim(` embed drain failed: ${String(err)}`)),
|
|
120
|
+
})
|
|
121
|
+
: null;
|
|
122
|
+
if (repo.rootPath)
|
|
123
|
+
freshnessSweep(ws.store, repo.repoId, repo.rootPath); // start fresh
|
|
124
|
+
const source = await openRepoSource(ws.store, repo);
|
|
125
|
+
if (!source) {
|
|
126
|
+
cli.io.err(cli.style.dim(` ${repo.slug} has no filesystem source — nothing to watch`));
|
|
127
|
+
lease.release();
|
|
128
|
+
if (drainer)
|
|
129
|
+
await drainer.close();
|
|
130
|
+
if (embedding)
|
|
131
|
+
await embedding.close();
|
|
132
|
+
return EXIT_OK;
|
|
133
|
+
}
|
|
134
|
+
const watcher = new Watcher(ws.store, repo.repoId, source, {
|
|
135
|
+
onCheckpoint: (r) => {
|
|
136
|
+
if (r.ingested.length || r.deleted.length || r.conflicted.length) {
|
|
137
|
+
cli.io.err(`${cli.style.ok(cli.render.g.sync)} +${r.ingested.length} -${r.deleted.length}${r.conflicted.length ? ` !${r.conflicted.length}` : ""}`);
|
|
138
|
+
drainer?.schedule();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
onError: (err) => cli.io.err(cli.style.err(` watch error: ${String(err)}`)),
|
|
142
|
+
});
|
|
143
|
+
await watcher.start();
|
|
144
|
+
drainer?.schedule(); // embed anything already stale at startup, in the background
|
|
145
|
+
cli.io.err(cli.style.dim(` watching ${repo.slug} — Ctrl-C to stop${drainer ? " · auto-embed on" : ""}`));
|
|
146
|
+
await new Promise((resolveWait) => {
|
|
147
|
+
const stop = () => {
|
|
148
|
+
const watchdog = setTimeout(() => process.exit(EXIT_OK), 3000);
|
|
149
|
+
void (async () => {
|
|
150
|
+
try {
|
|
151
|
+
await watcher.stop();
|
|
152
|
+
await source.close();
|
|
153
|
+
if (drainer) {
|
|
154
|
+
try {
|
|
155
|
+
await drainer.flush();
|
|
156
|
+
}
|
|
157
|
+
catch { /* reported via onError */ }
|
|
158
|
+
await drainer.close();
|
|
159
|
+
}
|
|
160
|
+
if (embedding)
|
|
161
|
+
await embedding.close();
|
|
162
|
+
lease.release();
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
clearTimeout(watchdog);
|
|
166
|
+
resolveWait();
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
};
|
|
170
|
+
process.once("SIGINT", stop);
|
|
171
|
+
process.once("SIGTERM", stop);
|
|
172
|
+
});
|
|
173
|
+
return EXIT_OK;
|
|
174
|
+
}
|
|
175
|
+
export const cmdSync = { name: "sync", summary: "Reconcile a repo with its filesystem source (--watch to stay live; --server for remote)", run: (cli, a) => runSync(cli, a) };
|
|
176
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { docsUpdate, renderOpsetPlan, isValidId } from "@omgbase/core";
|
|
3
|
+
import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { readContent, extractContentOpts } from "./_mutate.js";
|
|
5
|
+
import { cmdUpdate as cmdUpdateBlock } from "./mutate.js";
|
|
6
|
+
import { remoteCall } from "./_remote.js";
|
|
7
|
+
// `omg update <target> (-f file | -)`: a polymorphic-target command (the omgbase
|
|
8
|
+
// north-star — every command accepts any entity ref). A block id (b_…) replaces
|
|
9
|
+
// that block's markdown (CAS-pinned, the low-level op). A doc id or path is the
|
|
10
|
+
// note's docs.update: submit a complete proposed document; the engine reconciles
|
|
11
|
+
// it against the current tree, preserves stable block ids for recognizably-same
|
|
12
|
+
// structure, and commits the derived opset through the kernel write path. Global
|
|
13
|
+
// --dry-run (or --plan) prints the plan (identity effects + summary), commits
|
|
14
|
+
// nothing.
|
|
15
|
+
function runUpdate(cli, args) {
|
|
16
|
+
// Peek at the target to dispatch: a b_ id is a block replace; anything else
|
|
17
|
+
// (d_ id or path) is a whole-document update. The block path re-parses args.
|
|
18
|
+
const peek = extractContentOpts(args);
|
|
19
|
+
const target = peek.rest.find((a) => !a.startsWith("-"));
|
|
20
|
+
if (target && isValidId(target, "b"))
|
|
21
|
+
return cmdUpdateBlock.run(cli, args);
|
|
22
|
+
return runDocUpdate(cli, args);
|
|
23
|
+
}
|
|
24
|
+
async function runDocUpdate(cli, args) {
|
|
25
|
+
const content = extractContentOpts(args);
|
|
26
|
+
const { values, positionals } = parseArgs({
|
|
27
|
+
args: content.rest,
|
|
28
|
+
allowPositionals: true,
|
|
29
|
+
options: { actor: { type: "string" }, reason: { type: "string" }, plan: { type: "boolean" }, help: { type: "boolean" } },
|
|
30
|
+
});
|
|
31
|
+
if (values.help) {
|
|
32
|
+
return renderHelp(cli, {
|
|
33
|
+
name: "update",
|
|
34
|
+
summary: "Replace a block's markdown (b_… target), or reconcile a whole document from complete new bytes (doc id/path)",
|
|
35
|
+
usage: "update <block|doc> (-m <markdown> | -f <file> | -) [--plan] [--expect <hash>] [--reason <s>] [--actor <s>] [--dry-run]",
|
|
36
|
+
options: [
|
|
37
|
+
["-m <markdown>", "content inline"],
|
|
38
|
+
["-f <file>", "content from a file"],
|
|
39
|
+
["-", "content from stdin"],
|
|
40
|
+
["--plan", "(doc) print the reconciliation opset — identity effects + summary — and commit nothing"],
|
|
41
|
+
["--expect <hash>", "(block) fail with stale_expectation unless the block's hash still matches"],
|
|
42
|
+
["--reason <s>", "commit reason recorded in history"],
|
|
43
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const doc = positionals[0];
|
|
48
|
+
if (!doc)
|
|
49
|
+
throw new CliUsageError("update requires a <doc> (id or path)");
|
|
50
|
+
const bytes = readContent(content);
|
|
51
|
+
const dryRun = Boolean(values.plan) || cli.flags.dryRun;
|
|
52
|
+
let opset;
|
|
53
|
+
let result;
|
|
54
|
+
if (cli.flags.server) {
|
|
55
|
+
// Remote: docs_update reconciles + commits server-side, returning the same
|
|
56
|
+
// { opset, result } — render unchanged.
|
|
57
|
+
const r = await remoteCall(cli, "docs_update", {
|
|
58
|
+
doc,
|
|
59
|
+
content: bytes,
|
|
60
|
+
...(values.reason ? { reason: values.reason } : {}),
|
|
61
|
+
...(dryRun ? { dry_run: true } : {}),
|
|
62
|
+
});
|
|
63
|
+
opset = r.opset;
|
|
64
|
+
result = r.result;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const ws = cli.workspace();
|
|
68
|
+
const repo = cli.repo(ws);
|
|
69
|
+
if (!repo.rootPath)
|
|
70
|
+
throw new CliUsageError(`repo '${repo.slug}' has no filesystem source; 'update' needs a working tree`);
|
|
71
|
+
const ctx = {
|
|
72
|
+
repoId: repo.repoId,
|
|
73
|
+
rootPath: repo.rootPath,
|
|
74
|
+
...(ws.omgbaseDir ? { omgbaseDir: ws.omgbaseDir } : {}),
|
|
75
|
+
...(values.actor ? { actor: values.actor } : {}),
|
|
76
|
+
};
|
|
77
|
+
({ opset, result } = docsUpdate(ws.store, ctx, doc, bytes, {
|
|
78
|
+
dryRun,
|
|
79
|
+
...(values.reason ? { reason: values.reason } : {}),
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
if (cli.flags.mode !== "human") {
|
|
83
|
+
cli.io.out(JSON.stringify({ opset, result }));
|
|
84
|
+
return EXIT_OK;
|
|
85
|
+
}
|
|
86
|
+
if (dryRun || result === null) {
|
|
87
|
+
const s = opset.summary;
|
|
88
|
+
cli.io.err(cli.style.dim(` plan for ${cli.style.accent(opset.target.path)}${opset.converges ? "" : " — DOES NOT CONVERGE (will not apply)"}`));
|
|
89
|
+
cli.io.out(renderOpsetPlan(opset));
|
|
90
|
+
void s;
|
|
91
|
+
return EXIT_OK;
|
|
92
|
+
}
|
|
93
|
+
const g = cli.render.g;
|
|
94
|
+
cli.io.err(cli.style.dim(` ${cli.style.ok(g.ok)} updated ${cli.style.accent(opset.target.path)} · ${opset.summary.preserved} preserved, ${opset.summary.updated} updated, ${opset.summary.moved} moved, ${opset.summary.created} created, ${opset.summary.removed} removed`));
|
|
95
|
+
for (const id of result.results.flatMap((r) => r.ids))
|
|
96
|
+
cli.io.out(id);
|
|
97
|
+
return EXIT_OK;
|
|
98
|
+
}
|
|
99
|
+
export const cmdUpdateDoc = { name: "update", summary: "Replace a block, or reconcile a whole document (identity-preserving)", run: (c, a) => runUpdate(c, a) };
|
|
100
|
+
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Cli } from "./context.js";
|
|
2
|
+
import { CliUsageError } from "./output.js";
|
|
3
|
+
export interface Command {
|
|
4
|
+
name: string;
|
|
5
|
+
aliases?: string[];
|
|
6
|
+
summary: string;
|
|
7
|
+
/** false → hidden from the default help listing (dev/advanced). */
|
|
8
|
+
listed?: boolean;
|
|
9
|
+
run(cli: Cli, args: string[]): number | Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
export declare const COMMANDS: Command[];
|
|
12
|
+
export declare function resolveCommand(name: string): Command | null;
|
|
13
|
+
/**
|
|
14
|
+
* The usage error for an unrecognized command: names the typo, points at the
|
|
15
|
+
* command list (quoting the binary as it was invoked), and — when one listed
|
|
16
|
+
* command or alias is within two edits — offers it as "did you mean".
|
|
17
|
+
*/
|
|
18
|
+
export declare function unknownCommandError(prog: string, name: string): CliUsageError;
|