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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brendan Baldwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ import { EmbeddingWorker, EngineError, type EmbeddingProvider } from "@omgbase/core";
2
+ import type { Cli } from "../context.js";
3
+ export interface LoadedEmbedding {
4
+ worker: EmbeddingWorker;
5
+ provider: EmbeddingProvider;
6
+ /** the command/URL that was connected, for diagnostics */
7
+ providerName: string;
8
+ /** whether the provider is a remote endpoint (text leaves the machine) */
9
+ remote: boolean;
10
+ /** release resources (kill the spawned process). */
11
+ close(): Promise<void>;
12
+ }
13
+ /**
14
+ * Connect the configured embedding provider + build a worker for a repo, or
15
+ * null when embedding.provider is UNSET (callers surface semantic_unavailable).
16
+ * When a provider IS configured but can't be reached (command won't spawn,
17
+ * endpoint unreachable, bad handshake) this throws a loud, typed `embedder_failed`
18
+ * EngineError naming the provider + reason — never a silent null, so a broken
19
+ * embedder is never mistaken for an unconfigured one. Callers MUST call close()
20
+ * when done (e.g. in a finally) to release a spawned process.
21
+ */
22
+ export declare function loadEmbedding(ws: ReturnType<Cli["workspace"]>, repoId: string): Promise<LoadedEmbedding | null>;
23
+ /** Build the shared `embedder_failed` error: a configured embedder that can't be
24
+ * reached is a hard, clearly-diagnosed fault (not `semantic_unavailable`, which
25
+ * means "no provider configured"). */
26
+ export declare function embedderFailed(provider: string, err: unknown): EngineError;
27
+ export interface DrainDecision {
28
+ /** number of blocks that would be embedded (cache misses). */
29
+ pending: number;
30
+ /** true if the provider is a remote endpoint (block text leaves the machine). */
31
+ remote: boolean;
32
+ providerName: string;
33
+ }
34
+ /**
35
+ * Embed the repo's queued blocks now, connecting the configured provider and
36
+ * releasing it afterward. No-op (returns null) when no provider is configured.
37
+ * `verbose` prints per-batch progress in human mode. An optional `confirm` gate
38
+ * is consulted BEFORE any block text is handed to the provider — returning
39
+ * false skips the drain (also null). Callers that are themselves the explicit
40
+ * "drain now" intent (`omg embed drain`) omit `confirm`; callers doing it as a
41
+ * side effect (post-`attach`) pass one so the user consents to the egress.
42
+ * Shared by both so they speak the same egress notice + progress output.
43
+ */
44
+ export declare function drainEmbeddings(cli: Cli, ws: ReturnType<Cli["workspace"]>, repoId: string, opts?: {
45
+ verbose?: boolean;
46
+ prune?: boolean;
47
+ confirm?: (d: DrainDecision) => Promise<boolean>;
48
+ }): Promise<{
49
+ embedded: number;
50
+ cached: number;
51
+ pruned?: {
52
+ blocks: number;
53
+ docs: number;
54
+ };
55
+ } | null>;
@@ -0,0 +1,102 @@
1
+ import { EmbeddingWorker, buildEmbedTasks, buildDocEmbedTasks, embeddingSettings, createExternalProvider, resolveSettings, EngineError } from "@omgbase/core";
2
+ /**
3
+ * Connect the configured embedding provider + build a worker for a repo, or
4
+ * null when embedding.provider is UNSET (callers surface semantic_unavailable).
5
+ * When a provider IS configured but can't be reached (command won't spawn,
6
+ * endpoint unreachable, bad handshake) this throws a loud, typed `embedder_failed`
7
+ * EngineError naming the provider + reason — never a silent null, so a broken
8
+ * embedder is never mistaken for an unconfigured one. Callers MUST call close()
9
+ * when done (e.g. in a finally) to release a spawned process.
10
+ */
11
+ export async function loadEmbedding(ws, repoId) {
12
+ const settings = embeddingSettings(resolveSettings(ws.store, repoId));
13
+ if (!settings.provider)
14
+ return null;
15
+ let ext;
16
+ try {
17
+ ext = await createExternalProvider(settings);
18
+ }
19
+ catch (err) {
20
+ throw embedderFailed(settings.provider, err);
21
+ }
22
+ if (!ext)
23
+ return null;
24
+ return {
25
+ worker: new EmbeddingWorker(ws.store, ext.provider),
26
+ provider: ext.provider,
27
+ providerName: settings.provider,
28
+ remote: /^https?:\/\//i.test(settings.provider),
29
+ close: ext.close,
30
+ };
31
+ }
32
+ /** Build the shared `embedder_failed` error: a configured embedder that can't be
33
+ * reached is a hard, clearly-diagnosed fault (not `semantic_unavailable`, which
34
+ * means "no provider configured"). */
35
+ export function embedderFailed(provider, err) {
36
+ const reason = err instanceof Error ? err.message : String(err);
37
+ return new EngineError("embedder_failed", `embedder '${provider}' is configured but failed to start: ${reason}. Fix: put it on PATH or use an absolute path / http(s) URL for embedding.provider — or unset embedding.provider to run without semantic search.`, { data: { provider, reason } });
38
+ }
39
+ /**
40
+ * Embed the repo's queued blocks now, connecting the configured provider and
41
+ * releasing it afterward. No-op (returns null) when no provider is configured.
42
+ * `verbose` prints per-batch progress in human mode. An optional `confirm` gate
43
+ * is consulted BEFORE any block text is handed to the provider — returning
44
+ * false skips the drain (also null). Callers that are themselves the explicit
45
+ * "drain now" intent (`omg embed drain`) omit `confirm`; callers doing it as a
46
+ * side effect (post-`attach`) pass one so the user consents to the egress.
47
+ * Shared by both so they speak the same egress notice + progress output.
48
+ */
49
+ export async function drainEmbeddings(cli, ws, repoId, opts = {}) {
50
+ const loaded = await loadEmbedding(ws, repoId);
51
+ if (!loaded)
52
+ return null;
53
+ try {
54
+ const tasks = buildEmbedTasks(ws.store, repoId);
55
+ const pending = loaded.worker.staleBlocks(tasks);
56
+ if (opts.confirm) {
57
+ const ok = await opts.confirm({ pending: pending.length, remote: loaded.remote, providerName: loaded.providerName });
58
+ if (!ok)
59
+ return null;
60
+ }
61
+ // Egress notice (05 §6): a remote provider receives block text off-machine.
62
+ if (loaded.remote) {
63
+ cli.io.err(cli.style.warn(` embedding ${pending.length} block(s) via ${loaded.providerName} — block text is sent to this remote endpoint`));
64
+ }
65
+ else {
66
+ cli.io.err(cli.style.dim(` embedding ${pending.length} block(s) via ${loaded.providerName} (local process)`));
67
+ }
68
+ const verboseHuman = Boolean(opts.verbose) && cli.flags.mode === "human";
69
+ const result = await loaded.worker.process(tasks, {
70
+ ...(verboseHuman
71
+ ? { onProgress: ({ embedded, total }) => cli.io.err(cli.style.dim(` … ${embedded}/${total} embedded`)) }
72
+ : {}),
73
+ });
74
+ // Doc-grain vectors: embed whole documents (or pool block vectors for
75
+ // oversized docs) after blocks, so the pooled fallback can reuse the
76
+ // vectors just cached. This is also the one-time backfill path for existing
77
+ // repos — a fresh doc_embeddings table fills on the first drain.
78
+ const docTasks = buildDocEmbedTasks(ws.store, repoId);
79
+ const docResult = await loaded.worker.processDocs(docTasks, {
80
+ ...(verboseHuman
81
+ ? { onProgress: ({ embedded, total }) => cli.io.err(cli.style.dim(` … ${embedded}/${total} doc(s) embedded`)) }
82
+ : {}),
83
+ });
84
+ // Prune AFTER the drain, so the current model's vectors are in place before
85
+ // we reclaim the old model's — a crash mid-way leaves the new vectors, not a
86
+ // half-empty cache. Only touches rows for models other than the current one.
87
+ let pruned;
88
+ if (opts.prune) {
89
+ pruned = loaded.worker.pruneForeignVectors();
90
+ if (cli.flags.mode === "human" && (pruned.blocks || pruned.docs)) {
91
+ cli.io.err(cli.style.dim(` pruned ${pruned.blocks} block + ${pruned.docs} doc vector(s) from other models`));
92
+ }
93
+ }
94
+ // Fold doc work into the block counts so callers' "embedded/cached" report
95
+ // covers both grains (pooled docs count as embedded work done).
96
+ return { embedded: result.embedded + docResult.embedded + docResult.pooled, cached: result.cached + docResult.cached, ...(pruned ? { pruned } : {}) };
97
+ }
98
+ finally {
99
+ await loaded.close();
100
+ }
101
+ }
102
+ //# sourceMappingURL=_embed.js.map
@@ -0,0 +1,47 @@
1
+ import { type Op } from "@omgbase/core";
2
+ import type { Cli } from "../context.js";
3
+ import type { RepoRow } from "@omgbase/core";
4
+ import type { Workspace } from "@omgbase/core";
5
+ import { EXIT_OK, EXIT_ERROR } from "../output.js";
6
+ /** Read markdown content from -m, -f (file, or "-" = stdin), or bare stdin. */
7
+ export declare function readContent(values: {
8
+ m?: string;
9
+ f?: string;
10
+ }, allowStdin?: boolean): string;
11
+ /**
12
+ * Pull content-bearing options out of argv before `parseArgs` sees them, so a
13
+ * value that starts with `-` (list/task markdown like "- [ ] x") is taken
14
+ * verbatim as the next token rather than mis-parsed as a flag. Handles
15
+ * -m/--message and -f/--file (and their `=` forms). Returns the extracted
16
+ * values plus the remaining argv for normal option parsing.
17
+ */
18
+ export declare function extractContentOpts(args: string[]): {
19
+ m?: string;
20
+ f?: string;
21
+ rest: string[];
22
+ };
23
+ export declare function readStdin(): string;
24
+ /** Read a list of ids from stdin (one per line), for `-` block-list args. */
25
+ export declare function readIdsFromStdin(): string[];
26
+ /** Expand a block-list argument list, substituting stdin ids for a bare "-". */
27
+ export declare function expandBlockArgs(args: string[]): string[];
28
+ export interface RunOpsOptions {
29
+ reason?: string;
30
+ actor?: string;
31
+ }
32
+ /**
33
+ * Apply a set of kernel ops as one changeset and render the outcome. Honors
34
+ * cli.flags.dryRun (per-file unified diffs to stdout, nothing committed) and
35
+ * cli.flags.mode (--json emits ApplyResult verbatim). Typed conflicts are
36
+ * rendered by the caller's catch via the output contract; we let apply throw.
37
+ */
38
+ export declare function runOps(cli: Cli, ws: Workspace, repo: RepoRow, ops: Op[], opts?: RunOpsOptions): number;
39
+ /**
40
+ * Remote peer of runOps: call the changeset-producing tool that mirrors this
41
+ * command (blocks_insert, tasks_complete, …) against the `--server` engine — the
42
+ * server resolves refs + pins CAS + applies — then render the returned
43
+ * ApplyResult with the SAME renderers as local. --dry-run is threaded as the
44
+ * tool's dry_run so the preview comes back without committing.
45
+ */
46
+ export declare function runOpsRemote(cli: Cli, tool: string, toolArgs: Record<string, unknown>): Promise<number>;
47
+ export { EXIT_OK, EXIT_ERROR };
@@ -0,0 +1,211 @@
1
+ import { readFileSync, readSync } from "node:fs";
2
+ import { userInfo } from "node:os";
3
+ import { apply } from "@omgbase/core";
4
+ import { CliUsageError, EXIT_OK, EXIT_ERROR } from "../output.js";
5
+ import { remoteCall } from "./_remote.js";
6
+ // Shared plumbing for every write command (11 §5.6, §5.7). Sugar commands build
7
+ // their kernel Ops, then hand them here: this sets origin.actor (human:$USER by
8
+ // default, override --actor), runs under the writer flock (omgbaseDir), threads
9
+ // --dry-run (validate + render diffs, commit nothing), and renders results or
10
+ // typed conflicts uniformly.
11
+ /** Read markdown content from -m, -f (file, or "-" = stdin), or bare stdin. */
12
+ export function readContent(values, allowStdin = true) {
13
+ if (values.m !== undefined)
14
+ return values.m;
15
+ if (values.f !== undefined)
16
+ return values.f === "-" ? readStdin() : readFileSync(values.f, "utf8");
17
+ if (allowStdin)
18
+ return readStdin();
19
+ throw new CliUsageError("provide content with -m <md> or -f <file>");
20
+ }
21
+ /**
22
+ * Pull content-bearing options out of argv before `parseArgs` sees them, so a
23
+ * value that starts with `-` (list/task markdown like "- [ ] x") is taken
24
+ * verbatim as the next token rather than mis-parsed as a flag. Handles
25
+ * -m/--message and -f/--file (and their `=` forms). Returns the extracted
26
+ * values plus the remaining argv for normal option parsing.
27
+ */
28
+ export function extractContentOpts(args) {
29
+ const rest = [];
30
+ let m;
31
+ let f;
32
+ for (let i = 0; i < args.length; i++) {
33
+ const a = args[i];
34
+ if (a === "--") {
35
+ rest.push(...args.slice(i));
36
+ break;
37
+ }
38
+ if (a === "-m" || a === "--message") {
39
+ m = args[++i];
40
+ if (m === undefined)
41
+ throw new CliUsageError(`${a} requires a value`);
42
+ }
43
+ else if (a.startsWith("-m=")) {
44
+ m = a.slice(3);
45
+ }
46
+ else if (a.startsWith("--message=")) {
47
+ m = a.slice("--message=".length);
48
+ }
49
+ else if (a === "-f" || a === "--file") {
50
+ f = args[++i];
51
+ if (f === undefined)
52
+ throw new CliUsageError(`${a} requires a value`);
53
+ }
54
+ else if (a.startsWith("-f=")) {
55
+ f = a.slice(3);
56
+ }
57
+ else if (a.startsWith("--file=")) {
58
+ f = a.slice("--file=".length);
59
+ }
60
+ else {
61
+ rest.push(a);
62
+ }
63
+ }
64
+ return { ...(m !== undefined ? { m } : {}), ...(f !== undefined ? { f } : {}), rest };
65
+ }
66
+ export function readStdin() {
67
+ // Read fd 0 to EOF synchronously. `readFileSync(0)` is not enough: when stdin
68
+ // is a pipe whose writer hasn't produced data yet (the common `… | omg done -`
69
+ // case) the fd is non-blocking and the read throws EAGAIN — swallowing that as
70
+ // "" is why the piped idiom used to lose its input. Loop with an EAGAIN retry
71
+ // so we actually block for the upstream command's output.
72
+ const chunks = [];
73
+ const buf = Buffer.alloc(65536);
74
+ for (;;) {
75
+ let n;
76
+ try {
77
+ n = readSync(0, buf, 0, buf.length, null);
78
+ }
79
+ catch (err) {
80
+ const code = err.code;
81
+ if (code === "EAGAIN")
82
+ continue; // pipe not ready yet — retry
83
+ if (code === "EOF")
84
+ break; // some platforms signal EOF this way
85
+ break; // fd not readable (no stdin) — treat as empty
86
+ }
87
+ if (n === 0)
88
+ break; // clean EOF
89
+ chunks.push(Buffer.from(buf.subarray(0, n)));
90
+ }
91
+ return Buffer.concat(chunks).toString("utf8");
92
+ }
93
+ /** Read a list of ids from stdin (one per line), for `-` block-list args. */
94
+ export function readIdsFromStdin() {
95
+ return readStdin()
96
+ .split(/\r?\n/)
97
+ .map((s) => s.trim())
98
+ .filter(Boolean);
99
+ }
100
+ /** Expand a block-list argument list, substituting stdin ids for a bare "-". */
101
+ export function expandBlockArgs(args) {
102
+ const out = [];
103
+ for (const a of args) {
104
+ if (a === "-")
105
+ out.push(...readIdsFromStdin());
106
+ else
107
+ out.push(a);
108
+ }
109
+ return out;
110
+ }
111
+ function defaultActor() {
112
+ try {
113
+ return `human:${userInfo().username}`;
114
+ }
115
+ catch {
116
+ return "human:unknown";
117
+ }
118
+ }
119
+ /**
120
+ * Apply a set of kernel ops as one changeset and render the outcome. Honors
121
+ * cli.flags.dryRun (per-file unified diffs to stdout, nothing committed) and
122
+ * cli.flags.mode (--json emits ApplyResult verbatim). Typed conflicts are
123
+ * rendered by the caller's catch via the output contract; we let apply throw.
124
+ */
125
+ export function runOps(cli, ws, repo, ops, opts = {}) {
126
+ if (ops.length === 0) {
127
+ cli.io.err(cli.style.dim(" nothing to do"));
128
+ return EXIT_OK;
129
+ }
130
+ const req = {
131
+ repoId: repo.repoId,
132
+ ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
133
+ ops,
134
+ origin: { actor: opts.actor ?? defaultActor(), ...(opts.reason ? { reason: opts.reason } : {}) },
135
+ omgbaseDir: ws.omgbaseDir,
136
+ ...(cli.flags.dryRun ? { dryRun: true } : {}),
137
+ };
138
+ const result = apply(ws.store, req);
139
+ return renderApply(cli, result);
140
+ }
141
+ /**
142
+ * Remote peer of runOps: call the changeset-producing tool that mirrors this
143
+ * command (blocks_insert, tasks_complete, …) against the `--server` engine — the
144
+ * server resolves refs + pins CAS + applies — then render the returned
145
+ * ApplyResult with the SAME renderers as local. --dry-run is threaded as the
146
+ * tool's dry_run so the preview comes back without committing.
147
+ */
148
+ export async function runOpsRemote(cli, tool, toolArgs) {
149
+ const result = await remoteCall(cli, tool, {
150
+ ...toolArgs,
151
+ ...(cli.flags.dryRun ? { dry_run: true } : {}),
152
+ });
153
+ return renderApply(cli, result);
154
+ }
155
+ /** Capture + render an ApplyResult (shared by the local and remote paths). */
156
+ function renderApply(cli, result) {
157
+ cli.capture?.(result); // shell: ApplyResult — its minted/affected ids are the frame
158
+ if (cli.flags.mode !== "human") {
159
+ cli.io.out(JSON.stringify(result));
160
+ return EXIT_OK;
161
+ }
162
+ return cli.flags.dryRun ? renderDryRun(cli, result) : renderCommitted(cli, result);
163
+ }
164
+ function renderDryRun(cli, result) {
165
+ const { render, style, io } = cli;
166
+ io.err(style.dim(` dry run — ${Object.keys(result.diffs ?? {}).length} file(s) would change, nothing committed`));
167
+ for (const [path, { before, after }] of Object.entries(result.diffs ?? {})) {
168
+ io.out(`${style.bold(path)}`);
169
+ for (const line of unifiedDiff(before, after)) {
170
+ if (line.startsWith("+"))
171
+ io.out(style.ok(line));
172
+ else if (line.startsWith("-"))
173
+ io.out(style.err(line));
174
+ else
175
+ io.out(style.dim(line));
176
+ }
177
+ io.out("");
178
+ }
179
+ void render;
180
+ return EXIT_OK;
181
+ }
182
+ function renderCommitted(cli, result) {
183
+ const { render, style, io } = cli;
184
+ const g = render.g;
185
+ const n = result.revisions.length;
186
+ io.err(style.dim(` ${style.ok(g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
187
+ // stdout stays useful for pipes: the ids minted/affected, one per line.
188
+ const ids = result.results.flatMap((r) => r.ids);
189
+ for (const id of ids)
190
+ io.out(id);
191
+ return EXIT_OK;
192
+ }
193
+ // Minimal line-based unified diff (matches the engine's diffUnified style; a
194
+ // full Myers isn't needed for a human preview).
195
+ function unifiedDiff(before, after) {
196
+ const a = before.split("\n");
197
+ const b = after.split("\n");
198
+ const out = [];
199
+ const max = Math.max(a.length, b.length);
200
+ for (let i = 0; i < max; i++) {
201
+ if (a[i] === b[i])
202
+ continue;
203
+ if (a[i] !== undefined)
204
+ out.push(`- ${a[i]}`);
205
+ if (b[i] !== undefined)
206
+ out.push(`+ ${b[i]}`);
207
+ }
208
+ return out;
209
+ }
210
+ export { EXIT_OK, EXIT_ERROR };
211
+ //# sourceMappingURL=_mutate.js.map
@@ -0,0 +1,8 @@
1
+ import type { Cli } from "../context.js";
2
+ /** Call an MCP tool on the remote engine, threading the global `--repo` slug.
3
+ * Returns the tool's parsed JSON result (same shape the local core fn returns,
4
+ * so the command's existing renderer works unchanged). */
5
+ export declare function remoteCall<T>(cli: Cli, tool: string, args: Record<string, unknown>): Promise<T>;
6
+ /** Close the remote connection if one was opened (idempotent). Called after a
7
+ * one-shot command and on shell exit. */
8
+ export declare function closeRemote(): Promise<void>;
@@ -0,0 +1,61 @@
1
+ import { connectStdioEngine, connectHttpEngine } from "@omgbase/sync";
2
+ import { CliUsageError } from "../output.js";
3
+ // Remote (`--server`) mode plumbing (ADR-014). When a command runs with the
4
+ // global `--server <cmd|url>` flag, it reaches a remote engine over MCP instead
5
+ // of the embedded local store: it calls the MCP tool that mirrors its local
6
+ // core function and renders the (identically-shaped) result. One connection per
7
+ // process, opened lazily on first use and reused across a `shell` session;
8
+ // closed via `closeRemote` at the end of a one-shot command or on shell exit.
9
+ //
10
+ // The flag is an http(s) URL to reach a remote server over Streamable HTTP, or
11
+ // otherwise a command to spawn and talk to over stdio.
12
+ let engine = null;
13
+ /** The process's remote engine, connecting (spawning the command or opening the
14
+ * HTTP transport) on first use. `--repo` is threaded into each tool call by
15
+ * `remoteCall`. */
16
+ async function remoteEngine(cli) {
17
+ if (engine)
18
+ return engine;
19
+ const spec = (cli.flags.server ?? "").trim();
20
+ if (/^https?:\/\//i.test(spec)) {
21
+ const headers = parseHeaders(cli.flags.headers);
22
+ engine = await connectHttpEngine({ url: spec, ...(Object.keys(headers).length > 0 ? { headers } : {}) });
23
+ return engine;
24
+ }
25
+ if (cli.flags.headers?.length)
26
+ throw new CliUsageError("-H/--header only applies to an http(s) --server url");
27
+ const argv = spec.split(/\s+/).filter(Boolean);
28
+ if (argv.length === 0)
29
+ throw new CliUsageError("--server requires a command to spawn (e.g. --server \"omg mcp -C /vault\") or an http(s) URL");
30
+ engine = await connectStdioEngine({ command: argv[0], args: argv.slice(1) });
31
+ return engine;
32
+ }
33
+ /** Parse repeatable `-H "Name: value"` flags into a header map. Splits on the
34
+ * first colon; later values win for a repeated name. */
35
+ function parseHeaders(raw) {
36
+ const headers = {};
37
+ for (const h of raw ?? []) {
38
+ const i = h.indexOf(":");
39
+ if (i < 1)
40
+ throw new CliUsageError(`invalid header ${JSON.stringify(h)} — expected "Name: value"`);
41
+ headers[h.slice(0, i).trim()] = h.slice(i + 1).trim();
42
+ }
43
+ return headers;
44
+ }
45
+ /** Call an MCP tool on the remote engine, threading the global `--repo` slug.
46
+ * Returns the tool's parsed JSON result (same shape the local core fn returns,
47
+ * so the command's existing renderer works unchanged). */
48
+ export async function remoteCall(cli, tool, args) {
49
+ const eng = await remoteEngine(cli);
50
+ return eng.callTool(tool, { ...args, ...(cli.flags.repo ? { repo: cli.flags.repo } : {}) });
51
+ }
52
+ /** Close the remote connection if one was opened (idempotent). Called after a
53
+ * one-shot command and on shell exit. */
54
+ export async function closeRemote() {
55
+ if (!engine)
56
+ return;
57
+ const e = engine;
58
+ engine = null;
59
+ await e.close();
60
+ }
61
+ //# sourceMappingURL=_remote.js.map
@@ -0,0 +1,13 @@
1
+ import { type SyncSource, type Store, type RepoRow } from "@omgbase/core";
2
+ /** The built-in filesystem adapter's registry name. */
3
+ export declare const FS_ADAPTER = "fs";
4
+ /** Seed the built-in `fs` adapter row (idempotent). Required before creating an
5
+ * fs source (the `sources.adapter` FK is enforced). The stored command is the
6
+ * globally-installed bin name; the actual spawn uses the bundled binary via
7
+ * execPath (see spawnSource) so a source is never pinned to a stale path. */
8
+ export declare function ensureFsAdapter(store: Store): void;
9
+ /**
10
+ * Build the live filesystem SyncSource for a repo from its attached `fs` source
11
+ * (ADR-014), or null if it has none (a sourceless/headless repo).
12
+ */
13
+ export declare function openRepoSource(store: Store, repo: RepoRow): Promise<SyncSource | null>;
@@ -0,0 +1,47 @@
1
+ import { execPath } from "node:process";
2
+ import { createExternalSource, ensureAdapter, sourcesForRepo, renderConfigFlags, } from "@omgbase/core";
3
+ import { fsAdapterBinPath } from "@omgbase/fs-adapter";
4
+ // Resolve a repo's live sync source for `omg watch`/`omg mcp`/`omg sync`
5
+ // (sync-plugins, ADR-014 Stage 3). Resolution now prefers the source REGISTRY
6
+ // (adapters/sources/attachments tables): a repo's attached filesystem source is
7
+ // spawned from its `sources.config`. When a repo has no registered source we
8
+ // fall back to synthesizing the fs source from its `root_path` — the pre-registry
9
+ // behavior — so existing single-fs repos keep working unchanged until `attach`
10
+ // starts populating the registry (Stage 5) and `root_path` is retired (Stage 6).
11
+ /** The built-in filesystem adapter's registry name. */
12
+ export const FS_ADAPTER = "fs";
13
+ /** Seed the built-in `fs` adapter row (idempotent). Required before creating an
14
+ * fs source (the `sources.adapter` FK is enforced). The stored command is the
15
+ * globally-installed bin name; the actual spawn uses the bundled binary via
16
+ * execPath (see spawnSource) so a source is never pinned to a stale path. */
17
+ export function ensureFsAdapter(store) {
18
+ ensureAdapter(store, FS_ADAPTER, "omgbase-fs-adapter", []);
19
+ }
20
+ /** Spawn one registered source as a live SyncSource. The built-in `fs` adapter
21
+ * is served by the bundled `@omgbase/fs-adapter` (execPath + bin path); other
22
+ * adapters spawn their stored command. Source config renders to argv flags. */
23
+ function spawnSource(source) {
24
+ const flags = renderConfigFlags(source.config);
25
+ if (source.adapter === FS_ADAPTER) {
26
+ return createExternalSource({ command: execPath, args: [fsAdapterBinPath(), ...flags], ...(hasEnv(source) ? { env: source.env } : {}) });
27
+ }
28
+ // Custom adapter: spawn its stored command (looked up by the caller).
29
+ throw new Error(`no launcher for adapter '${source.adapter}' (only the built-in '${FS_ADAPTER}' is wired)`);
30
+ }
31
+ function hasEnv(source) {
32
+ return Object.keys(source.env).length > 0;
33
+ }
34
+ /**
35
+ * Build the live filesystem SyncSource for a repo from its attached `fs` source
36
+ * (ADR-014), or null if it has none (a sourceless/headless repo).
37
+ */
38
+ export async function openRepoSource(store, repo) {
39
+ const fsSource = sourcesForRepo(store, repo.repoId).find((s) => s.adapter === FS_ADAPTER);
40
+ if (!fsSource)
41
+ return null;
42
+ const root = fsSource.config.root;
43
+ if (typeof root !== "string" || root === "")
44
+ return null;
45
+ return spawnSource(fsSource);
46
+ }
47
+ //# sourceMappingURL=_source.js.map
@@ -0,0 +1,6 @@
1
+ import type { Command } from "../commands.js";
2
+ export declare const cmdRebuild: Command;
3
+ export declare const cmdGc: Command;
4
+ export declare const cmdDoctor: Command;
5
+ export declare const cmdConfig: Command;
6
+ export declare const cmdEmbed: Command;