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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cmd/_embed.d.ts +55 -0
  3. package/dist/src/cmd/_embed.js +102 -0
  4. package/dist/src/cmd/_mutate.d.ts +47 -0
  5. package/dist/src/cmd/_mutate.js +211 -0
  6. package/dist/src/cmd/_remote.d.ts +8 -0
  7. package/dist/src/cmd/_remote.js +61 -0
  8. package/dist/src/cmd/_source.d.ts +13 -0
  9. package/dist/src/cmd/_source.js +47 -0
  10. package/dist/src/cmd/admin.d.ts +6 -0
  11. package/dist/src/cmd/admin.js +289 -0
  12. package/dist/src/cmd/bootstrap.d.ts +3 -0
  13. package/dist/src/cmd/bootstrap.js +252 -0
  14. package/dist/src/cmd/cat.d.ts +2 -0
  15. package/dist/src/cmd/cat.js +80 -0
  16. package/dist/src/cmd/diff.d.ts +2 -0
  17. package/dist/src/cmd/diff.js +76 -0
  18. package/dist/src/cmd/docs.d.ts +6 -0
  19. package/dist/src/cmd/docs.js +154 -0
  20. package/dist/src/cmd/edit.d.ts +2 -0
  21. package/dist/src/cmd/edit.js +74 -0
  22. package/dist/src/cmd/find.d.ts +2 -0
  23. package/dist/src/cmd/find.js +98 -0
  24. package/dist/src/cmd/help.d.ts +2 -0
  25. package/dist/src/cmd/help.js +47 -0
  26. package/dist/src/cmd/hist.d.ts +2 -0
  27. package/dist/src/cmd/hist.js +58 -0
  28. package/dist/src/cmd/links.d.ts +2 -0
  29. package/dist/src/cmd/links.js +76 -0
  30. package/dist/src/cmd/log.d.ts +2 -0
  31. package/dist/src/cmd/log.js +109 -0
  32. package/dist/src/cmd/ls.d.ts +2 -0
  33. package/dist/src/cmd/ls.js +88 -0
  34. package/dist/src/cmd/mcp.d.ts +2 -0
  35. package/dist/src/cmd/mcp.js +192 -0
  36. package/dist/src/cmd/mutate.d.ts +10 -0
  37. package/dist/src/cmd/mutate.js +352 -0
  38. package/dist/src/cmd/node.d.ts +2 -0
  39. package/dist/src/cmd/node.js +66 -0
  40. package/dist/src/cmd/oqx.d.ts +2 -0
  41. package/dist/src/cmd/oqx.js +193 -0
  42. package/dist/src/cmd/outline.d.ts +2 -0
  43. package/dist/src/cmd/outline.js +87 -0
  44. package/dist/src/cmd/retarget.d.ts +2 -0
  45. package/dist/src/cmd/retarget.js +91 -0
  46. package/dist/src/cmd/run.d.ts +2 -0
  47. package/dist/src/cmd/run.js +135 -0
  48. package/dist/src/cmd/shell.d.ts +2 -0
  49. package/dist/src/cmd/shell.js +159 -0
  50. package/dist/src/cmd/show.d.ts +2 -0
  51. package/dist/src/cmd/show.js +107 -0
  52. package/dist/src/cmd/source.d.ts +2 -0
  53. package/dist/src/cmd/source.js +231 -0
  54. package/dist/src/cmd/status.d.ts +2 -0
  55. package/dist/src/cmd/status.js +95 -0
  56. package/dist/src/cmd/sync.d.ts +2 -0
  57. package/dist/src/cmd/sync.js +176 -0
  58. package/dist/src/cmd/update.d.ts +2 -0
  59. package/dist/src/cmd/update.js +100 -0
  60. package/dist/src/commands.d.ts +18 -0
  61. package/dist/src/commands.js +112 -0
  62. package/dist/src/context.d.ts +73 -0
  63. package/dist/src/context.js +80 -0
  64. package/dist/src/dispatch.d.ts +8 -0
  65. package/dist/src/dispatch.js +148 -0
  66. package/dist/src/main.d.ts +3 -0
  67. package/dist/src/main.js +67 -0
  68. package/dist/src/output.d.ts +60 -0
  69. package/dist/src/output.js +131 -0
  70. package/dist/src/render.d.ts +44 -0
  71. package/dist/src/render.js +116 -0
  72. package/dist/src/shell/refs.d.ts +37 -0
  73. package/dist/src/shell/refs.js +111 -0
  74. package/dist/src/shell/session.d.ts +71 -0
  75. package/dist/src/shell/session.js +317 -0
  76. package/dist/src/shell/tokenize.d.ts +3 -0
  77. package/dist/src/shell/tokenize.js +79 -0
  78. package/dist/src/style.d.ts +58 -0
  79. package/dist/src/style.js +159 -0
  80. package/package.json +63 -0
