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
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
|