@@ -0,0 +1,289 @@
1
+ import { parseArgs } from "node:util";
2
+ import { rebuildIndex, runGc, reposStatus, buildEmbedTasks, buildDocEmbedTasks, resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, RepoSelectionError, } from "@omgbase/core";
3
+ import { CliUsageError, EngineErrorLike, EXIT_OK, EXIT_ERROR, renderHelp } from "../output.js";
4
+ import { loadEmbedding, drainEmbeddings } from "./_embed.js";
5
+ // admin/maintenance (11 §5.8–5.9). The live watcher moved to `omg sync --watch`.
6
+ // ---- rebuild-index ----------------------------------------------------------
7
+ function runRebuild(cli, args) {
8
+ const { values } = parseArgs({
9
+ args,
10
+ allowPositionals: true,
11
+ options: { sections: { type: "boolean" }, edges: { type: "boolean" }, fts: { type: "boolean" }, "block-changes": { type: "boolean" }, all: { type: "boolean" }, help: { type: "boolean" } },
12
+ });
13
+ if (values.help) {
14
+ return renderHelp(cli, {
15
+ name: "rebuild-index",
16
+ summary: "Rebuild derived tables from the authoritative docs/blocks (safe any time; never touches content or history)",
17
+ usage: "rebuild-index [--sections|--edges|--fts|--block-changes|--all]",
18
+ options: [
19
+ ["--sections", "section spans"],
20
+ ["--edges", "the link graph"],
21
+ ["--fts", "the full-text index"],
22
+ ["--block-changes", "the per-block change log"],
23
+ ["--all", "everything (default)"],
24
+ ],
25
+ });
26
+ }
27
+ const target = values.sections ? "sections" : values.edges ? "edges" : values.fts ? "fts" : values["block-changes"] ? "block_changes" : "all";
28
+ const ws = cli.workspace();
29
+ rebuildIndex(ws.store, target);
30
+ cli.io.err(cli.style.dim(` ${cli.style.ok(cli.render.g.ok)} rebuilt ${target}`));
31
+ return EXIT_OK;
32
+ }
33
+ // ---- gc ---------------------------------------------------------------------
34
+ function runGcCmd(cli, args) {
35
+ const { values } = parseArgs({ args, allowPositionals: true, options: { "dry-run": { type: "boolean" }, help: { type: "boolean" } } });
36
+ if (values.help) {
37
+ return renderHelp(cli, {
38
+ name: "gc",
39
+ summary: "Mark-and-sweep unreferenced blobs and tree nodes (refuses unless gc.enabled is set in repo settings)",
40
+ usage: "gc [--dry-run]",
41
+ options: [["--dry-run", "report what would be swept, sweep nothing (allowed even when gc is disabled)"]],
42
+ });
43
+ }
44
+ // `--dry-run` is also a global flag (parseGlobals consumes it) — honor either spelling.
45
+ const dryRun = Boolean(values["dry-run"]) || cli.flags.dryRun;
46
+ const ws = cli.workspace();
47
+ const repo = cli.repo(ws);
48
+ const settings = resolveSettings(ws.store, repo.repoId);
49
+ const enabled = Boolean(settings.gc?.enabled);
50
+ if (!enabled && !dryRun) {
51
+ throw new EngineErrorLike("target_missing", "gc is disabled; set gc.enabled=true (or use --dry-run)");
52
+ }
53
+ const result = runGc(ws.store, { enabled: enabled || dryRun });
54
+ if (cli.flags.mode !== "human")
55
+ cli.io.out(JSON.stringify(result));
56
+ else
57
+ cli.io.err(cli.style.dim(` swept ${result.blobsSwept} blobs, ${result.treeNodesSwept} tree nodes`));
58
+ return EXIT_OK;
59
+ }
60
+ // ---- doctor -----------------------------------------------------------------
61
+ function runDoctor(cli, args) {
62
+ const { values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
63
+ if (values.help) {
64
+ return renderHelp(cli, {
65
+ name: "doctor",
66
+ summary: "Invariant sweep: convergence, FTS rows vs live blocks, dangling revisions, SQLite integrity — exit 1 on any failure (CI-able)",
67
+ usage: "doctor [--json]",
68
+ options: [["--json", "`{ ok, checks: [{ name, ok, detail }] }`"]],
69
+ });
70
+ }
71
+ const ws = cli.workspace();
72
+ const repo = cli.repo(ws);
73
+ const db = ws.store.db;
74
+ const checks = [];
75
+ const status = reposStatus(ws.store, repo.repoId);
76
+ checks.push({ name: "convergence", ok: status.unconverged === 0, detail: `${status.unconverged} unconverged` });
77
+ const ftsCount = db.prepare("SELECT count(*) c FROM blocks_fts").get().c;
78
+ const liveBlocks = db.prepare("SELECT count(*) c FROM blocks WHERE deleted_commit IS NULL").get().c;
79
+ checks.push({ name: "fts rows == live blocks", ok: ftsCount === liveBlocks, detail: `fts=${ftsCount} live=${liveBlocks}` });
80
+ const dangling = db.prepare("SELECT count(*) c FROM docs d WHERE d.deleted_commit IS NULL AND d.current_rev IS NOT NULL AND NOT EXISTS (SELECT 1 FROM revisions r WHERE r.rev_id = d.current_rev)").get().c;
81
+ checks.push({ name: "no dangling current_rev", ok: dangling === 0, detail: `${dangling} dangling` });
82
+ const integrity = db.pragma("integrity_check", { simple: true });
83
+ checks.push({ name: "sqlite integrity", ok: integrity === "ok", detail: integrity });
84
+ const allOk = checks.every((c) => c.ok);
85
+ if (cli.flags.mode !== "human") {
86
+ cli.io.out(JSON.stringify({ ok: allOk, checks }));
87
+ return allOk ? EXIT_OK : EXIT_ERROR;
88
+ }
89
+ const { style, io, render } = cli;
90
+ for (const c of checks) {
91
+ const mark = c.ok ? style.ok(render.g.ok) : style.err(render.g.err);
92
+ io.out(` ${mark} ${c.name}${c.detail && !c.ok ? style.dim(` (${c.detail})`) : ""}`);
93
+ }
94
+ return allOk ? EXIT_OK : EXIT_ERROR;
95
+ }
96
+ function resolveConfigScope(cli, ws) {
97
+ if (cli.flags.repo === "")
98
+ return { kind: "workspace" };
99
+ try {
100
+ const repo = cli.repo(ws);
101
+ return { kind: "repo", repoId: repo.repoId, slug: repo.slug };
102
+ }
103
+ catch (err) {
104
+ // An explicit --repo <slug> that doesn't resolve is a real error. A bare
105
+ // (cwd-based) selection that's ambiguous or empty just means "workspace
106
+ // layer" — the workspace is what you mean when not clearly inside a repo.
107
+ if (cli.flags.repo)
108
+ throw err;
109
+ if (err instanceof RepoSelectionError)
110
+ return { kind: "workspace" };
111
+ throw err;
112
+ }
113
+ }
114
+ function runConfig(cli, args) {
115
+ const [sub, ...rest] = args;
116
+ if (sub === "--help" || sub === "help" || args.includes("--help")) {
117
+ return renderHelp(cli, {
118
+ name: "config",
119
+ summary: "Read/write settings — one schema at two layers: workspace defaults, overridable per repo",
120
+ usage: ["config [list]", "config get <key>", "config set <key> <value>"],
121
+ options: [
122
+ ["list", "show the effective settings (default); at repo scope, overrides are marked"],
123
+ ["get <key>", "print one effective value (dotted path, e.g. embedding.provider)"],
124
+ ["set <key> <value>", "write a value at the selected layer (true/false/null/numbers are typed)"],
125
+ ["--repo <slug>", "target a repo's layer"],
126
+ ['--repo ""', "target the workspace layer (what every repo inherits)"],
127
+ ],
128
+ notes: [`e.g. ${cli.prog} config set embedding.provider omgbase-embedder --repo ""`],
129
+ });
130
+ }
131
+ const ws = cli.workspace();
132
+ const scope = resolveConfigScope(cli, ws);
133
+ const label = scope.kind === "workspace" ? "workspace" : scope.slug;
134
+ const readLayer = () => scope.kind === "workspace" ? workspaceSettings(ws.store) : repoOwnSettings(ws.store, scope.repoId);
135
+ const writeLayer = (s) => scope.kind === "workspace" ? writeWorkspaceSettings(ws.store, s) : writeRepoSettings(ws.store, scope.repoId, s);
136
+ if (sub === "list" || sub === undefined) {
137
+ // At repo scope show the EFFECTIVE (merged) view with an override marker;
138
+ // at workspace scope the layer is the effective view.
139
+ if (scope.kind === "workspace") {
140
+ const s = workspaceSettings(ws.store);
141
+ if (cli.flags.mode !== "human")
142
+ cli.io.out(JSON.stringify(s));
143
+ else {
144
+ cli.io.err(cli.style.dim(` workspace defaults`));
145
+ for (const [k, v] of Object.entries(s))
146
+ cli.io.out(` ${cli.style.accent(k)} ${cli.style.dim("=")} ${JSON.stringify(v)}`);
147
+ }
148
+ return EXIT_OK;
149
+ }
150
+ const effective = resolveSettings(ws.store, scope.repoId);
151
+ const own = repoOwnSettings(ws.store, scope.repoId);
152
+ if (cli.flags.mode !== "human") {
153
+ cli.io.out(JSON.stringify(effective));
154
+ return EXIT_OK;
155
+ }
156
+ cli.io.err(cli.style.dim(` ${label} (effective; ${cli.render.g.diamond} = overrides workspace default)`));
157
+ for (const [k, v] of Object.entries(effective)) {
158
+ const overridden = Object.prototype.hasOwnProperty.call(own, k);
159
+ const mark = overridden ? cli.style.accent(cli.render.g.diamond) : " ";
160
+ cli.io.out(` ${mark} ${cli.style.accent(k)} ${cli.style.dim("=")} ${JSON.stringify(v)}`);
161
+ }
162
+ return EXIT_OK;
163
+ }
164
+ if (sub === "get") {
165
+ const key = rest[0];
166
+ if (!key)
167
+ throw new CliUsageError("config get <key>");
168
+ // get returns the EFFECTIVE value (what actually takes effect for the scope).
169
+ const source = scope.kind === "workspace" ? workspaceSettings(ws.store) : resolveSettings(ws.store, scope.repoId);
170
+ const val = getPath(source, key);
171
+ cli.io.out(val === undefined ? "" : typeof val === "string" ? val : JSON.stringify(val));
172
+ return EXIT_OK;
173
+ }
174
+ if (sub === "set") {
175
+ const key = rest[0];
176
+ const raw = rest[1];
177
+ if (!key || raw === undefined)
178
+ throw new CliUsageError("config set <key> <value>");
179
+ const settings = readLayer();
180
+ setPath(settings, key, coerce(raw));
181
+ writeLayer(settings);
182
+ cli.io.err(cli.style.dim(` set ${key} (${label})`));
183
+ return EXIT_OK;
184
+ }
185
+ throw new CliUsageError(`unknown config subcommand '${sub}' (get|set|list)`, `run '${cli.prog} config --help'`);
186
+ }
187
+ // ---- embed ------------------------------------------------------------------
188
+ async function runEmbed(cli, args) {
189
+ const sub = args.find((a) => !a.startsWith("-"));
190
+ if (args.includes("--help") || args.includes("-h") || sub === "help") {
191
+ return renderHelp(cli, {
192
+ name: "embed",
193
+ summary: "The embedding queue: report the provider + what is queued, or drain it to embed now",
194
+ usage: ["embed [status]", "embed drain [--verbose] [--prune]"],
195
+ options: [
196
+ ["status", "(default) provider, model, and how many blocks/docs are embeddable but not yet embedded"],
197
+ ["drain", "embed the queued blocks now — status alone makes no progress"],
198
+ ["-v, --verbose", "(drain) per-batch progress"],
199
+ ["--prune", "(drain) afterwards delete vectors left by other models (e.g. after switching models)"],
200
+ ],
201
+ notes: [`needs embedding.provider (\`${cli.prog} config set embedding.provider omgbase-embedder --repo ""\`); without one, semantic search is simply off.`],
202
+ });
203
+ }
204
+ const verbose = args.includes("--verbose") || args.includes("-v");
205
+ const prune = args.includes("--prune");
206
+ const ws = cli.workspace();
207
+ const repo = cli.repo(ws);
208
+ const loaded = await loadEmbedding(ws, repo.repoId);
209
+ if (!loaded) {
210
+ // No provider configured (05 §6). Honest report; not an error.
211
+ const hint = 'set one with `omg config set embedding.provider <command|url>` (e.g. omgbase-embedder)';
212
+ if (cli.flags.mode !== "human")
213
+ cli.io.out(JSON.stringify({ provider: null, queued: 0 }));
214
+ else
215
+ cli.io.err(cli.style.dim(` no embedding provider configured — ${hint}`));
216
+ return EXIT_OK;
217
+ }
218
+ if (sub === "drain") {
219
+ await loaded.close(); // drainEmbeddings connects its own provider
220
+ const result = await drainEmbeddings(cli, ws, repo.repoId, { verbose, prune });
221
+ if (cli.flags.mode !== "human")
222
+ cli.io.out(JSON.stringify({ provider: loaded.providerName, ...(result ?? { embedded: 0, cached: 0 }) }));
223
+ else if (result)
224
+ cli.io.err(` ${cli.style.ok(cli.render.g.ok)} embedded ${result.embedded}, cached ${result.cached}`);
225
+ return EXIT_OK;
226
+ }
227
+ try {
228
+ const tasks = buildEmbedTasks(ws.store, repo.repoId);
229
+ // Queue depth = embeddable blocks whose current-context vector isn't cached.
230
+ const pending = loaded.worker.staleBlocks(tasks);
231
+ // Doc-grain queue depth = live docs whose current-content vector isn't cached.
232
+ const docTasks = buildDocEmbedTasks(ws.store, repo.repoId);
233
+ const docPending = loaded.worker.staleDocs(docTasks);
234
+ // Vectors left by a previous model (dead weight after a model switch).
235
+ const foreign = loaded.worker.foreignVectorCount();
236
+ // status
237
+ const payload = { provider: loaded.providerName, model: loaded.provider.model, dim: loaded.provider.dim, embeddable: tasks.length, queued: pending.length, docs: docTasks.length, docsQueued: docPending.length, foreignBlocks: foreign.blocks, foreignDocs: foreign.docs };
238
+ if (cli.flags.mode !== "human")
239
+ cli.io.out(JSON.stringify(payload));
240
+ else {
241
+ cli.io.out(` provider ${cli.style.accent(loaded.providerName)} ${cli.style.dim(`(${loaded.provider.model}, ${loaded.provider.dim}d)`)}`);
242
+ cli.io.out(` embeddable ${tasks.length} ${cli.style.dim("queued")} ${payload.queued}`);
243
+ cli.io.out(` docs ${docTasks.length} ${cli.style.dim("queued")} ${payload.docsQueued}`);
244
+ // `status` reports but never embeds — point the reader at the verb that does.
245
+ if (payload.queued > 0 || payload.docsQueued > 0)
246
+ cli.io.out(cli.style.dim(` run \`omg embed drain\` to embed the ${payload.queued} queued block(s) + ${payload.docsQueued} doc(s)`));
247
+ // Stale vectors from another model don't affect search (queries filter by
248
+ // model) but waste space — nudge toward reclaiming them.
249
+ if (foreign.blocks > 0 || foreign.docs > 0)
250
+ cli.io.out(cli.style.dim(` ${foreign.blocks} block + ${foreign.docs} doc vector(s) from other models — \`omg embed drain --prune\` to reclaim`));
251
+ }
252
+ return EXIT_OK;
253
+ }
254
+ finally {
255
+ await loaded.close();
256
+ }
257
+ }
258
+ // helpers ---------------------------------------------------------------------
259
+ function getPath(obj, path) {
260
+ return path.split(".").reduce((o, k) => (o && typeof o === "object" ? o[k] : undefined), obj);
261
+ }
262
+ function setPath(obj, path, value) {
263
+ const parts = path.split(".");
264
+ let cur = obj;
265
+ for (let i = 0; i < parts.length - 1; i++) {
266
+ const k = parts[i];
267
+ if (typeof cur[k] !== "object" || cur[k] === null)
268
+ cur[k] = {};
269
+ cur = cur[k];
270
+ }
271
+ cur[parts[parts.length - 1]] = value;
272
+ }
273
+ function coerce(raw) {
274
+ if (raw === "true")
275
+ return true;
276
+ if (raw === "false")
277
+ return false;
278
+ if (raw === "null")
279
+ return null;
280
+ if (/^-?\d+(\.\d+)?$/.test(raw))
281
+ return Number(raw);
282
+ return raw;
283
+ }
284
+ export const cmdRebuild = { name: "rebuild-index", summary: "Rebuild derived tables", run: (c, a) => runRebuild(c, a) };
285
+ export const cmdGc = { name: "gc", summary: "Mark-and-sweep (flag-gated)", run: (c, a) => runGcCmd(c, a) };
286
+ export const cmdDoctor = { name: "doctor", summary: "Invariant sweep (CI-able)", run: (c, a) => runDoctor(c, a) };
287
+ export const cmdConfig = { name: "config", summary: "Read/write repo settings", run: (c, a) => runConfig(c, a) };
288
+ export const cmdEmbed = { name: "embed", summary: "Embedding queue: status, or drain to embed", run: (c, a) => runEmbed(c, a) };
289
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1,3 @@
1
+ import type { Command } from "../commands.js";
2
+ export declare const cmdInit: Command;
3
+ export declare const cmdRepos: Command;
@@ -0,0 +1,252 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
2
+ import { join, resolve, dirname, relative, sep } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { createInterface } from "node:readline";
5
+ import { spawnSync } from "node:child_process";
6
+ import { Workspace, reposStatus, workspaceSettings, writeWorkspaceSettings } from "@omgbase/core";
7
+ import { columns as columnsLocal } from "../render.js";
8
+ import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
9
+ // Bootstrap: init / repos (11 §5.1). Pointing a repo at a directory is
10
+ // `omg source add <dir>` (ADR-014) — a repo owns identity; sources bring bytes.
11
+ async function confirmTTY(cli, question) {
12
+ if (!cli.io.stdoutTTY || !process.stdin.isTTY)
13
+ return false;
14
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
15
+ try {
16
+ const answer = await new Promise((res) => rl.question(`${question} [y/N] `, res));
17
+ return /^y(es)?$/i.test(answer.trim());
18
+ }
19
+ finally {
20
+ rl.close();
21
+ }
22
+ }
23
+ /** Nearest ancestor (inclusive) that is a git working tree, or null. */
24
+ function findGitRoot(start) {
25
+ let dir = resolve(start);
26
+ for (;;) {
27
+ if (existsSync(join(dir, ".git")))
28
+ return dir;
29
+ const parent = dirname(dir);
30
+ if (parent === dir)
31
+ return null;
32
+ dir = parent;
33
+ }
34
+ }
35
+ /**
36
+ * Keep the db out of git. The workspace `.omgbase/` only matters to git when it
37
+ * lives inside a git working tree, so we locate the enclosing git root (if any)
38
+ * and offer to ignore it via the *closest* existing `.gitignore` at or above
39
+ * the workspace (bounded by the git root); if none exists in that span, we
40
+ * create one at the git root. The ignore line is written relative to whichever
41
+ * `.gitignore` we touch, so the pattern resolves correctly regardless of depth.
42
+ * Outside a git repo there is nothing to ignore — we say so and skip.
43
+ */
44
+ function offerGitignore(cli, omgbaseParent, yes) {
45
+ const gitRoot = findGitRoot(omgbaseParent);
46
+ if (!gitRoot) {
47
+ cli.io.err(cli.style.dim(" not inside a git repo — no .gitignore needed for .omgbase/"));
48
+ return;
49
+ }
50
+ // Find the closest existing .gitignore from the workspace up to the git root;
51
+ // fall back to creating one at the git root.
52
+ const findGitignore = () => {
53
+ let dir = resolve(omgbaseParent);
54
+ for (;;) {
55
+ const gi = join(dir, ".gitignore");
56
+ if (existsSync(gi))
57
+ return { path: gi, exists: true };
58
+ if (dir === gitRoot)
59
+ return { path: join(gitRoot, ".gitignore"), exists: false };
60
+ const parent = dirname(dir);
61
+ if (parent === dir)
62
+ return { path: join(gitRoot, ".gitignore"), exists: false };
63
+ dir = parent;
64
+ }
65
+ };
66
+ const target = findGitignore();
67
+ // The pattern: `.omgbase/` at the workspace, expressed relative to the
68
+ // .gitignore that will hold it (so a repo-root .gitignore gets `sub/dir/.omgbase/`).
69
+ const rel = relative(dirname(target.path), join(omgbaseParent, ".omgbase")).split(sep).join("/");
70
+ const line = `${rel}/`;
71
+ const already = target.exists &&
72
+ readFileSync(target.path, "utf8")
73
+ .split(/\r?\n/)
74
+ .some((l) => l.trim() === line || l.trim() === rel);
75
+ if (already)
76
+ return;
77
+ const write = () => {
78
+ if (target.exists) {
79
+ const prefix = !readFileSync(target.path, "utf8").endsWith("\n") ? "\n" : "";
80
+ appendFileSync(target.path, `${prefix}${line}\n`);
81
+ }
82
+ else {
83
+ writeFileSync(target.path, `${line}\n`);
84
+ }
85
+ const shown = shortenHome(target.path);
86
+ cli.io.err(cli.style.dim(` ${target.exists ? "added" : "created"} ${line} in ${shown}`));
87
+ };
88
+ // never silent (11 §5.1): --yes writes, TTY prompts, non-TTY without --yes
89
+ // prints a notice and skips.
90
+ if (yes)
91
+ return write();
92
+ const verb = target.exists ? "append to" : "create";
93
+ return confirmTTY(cli, `${verb} ${shortenHome(target.path)} to ignore ${line}?`).then((ok) => {
94
+ if (ok)
95
+ write();
96
+ else
97
+ cli.io.err(cli.style.dim(` skipped .gitignore; add ${line} yourself to keep the db out of git`));
98
+ });
99
+ }
100
+ const EMBEDDER_CMD = "omgbase-embedder";
101
+ const EMBEDDER_INSTALL = "npm i -g @omgbase/embedder";
102
+ /** Is `cmd` runnable on PATH? Probes with a short-lived spawn (no shell). */
103
+ function commandExists(cmd) {
104
+ const probe = spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
105
+ return probe.status === 0;
106
+ }
107
+ function setProvider(cli, ws, provider) {
108
+ const current = workspaceSettings(ws.store);
109
+ const emb = current.embedding ?? {};
110
+ writeWorkspaceSettings(ws.store, { ...current, embedding: { ...emb, provider } });
111
+ cli.io.err(cli.style.dim(` embedding.provider = ${provider} (workspace default)`));
112
+ }
113
+ /**
114
+ * Configure the workspace's embedding provider (workspace layer, so every repo
115
+ * shares one vector space). Semantic search is off until a provider is set.
116
+ * Three paths:
117
+ * • `--embedder <value>` — set it verbatim, no prompt, no PATH check (the
118
+ * caller stated explicit intent; the value may be any command or URL).
119
+ * • otherwise, only when `omgbase-embedder` is actually installed do we offer
120
+ * it (—yes accepts, TTY prompts). We never wire a provider that isn't
121
+ * there, so `omg embed` can't be left pointing at a missing command.
122
+ * • not installed and no `--embedder` ⇒ we don't touch settings and return
123
+ * true so init can print install/config guidance at the end.
124
+ * Returns whether the caller should show the "no provider" guidance footer.
125
+ */
126
+ async function offerEmbedder(cli, ws, yes, embedder) {
127
+ const emb = workspaceSettings(ws.store).embedding;
128
+ if (emb?.provider)
129
+ return false; // already configured — leave it alone
130
+ if (embedder !== undefined) {
131
+ setProvider(cli, ws, embedder);
132
+ return false;
133
+ }
134
+ // Only offer the built-in embedder if it's genuinely runnable.
135
+ if (!commandExists(EMBEDDER_CMD))
136
+ return true;
137
+ const accept = yes || (await confirmTTY(cli, `set ${EMBEDDER_CMD} as the embedding provider (enables semantic search)?`));
138
+ if (!accept)
139
+ return true;
140
+ setProvider(cli, ws, EMBEDDER_CMD);
141
+ return false;
142
+ }
143
+ async function runInit(cli, args) {
144
+ const { values, positionals } = parseArgs({
145
+ args,
146
+ allowPositionals: true,
147
+ options: {
148
+ yes: { type: "boolean", short: "y" },
149
+ embedder: { type: "string" },
150
+ "no-embedder": { type: "boolean" },
151
+ help: { type: "boolean" },
152
+ },
153
+ });
154
+ if (values.help) {
155
+ return renderHelp(cli, {
156
+ name: "init",
157
+ summary: `Create an omgbase workspace (.omgbase/ + database) in <dir> (default cwd). Ingests nothing — run \`${cli.prog} source add .\` next`,
158
+ usage: "init [<dir>] [--yes] [--embedder <cmd|url> | --no-embedder]",
159
+ options: [
160
+ ["-y, --yes", "accept the prompts (.gitignore entry, embedder offer) non-interactively"],
161
+ ["--embedder <cmd|url>", "set the embedding provider verbatim (optional; enables semantic search)"],
162
+ ["--no-embedder", "skip the embedder offer entirely"],
163
+ ],
164
+ });
165
+ }
166
+ const dir = resolve(positionals[0] ?? cli.cwd);
167
+ mkdirSync(dir, { recursive: true });
168
+ const omgbaseDir = join(dir, ".omgbase");
169
+ if (existsSync(join(omgbaseDir, "omgbase.db"))) {
170
+ throw new EngineErrorLike("path_taken", `workspace already initialized at ${dir}`);
171
+ }
172
+ // Create the workspace + db only. Ingesting a working tree is a separate,
173
+ // consent-gated step (`omg source add`) so init never silently absorbs whatever
174
+ // happens to live under cwd (home dir, desktop, …).
175
+ const ws = Workspace.open(dir);
176
+ let needsProviderHint = false;
177
+ try {
178
+ await offerGitignore(cli, dir, Boolean(values.yes));
179
+ if (!values["no-embedder"]) {
180
+ needsProviderHint = await offerEmbedder(cli, ws, Boolean(values.yes), values.embedder);
181
+ }
182
+ }
183
+ finally {
184
+ ws.close();
185
+ }
186
+ if (cli.flags.mode !== "human") {
187
+ cli.io.out(JSON.stringify({ workspace: dir }));
188
+ return EXIT_OK;
189
+ }
190
+ const { render, style, io } = cli;
191
+ io.out(render.wordmark("initialized"));
192
+ io.out(render.rule(40));
193
+ io.out(` ${style.ok(render.g.ok)} workspace ${style.path(dir)}`);
194
+ io.err(style.dim(` next: ${style.accent("omg source add .")} to point a repo at a directory of files`));
195
+ if (needsProviderHint) {
196
+ // Optional, and say so: everything except semantic ranking works without a
197
+ // provider. Name what the embedder is (a local model that turns blocks into
198
+ // vectors) so the reader can decide whether they want it at all.
199
+ io.err(style.dim(` optional: semantic search is off — no embedding provider set. Full-text search, queries, and edits all work without one.`));
200
+ io.err(style.dim(` to enable it, install the built-in embedder (@omgbase/embedder: a local transformers.js model that turns blocks into vectors so ${style.accent(`${cli.prog} find`)} and ${style.accent('semantic("…")')} can rank by meaning), then point the workspace at it:`));
201
+ io.err(style.dim(` ${EMBEDDER_INSTALL} && ${cli.prog} config set embedding.provider ${EMBEDDER_CMD} --repo ""`));
202
+ }
203
+ return EXIT_OK;
204
+ }
205
+ function runRepos(cli, args) {
206
+ const { values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
207
+ if (values.help) {
208
+ return renderHelp(cli, {
209
+ name: "repos",
210
+ summary: "List the repos in this workspace: slug, root path (from its fs source), doc/block counts",
211
+ usage: "repos [--ids|--json|--jsonl]",
212
+ options: [["--ids", "slugs only, one per line"]],
213
+ });
214
+ }
215
+ const ws = cli.workspace();
216
+ const repos = ws.repos().map((r) => ({ ...r, status: reposStatus(ws.store, r.repoId) }));
217
+ if (cli.flags.mode === "json") {
218
+ cli.io.out(JSON.stringify(repos.map((r) => ({ slug: r.slug, root: r.rootPath, docs: r.status.docs, blocks: r.status.blocks }))));
219
+ return EXIT_OK;
220
+ }
221
+ if (cli.flags.mode === "jsonl") {
222
+ for (const r of repos)
223
+ cli.io.out(JSON.stringify({ slug: r.slug, root: r.rootPath, docs: r.status.docs, blocks: r.status.blocks }));
224
+ return EXIT_OK;
225
+ }
226
+ if (cli.flags.mode === "ids") {
227
+ for (const r of repos)
228
+ cli.io.out(r.slug);
229
+ return EXIT_OK;
230
+ }
231
+ const { render, style, io } = cli;
232
+ const rows = repos.map((r) => [
233
+ ` ${render.g.diamond} ${style.accent(r.slug)}`,
234
+ style.path(r.rootPath ? shortenHome(r.rootPath) : "(no source)"),
235
+ style.dim(`${r.status.docs} docs`),
236
+ style.dim(`${r.status.blocks} blocks`),
237
+ ]);
238
+ if (rows.length === 0)
239
+ io.out(style.dim(" no repos attached"));
240
+ else
241
+ for (const line of columnsLocal(rows))
242
+ io.out(line);
243
+ return EXIT_OK;
244
+ }
245
+ // small local helpers ---------------------------------------------------------
246
+ function shortenHome(p) {
247
+ const home = process.env.HOME;
248
+ return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
249
+ }
250
+ export const cmdInit = { name: "init", summary: "Create a workspace (run `source add` to ingest files)", run: (cli, a) => runInit(cli, a) };
251
+ export const cmdRepos = { name: "repos", summary: "List repos in this workspace", run: (cli, a) => runRepos(cli, a) };
252
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../commands.js";
2
+ export declare const cmdCat: Command;
@@ -0,0 +1,80 @@
1
+ import { parseArgs } from "node:util";
2
+ import { resolveRef, nodesGet, docsRead } from "@omgbase/core";
3
+ import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
+ import { remoteCall } from "./_remote.js";
5
+ import { expandBlockArgs } from "./_mutate.js";
6
+ async function catOne(cli, ref, resolution) {
7
+ // Remote: the polymorphic `read_ref` tool classifies + reads server-side.
8
+ if (cli.flags.server) {
9
+ const r = await remoteCall(cli, "read_ref", { ref, ...(resolution !== "raw" ? { resolution } : {}) });
10
+ if (r.kind === "document") {
11
+ return { text: String(r.content ?? ""), json: { doc: r.docId, path: r.path, content: r.content }, capture: r.content };
12
+ }
13
+ return { text: String(r.raw ?? r.text ?? r.label ?? ""), json: r, capture: r };
14
+ }
15
+ const ws = cli.workspace();
16
+ const repo = cli.repo(ws);
17
+ const resolved = resolveRef(ws.store, repo.repoId, ref);
18
+ if (!resolved)
19
+ throw new EngineErrorLike("doc_missing", `no node ${ref}`);
20
+ if (resolved.kind === "document") {
21
+ // Cat the whole document byte-for-byte via the faithful reconstructor
22
+ // (leading trivia + frontmatter + each top-level block's raw + its trivia).
23
+ // This is the same content docs_read returns and what ingest's convergence
24
+ // check asserts against — never a hand-rolled block join (which dropped
25
+ // trivia and mangled spacing).
26
+ const res = docsRead(ws.store, resolved.docId);
27
+ if (!res)
28
+ throw new EngineErrorLike("doc_missing", `no document ${ref}`);
29
+ // shell: the bytes (a string; @_ only)
30
+ return { text: res.content, json: { doc: res.docId, path: res.path, content: res.content }, capture: res.content };
31
+ }
32
+ const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution });
33
+ if (!node)
34
+ throw new EngineErrorLike("block_missing", `no block ${ref}`);
35
+ // shell: the block (single entity)
36
+ return { text: node.raw ?? node.text ?? node.label ?? "", json: node, capture: node };
37
+ }
38
+ async function runCat(cli, args) {
39
+ const { values, positionals } = parseArgs({
40
+ args,
41
+ allowPositionals: true,
42
+ options: { resolution: { type: "string" }, help: { type: "boolean" } },
43
+ });
44
+ if (values.help) {
45
+ return renderHelp(cli, {
46
+ name: "cat",
47
+ summary: "Content bytes of a node — exact raw bytes by default, pipe-clean (`show` is the metadata card)",
48
+ usage: "cat <node…|-> [--resolution raw|text|outline|skeleton|full]",
49
+ options: [
50
+ ["<node…>", "one or more refs: a doc/block id, a path, or a locator (`notes/x.md#Risks/p[2]`)"],
51
+ ["-", `read refs from stdin, one per line (\`${cli.prog} q … --ids | ${cli.prog} cat -\`)`],
52
+ ["--resolution <r>", "raw (default: exact bytes) | text | outline | skeleton | full"],
53
+ ],
54
+ });
55
+ }
56
+ const refs = expandBlockArgs(positionals);
57
+ if (refs.length === 0)
58
+ throw new CliUsageError("cat requires a <node> (or - to read refs from stdin)");
59
+ const resolution = (values.resolution ?? "raw");
60
+ const results = [];
61
+ for (const ref of refs)
62
+ results.push(await catOne(cli, ref, resolution));
63
+ // One ref keeps the single-entity shape (a string/object); several become a list.
64
+ const single = results.length === 1 ? results[0] : null;
65
+ cli.capture?.(single ? single.capture : results.map((r) => r.capture));
66
+ if (cli.flags.mode === "json") {
67
+ cli.io.out(JSON.stringify(single ? single.json : results.map((r) => r.json)));
68
+ return EXIT_OK;
69
+ }
70
+ if (cli.flags.mode === "jsonl") {
71
+ for (const r of results)
72
+ cli.io.out(JSON.stringify(r.json));
73
+ return EXIT_OK;
74
+ }
75
+ for (const r of results)
76
+ cli.io.out(r.text);
77
+ return EXIT_OK;
78
+ }
79
+ export const cmdCat = { name: "cat", summary: "Content bytes of a node (default raw)", run: (cli, a) => runCat(cli, a) };
80
+ //# sourceMappingURL=cat.js.map
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../commands.js";
2
+ export declare const cmdDiff: Command;