@grosspoetrysystems/oompf 0.2.1 → 0.3.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/README.md +49 -11
- package/dist/index.d.mts +56 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +737 -127
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -2,16 +2,152 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Cli, z } from "incur";
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
7
|
import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
7
8
|
import { env } from "node:process";
|
|
8
|
-
import {
|
|
9
|
+
import { confirm, isCancel, select } from "@clack/prompts";
|
|
9
10
|
//#region \0rolldown/runtime.js
|
|
10
11
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
11
12
|
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
12
13
|
//#endregion
|
|
13
14
|
//#region package.json
|
|
14
|
-
var version = "0.
|
|
15
|
+
var version = "0.3.0";
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region ../../packages/core/src/spawn.ts
|
|
18
|
+
/**
|
|
19
|
+
* The workspace's single process-spawning primitive.
|
|
20
|
+
*
|
|
21
|
+
* Backed by `node:child_process` rather than `Bun.spawn`. Bun implements the
|
|
22
|
+
* Node API, so one implementation serves both runtimes — whereas reaching for
|
|
23
|
+
* the `Bun` global makes the published CLI throw
|
|
24
|
+
* `Cannot read properties of undefined (reading 'spawn')` the moment it runs
|
|
25
|
+
* under Node, which is exactly how this module came to exist.
|
|
26
|
+
*
|
|
27
|
+
* Every caller passes an explicit argument array. There is no shell string and
|
|
28
|
+
* no interpolation, so profile names, descriptions, and file contents can never
|
|
29
|
+
* be reinterpreted as shell syntax.
|
|
30
|
+
*
|
|
31
|
+
* Import only from CLI-side code: spawning is unavailable inside a Cloudflare
|
|
32
|
+
* Worker.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Run a command, capturing its output.
|
|
36
|
+
*
|
|
37
|
+
* Rejects with the underlying spawn error when the executable is missing (an
|
|
38
|
+
* `ENOENT`), so callers can distinguish "not installed" from "ran and failed"
|
|
39
|
+
* — a non-zero exit code always means the binary ran.
|
|
40
|
+
*/
|
|
41
|
+
function spawnCapture(input) {
|
|
42
|
+
const { args, command, env, stdin } = input;
|
|
43
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
44
|
+
const child = spawn(command, [...args], {
|
|
45
|
+
stdio: [
|
|
46
|
+
stdin === void 0 ? "ignore" : "pipe",
|
|
47
|
+
"pipe",
|
|
48
|
+
"pipe"
|
|
49
|
+
],
|
|
50
|
+
...env === void 0 ? {} : { env }
|
|
51
|
+
});
|
|
52
|
+
const { stderr: errStream, stdout: outStream } = child;
|
|
53
|
+
if (outStream === null || errStream === null) {
|
|
54
|
+
reject(/* @__PURE__ */ new Error(`Could not capture output from "${command}".`));
|
|
55
|
+
return promise;
|
|
56
|
+
}
|
|
57
|
+
let stdout = "";
|
|
58
|
+
let stderr = "";
|
|
59
|
+
outStream.setEncoding("utf8");
|
|
60
|
+
errStream.setEncoding("utf8");
|
|
61
|
+
outStream.on("data", (chunk) => {
|
|
62
|
+
stdout += chunk;
|
|
63
|
+
});
|
|
64
|
+
errStream.on("data", (chunk) => {
|
|
65
|
+
stderr += chunk;
|
|
66
|
+
});
|
|
67
|
+
child.on("error", reject);
|
|
68
|
+
child.on("close", (exitCode) => {
|
|
69
|
+
resolve({
|
|
70
|
+
exitCode,
|
|
71
|
+
stderr,
|
|
72
|
+
stdout
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
if (stdin !== void 0 && child.stdin !== null) {
|
|
76
|
+
child.stdin.on("error", () => void 0);
|
|
77
|
+
child.stdin.end(stdin, "utf8");
|
|
78
|
+
}
|
|
79
|
+
return promise;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region ../../packages/core/src/agent-runtime.ts
|
|
83
|
+
/**
|
|
84
|
+
* Agent-runtime detection and selection.
|
|
85
|
+
*
|
|
86
|
+
* OOMPF commands ask the local `omp` binary for profile paths. That binary is
|
|
87
|
+
* one of two agent runtimes — `omp` (OMP) or `pi` (Programmable Intelligence).
|
|
88
|
+
* This module resolves which binary a command should invoke: the installed
|
|
89
|
+
* runtime, the sole installed one, or an explicit `--agent` request.
|
|
90
|
+
*
|
|
91
|
+
* It selects the *binary* only. `pi`'s profile-path layout compatibility is
|
|
92
|
+
* tracked upstream (GPS-148) and is deliberately out of scope here — the
|
|
93
|
+
* chosen command simply flows through the existing `ompCommand` seam.
|
|
94
|
+
*/
|
|
95
|
+
/** The executable name for each agent runtime. */
|
|
96
|
+
const AGENT_RUNTIME_COMMANDS = {
|
|
97
|
+
omp: "omp",
|
|
98
|
+
pi: "pi"
|
|
99
|
+
};
|
|
100
|
+
/** No usable agent runtime is installed (or the requested one is absent). */
|
|
101
|
+
var AgentRuntimeUnavailableError = class extends Error {
|
|
102
|
+
constructor(message) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.name = "AgentRuntimeUnavailableError";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/** Default probe: the binary exists when `--version` runs at all. */
|
|
108
|
+
async function defaultProbe(command) {
|
|
109
|
+
try {
|
|
110
|
+
await spawnCapture({
|
|
111
|
+
args: ["--version"],
|
|
112
|
+
command
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const INSTALL_BOTH = `Install either ${AGENT_RUNTIME_COMMANDS.omp} or ${AGENT_RUNTIME_COMMANDS.pi} and retry.`;
|
|
120
|
+
/**
|
|
121
|
+
* Resolve which agent-runtime binary a command should drive.
|
|
122
|
+
*
|
|
123
|
+
* Precedence:
|
|
124
|
+
* 1. `requested` set -> probe only that runtime. Present -> return it;
|
|
125
|
+
* absent -> throw {@link AgentRuntimeUnavailableError}.
|
|
126
|
+
* 2. `requested` unset -> probe both concurrently. Both present -> `omp`;
|
|
127
|
+
* exactly one -> that one; neither -> throw
|
|
128
|
+
* {@link AgentRuntimeUnavailableError}.
|
|
129
|
+
*/
|
|
130
|
+
async function resolveAgentRuntime(options) {
|
|
131
|
+
const probe = options?.probe ?? defaultProbe;
|
|
132
|
+
if (options?.requested !== void 0) {
|
|
133
|
+
const requested = options.requested;
|
|
134
|
+
if (await probe(AGENT_RUNTIME_COMMANDS[requested])) return {
|
|
135
|
+
command: AGENT_RUNTIME_COMMANDS[requested],
|
|
136
|
+
runtime: requested
|
|
137
|
+
};
|
|
138
|
+
throw new AgentRuntimeUnavailableError(`The ${requested} agent runtime is not installed (expected the ${AGENT_RUNTIME_COMMANDS[requested]} binary on PATH). ${INSTALL_BOTH}`);
|
|
139
|
+
}
|
|
140
|
+
const [ompPresent, piPresent] = await Promise.all([probe(AGENT_RUNTIME_COMMANDS.omp), probe(AGENT_RUNTIME_COMMANDS.pi)]);
|
|
141
|
+
if (ompPresent) return {
|
|
142
|
+
command: AGENT_RUNTIME_COMMANDS.omp,
|
|
143
|
+
runtime: "omp"
|
|
144
|
+
};
|
|
145
|
+
if (piPresent) return {
|
|
146
|
+
command: AGENT_RUNTIME_COMMANDS.pi,
|
|
147
|
+
runtime: "pi"
|
|
148
|
+
};
|
|
149
|
+
throw new AgentRuntimeUnavailableError(`No agent runtime is installed (expected ${AGENT_RUNTIME_COMMANDS.omp} or ${AGENT_RUNTIME_COMMANDS.pi} on PATH). ${INSTALL_BOTH}`);
|
|
150
|
+
}
|
|
15
151
|
//#endregion
|
|
16
152
|
//#region ../../packages/core/src/guards.ts
|
|
17
153
|
/**
|
|
@@ -374,6 +510,23 @@ function extractTags(value, warnings) {
|
|
|
374
510
|
if (droppedNonString) warnings.push("oompf.tags dropped one or more non-string entries.");
|
|
375
511
|
return tags;
|
|
376
512
|
}
|
|
513
|
+
/** Keep a link only when its URL is a safe `http(s)` target. */
|
|
514
|
+
function httpLink(url, label, warnings) {
|
|
515
|
+
let protocol;
|
|
516
|
+
try {
|
|
517
|
+
protocol = new URL(url).protocol;
|
|
518
|
+
} catch {
|
|
519
|
+
protocol = "";
|
|
520
|
+
}
|
|
521
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
522
|
+
warnings.push("oompf.links dropped an entry with a non-http(s) URL.");
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
label,
|
|
527
|
+
url
|
|
528
|
+
};
|
|
529
|
+
}
|
|
377
530
|
/** Coerce a single link entry (string URL or `{ url, label? }`) into a link. */
|
|
378
531
|
function extractLink(entry, warnings) {
|
|
379
532
|
if (typeof entry === "string") {
|
|
@@ -382,10 +535,7 @@ function extractLink(entry, warnings) {
|
|
|
382
535
|
warnings.push("oompf.links dropped an entry with an empty URL.");
|
|
383
536
|
return null;
|
|
384
537
|
}
|
|
385
|
-
return
|
|
386
|
-
label: null,
|
|
387
|
-
url
|
|
388
|
-
};
|
|
538
|
+
return httpLink(url, null, warnings);
|
|
389
539
|
}
|
|
390
540
|
if (isRecord(entry)) {
|
|
391
541
|
const rawUrl = entry.url;
|
|
@@ -393,10 +543,8 @@ function extractLink(entry, warnings) {
|
|
|
393
543
|
warnings.push("oompf.links dropped an entry missing a string `url`.");
|
|
394
544
|
return null;
|
|
395
545
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
url: rawUrl.trim()
|
|
399
|
-
};
|
|
546
|
+
const label = typeof entry.label === "string" && entry.label.trim() !== "" ? entry.label.trim() : null;
|
|
547
|
+
return httpLink(rawUrl.trim(), label, warnings);
|
|
400
548
|
}
|
|
401
549
|
warnings.push("oompf.links dropped an entry that was not a string or object.");
|
|
402
550
|
return null;
|
|
@@ -447,6 +595,43 @@ function extractMetadata(document) {
|
|
|
447
595
|
warnings
|
|
448
596
|
};
|
|
449
597
|
}
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region ../../packages/core/src/model-catalog.ts
|
|
600
|
+
/**
|
|
601
|
+
* GPS-150-1 is the first reviewed snapshot. A catalog revision changes only
|
|
602
|
+
* when its mappings change in a normal code review; upgrade output reports it
|
|
603
|
+
* so a proposed change remains attributable and reproducible.
|
|
604
|
+
*/
|
|
605
|
+
const MODEL_CATALOG = {
|
|
606
|
+
models: [{
|
|
607
|
+
deployment: "hosted",
|
|
608
|
+
docsUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
|
|
609
|
+
id: "anthropic/claude-opus-4",
|
|
610
|
+
providerId: "anthropic",
|
|
611
|
+
reasoning: "reasoning",
|
|
612
|
+
roles: [
|
|
613
|
+
"coding",
|
|
614
|
+
"planning",
|
|
615
|
+
"review"
|
|
616
|
+
],
|
|
617
|
+
successor: "anthropic/claude-opus-4.8",
|
|
618
|
+
tier: "frontier"
|
|
619
|
+
}, {
|
|
620
|
+
deployment: "hosted",
|
|
621
|
+
docsUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
|
|
622
|
+
id: "anthropic/claude-opus-4.8",
|
|
623
|
+
providerId: "anthropic",
|
|
624
|
+
reasoning: "reasoning",
|
|
625
|
+
roles: [
|
|
626
|
+
"coding",
|
|
627
|
+
"planning",
|
|
628
|
+
"review"
|
|
629
|
+
],
|
|
630
|
+
successor: null,
|
|
631
|
+
tier: "frontier"
|
|
632
|
+
}],
|
|
633
|
+
revision: "gps-150-1"
|
|
634
|
+
};
|
|
450
635
|
/**
|
|
451
636
|
* Charset/shape rule. The leading class forbids a name starting with `.`, `_`,
|
|
452
637
|
* or `-`; the `{0,63}` bound caps the total length at 64 characters.
|
|
@@ -499,71 +684,6 @@ function validateProfileName(name) {
|
|
|
499
684
|
};
|
|
500
685
|
}
|
|
501
686
|
//#endregion
|
|
502
|
-
//#region ../../packages/core/src/spawn.ts
|
|
503
|
-
/**
|
|
504
|
-
* The workspace's single process-spawning primitive.
|
|
505
|
-
*
|
|
506
|
-
* Backed by `node:child_process` rather than `Bun.spawn`. Bun implements the
|
|
507
|
-
* Node API, so one implementation serves both runtimes — whereas reaching for
|
|
508
|
-
* the `Bun` global makes the published CLI throw
|
|
509
|
-
* `Cannot read properties of undefined (reading 'spawn')` the moment it runs
|
|
510
|
-
* under Node, which is exactly how this module came to exist.
|
|
511
|
-
*
|
|
512
|
-
* Every caller passes an explicit argument array. There is no shell string and
|
|
513
|
-
* no interpolation, so profile names, descriptions, and file contents can never
|
|
514
|
-
* be reinterpreted as shell syntax.
|
|
515
|
-
*
|
|
516
|
-
* Import only from CLI-side code: spawning is unavailable inside a Cloudflare
|
|
517
|
-
* Worker.
|
|
518
|
-
*/
|
|
519
|
-
/**
|
|
520
|
-
* Run a command, capturing its output.
|
|
521
|
-
*
|
|
522
|
-
* Rejects with the underlying spawn error when the executable is missing (an
|
|
523
|
-
* `ENOENT`), so callers can distinguish "not installed" from "ran and failed"
|
|
524
|
-
* — a non-zero exit code always means the binary ran.
|
|
525
|
-
*/
|
|
526
|
-
function spawnCapture(input) {
|
|
527
|
-
const { args, command, env, stdin } = input;
|
|
528
|
-
const { promise, resolve, reject } = Promise.withResolvers();
|
|
529
|
-
const child = spawn(command, [...args], {
|
|
530
|
-
stdio: [
|
|
531
|
-
stdin === void 0 ? "ignore" : "pipe",
|
|
532
|
-
"pipe",
|
|
533
|
-
"pipe"
|
|
534
|
-
],
|
|
535
|
-
...env === void 0 ? {} : { env }
|
|
536
|
-
});
|
|
537
|
-
const { stderr: errStream, stdout: outStream } = child;
|
|
538
|
-
if (outStream === null || errStream === null) {
|
|
539
|
-
reject(/* @__PURE__ */ new Error(`Could not capture output from "${command}".`));
|
|
540
|
-
return promise;
|
|
541
|
-
}
|
|
542
|
-
let stdout = "";
|
|
543
|
-
let stderr = "";
|
|
544
|
-
outStream.setEncoding("utf8");
|
|
545
|
-
errStream.setEncoding("utf8");
|
|
546
|
-
outStream.on("data", (chunk) => {
|
|
547
|
-
stdout += chunk;
|
|
548
|
-
});
|
|
549
|
-
errStream.on("data", (chunk) => {
|
|
550
|
-
stderr += chunk;
|
|
551
|
-
});
|
|
552
|
-
child.on("error", reject);
|
|
553
|
-
child.on("close", (exitCode) => {
|
|
554
|
-
resolve({
|
|
555
|
-
exitCode,
|
|
556
|
-
stderr,
|
|
557
|
-
stdout
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
if (stdin !== void 0 && child.stdin !== null) {
|
|
561
|
-
child.stdin.on("error", () => void 0);
|
|
562
|
-
child.stdin.end(stdin, "utf8");
|
|
563
|
-
}
|
|
564
|
-
return promise;
|
|
565
|
-
}
|
|
566
|
-
//#endregion
|
|
567
687
|
//#region ../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
|
|
568
688
|
var require_identity = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
569
689
|
const ALIAS = Symbol.for("yaml.alias");
|
|
@@ -7254,6 +7374,17 @@ function assertProfileDocument(value) {
|
|
|
7254
7374
|
* names — even hostile ones — are passed as opaque arguments with no shell
|
|
7255
7375
|
* interpolation.
|
|
7256
7376
|
*/
|
|
7377
|
+
/** A named OMP profile whose resolved agent directory is absent. */
|
|
7378
|
+
var OmpProfileNotFoundError = class extends Error {
|
|
7379
|
+
profile;
|
|
7380
|
+
resolvedPath;
|
|
7381
|
+
constructor(profile, resolvedPath) {
|
|
7382
|
+
super(`Resolved agent directory for profile "${profile}" is not a directory (missing): "${resolvedPath}".`);
|
|
7383
|
+
this.profile = profile;
|
|
7384
|
+
this.resolvedPath = resolvedPath;
|
|
7385
|
+
this.name = "OmpProfileNotFoundError";
|
|
7386
|
+
}
|
|
7387
|
+
};
|
|
7257
7388
|
const DEFAULT_OMP_COMMAND = "omp";
|
|
7258
7389
|
/**
|
|
7259
7390
|
* Config filenames OOMPF understands, in preference order: `config.yml` wins
|
|
@@ -7300,10 +7431,26 @@ async function resolveConfigPath(profile, ompCommand, requireDirectory) {
|
|
|
7300
7431
|
if (!isAbsolute(resolved)) throw new Error(`OMP returned a non-absolute config path for the ${label}: "${resolved}".`);
|
|
7301
7432
|
if (requireDirectory) {
|
|
7302
7433
|
const kind = await statKind(resolved);
|
|
7434
|
+
if (kind === "missing" && profile !== null) throw new OmpProfileNotFoundError(profile, resolved);
|
|
7303
7435
|
if (kind !== "directory") throw new Error(`Resolved agent directory for the ${label} is not a directory (${kind}): "${resolved}".`);
|
|
7304
7436
|
}
|
|
7305
7437
|
return resolved;
|
|
7306
7438
|
}
|
|
7439
|
+
/**
|
|
7440
|
+
* Verify a named profile exists before invoking its OMP path resolver.
|
|
7441
|
+
*
|
|
7442
|
+
* OMP may create the named agent directory as a side effect of `config path`,
|
|
7443
|
+
* so checking only after that command turns an absent profile into a
|
|
7444
|
+
* config-less one. The default path is side-effect-free for named profiles and
|
|
7445
|
+
* gives us the same profiles root used by discovery.
|
|
7446
|
+
*/
|
|
7447
|
+
async function requireExistingProfile(profile, ompCommand) {
|
|
7448
|
+
const defaultAgentDir = await resolveConfigPath(null, ompCommand, false);
|
|
7449
|
+
const agentDir = join(dirname(defaultAgentDir), "profiles", profile, "agent");
|
|
7450
|
+
const kind = await statKind(agentDir);
|
|
7451
|
+
if (kind === "missing") throw new OmpProfileNotFoundError(profile, agentDir);
|
|
7452
|
+
if (kind !== "directory") throw new Error(`Resolved agent directory for profile "${profile}" is not a directory (${kind}): "${agentDir}".`);
|
|
7453
|
+
}
|
|
7307
7454
|
async function findConfigFile(agentDir) {
|
|
7308
7455
|
for (const filename of CONFIG_FILENAMES$1) {
|
|
7309
7456
|
const candidate = join(agentDir, filename);
|
|
@@ -7337,7 +7484,9 @@ async function resolveInstallTarget(name, options) {
|
|
|
7337
7484
|
async function resolveProfileConfig(profile, options) {
|
|
7338
7485
|
const validation = validateProfileName(profile);
|
|
7339
7486
|
if (!validation.ok) throw new Error(validation.reason);
|
|
7340
|
-
const
|
|
7487
|
+
const ompCommand = options?.ompCommand ?? DEFAULT_OMP_COMMAND;
|
|
7488
|
+
await requireExistingProfile(validation.value, ompCommand);
|
|
7489
|
+
const agentDir = await resolveConfigPath(validation.value, ompCommand, true);
|
|
7341
7490
|
const configPath = await findConfigFile(agentDir);
|
|
7342
7491
|
return {
|
|
7343
7492
|
agentDir,
|
|
@@ -7380,6 +7529,182 @@ async function discoverProfiles(options) {
|
|
|
7380
7529
|
return discovered;
|
|
7381
7530
|
}
|
|
7382
7531
|
//#endregion
|
|
7532
|
+
//#region ../../packages/core/src/upgrade.ts
|
|
7533
|
+
/** Thinking suffixes that belong to the model selector's execution settings. */
|
|
7534
|
+
const THINKING_LEVELS = {
|
|
7535
|
+
auto: true,
|
|
7536
|
+
high: true,
|
|
7537
|
+
inherit: true,
|
|
7538
|
+
low: true,
|
|
7539
|
+
max: true,
|
|
7540
|
+
medium: true,
|
|
7541
|
+
minimal: true,
|
|
7542
|
+
off: true,
|
|
7543
|
+
xhigh: true
|
|
7544
|
+
};
|
|
7545
|
+
function splitThinkingSuffix(model) {
|
|
7546
|
+
const colon = model.lastIndexOf(":");
|
|
7547
|
+
if (colon <= 0) return {
|
|
7548
|
+
modelSelector: model,
|
|
7549
|
+
thinkingLevel: null
|
|
7550
|
+
};
|
|
7551
|
+
const suffix = model.slice(colon + 1);
|
|
7552
|
+
return Object.hasOwn(THINKING_LEVELS, suffix) ? {
|
|
7553
|
+
modelSelector: model.slice(0, colon),
|
|
7554
|
+
thinkingLevel: suffix
|
|
7555
|
+
} : {
|
|
7556
|
+
modelSelector: model,
|
|
7557
|
+
thinkingLevel: null
|
|
7558
|
+
};
|
|
7559
|
+
}
|
|
7560
|
+
/** Map common profile role labels to the catalog's broader role-fit classes. */
|
|
7561
|
+
const ROLE_TO_CATALOG_ROLE = {
|
|
7562
|
+
chat: "chat",
|
|
7563
|
+
coder: "coding",
|
|
7564
|
+
coding: "coding",
|
|
7565
|
+
planner: "planning",
|
|
7566
|
+
planning: "planning",
|
|
7567
|
+
review: "review",
|
|
7568
|
+
reviewer: "review"
|
|
7569
|
+
};
|
|
7570
|
+
function replacementFor(model, path, role, catalog, plan) {
|
|
7571
|
+
if (model.startsWith("@")) return model;
|
|
7572
|
+
const parsed = splitThinkingSuffix(model);
|
|
7573
|
+
const current = catalog.models.find((entry) => entry.id === parsed.modelSelector);
|
|
7574
|
+
if (current === void 0) {
|
|
7575
|
+
plan.unchanged.push({
|
|
7576
|
+
model,
|
|
7577
|
+
path,
|
|
7578
|
+
reason: "unclassified",
|
|
7579
|
+
role
|
|
7580
|
+
});
|
|
7581
|
+
return model;
|
|
7582
|
+
}
|
|
7583
|
+
if (current.successor === null || current.successor === current.id) {
|
|
7584
|
+
plan.unchanged.push({
|
|
7585
|
+
model,
|
|
7586
|
+
path,
|
|
7587
|
+
reason: current.successor === current.id ? "already_current" : "no_successor",
|
|
7588
|
+
role
|
|
7589
|
+
});
|
|
7590
|
+
return model;
|
|
7591
|
+
}
|
|
7592
|
+
const successor = catalog.models.find((entry) => entry.id === current.successor);
|
|
7593
|
+
if (successor === void 0) {
|
|
7594
|
+
plan.unchanged.push({
|
|
7595
|
+
model,
|
|
7596
|
+
path,
|
|
7597
|
+
reason: "unavailable",
|
|
7598
|
+
role
|
|
7599
|
+
});
|
|
7600
|
+
return model;
|
|
7601
|
+
}
|
|
7602
|
+
const catalogRole = role === null ? void 0 : ROLE_TO_CATALOG_ROLE[role];
|
|
7603
|
+
if (catalogRole !== void 0 && !successor.roles.includes(catalogRole)) {
|
|
7604
|
+
plan.unchanged.push({
|
|
7605
|
+
model,
|
|
7606
|
+
path,
|
|
7607
|
+
reason: "slot_mismatch",
|
|
7608
|
+
role
|
|
7609
|
+
});
|
|
7610
|
+
return model;
|
|
7611
|
+
}
|
|
7612
|
+
const to = parsed.thinkingLevel === null ? successor.id : `${successor.id}:${parsed.thinkingLevel}`;
|
|
7613
|
+
plan.changes.push({
|
|
7614
|
+
from: model,
|
|
7615
|
+
path,
|
|
7616
|
+
reason: "successor",
|
|
7617
|
+
role,
|
|
7618
|
+
to
|
|
7619
|
+
});
|
|
7620
|
+
return to;
|
|
7621
|
+
}
|
|
7622
|
+
function replaceArray(values, displayPath, nodePath, role, catalog, plan, setNode) {
|
|
7623
|
+
for (let index = 0; index < values.length; index++) {
|
|
7624
|
+
const value = values[index];
|
|
7625
|
+
if (typeof value !== "string") continue;
|
|
7626
|
+
const next = replacementFor(value, `${displayPath}[${index}]`, role, catalog, plan);
|
|
7627
|
+
if (next !== value) setNode([...nodePath, index], next);
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7630
|
+
function replaceModelRoles(document, catalog, plan, setNode) {
|
|
7631
|
+
const modelRoles = document.modelRoles;
|
|
7632
|
+
if (!isRecord(modelRoles)) return;
|
|
7633
|
+
for (const [role, assigned] of Object.entries(modelRoles)) {
|
|
7634
|
+
const displayPath = `modelRoles.${role}`;
|
|
7635
|
+
const nodePath = ["modelRoles", role];
|
|
7636
|
+
if (typeof assigned === "string") {
|
|
7637
|
+
const next = replacementFor(assigned, displayPath, role, catalog, plan);
|
|
7638
|
+
if (next !== assigned) setNode(nodePath, next);
|
|
7639
|
+
} else if (Array.isArray(assigned)) replaceArray(assigned, displayPath, nodePath, role, catalog, plan, setNode);
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
function replaceEnabledModels(document, catalog, plan, setNode) {
|
|
7643
|
+
if (Array.isArray(document.enabledModels)) replaceArray(document.enabledModels, "enabledModels", ["enabledModels"], null, catalog, plan, setNode);
|
|
7644
|
+
}
|
|
7645
|
+
function replaceRetryChains(document, catalog, plan, setNode) {
|
|
7646
|
+
if (!isRecord(document.retry)) return;
|
|
7647
|
+
const chains = document.retry.fallbackChains;
|
|
7648
|
+
if (isRecord(chains)) {
|
|
7649
|
+
for (const [role, value] of Object.entries(chains)) if (Array.isArray(value)) replaceArray(value, `retry.fallbackChains.${role}`, [
|
|
7650
|
+
"retry",
|
|
7651
|
+
"fallbackChains",
|
|
7652
|
+
role
|
|
7653
|
+
], null, catalog, plan, setNode);
|
|
7654
|
+
return;
|
|
7655
|
+
}
|
|
7656
|
+
if (!Array.isArray(chains)) return;
|
|
7657
|
+
if (chains.every((value) => typeof value === "string")) {
|
|
7658
|
+
replaceArray(chains, "retry.fallbackChains", ["retry", "fallbackChains"], null, catalog, plan, setNode);
|
|
7659
|
+
return;
|
|
7660
|
+
}
|
|
7661
|
+
for (let index = 0; index < chains.length; index++) {
|
|
7662
|
+
const value = chains[index];
|
|
7663
|
+
if (Array.isArray(value)) replaceArray(value, `retry.fallbackChains[${index}]`, [
|
|
7664
|
+
"retry",
|
|
7665
|
+
"fallbackChains",
|
|
7666
|
+
index
|
|
7667
|
+
], null, catalog, plan, setNode);
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
function applyReplacements(source, replacements) {
|
|
7671
|
+
let output = source;
|
|
7672
|
+
for (const replacement of [...replacements].sort((left, right) => right.start - left.start)) output = output.slice(0, replacement.start) + replacement.value + output.slice(replacement.end);
|
|
7673
|
+
return output;
|
|
7674
|
+
}
|
|
7675
|
+
/**
|
|
7676
|
+
* Produce a model-only YAML upgrade plan. The input document is never mutated;
|
|
7677
|
+
* only recognized model-selector locations are changed in the serialized copy.
|
|
7678
|
+
*/
|
|
7679
|
+
function proposeUpgrade(yaml, catalog) {
|
|
7680
|
+
const document = (0, import_dist.parseDocument)(yaml, { keepSourceTokens: true });
|
|
7681
|
+
const parsed = document.toJS();
|
|
7682
|
+
if (!isRecord(parsed)) throw new Error("Profile YAML must have a mapping root.");
|
|
7683
|
+
const plan = {
|
|
7684
|
+
changes: [],
|
|
7685
|
+
unchanged: []
|
|
7686
|
+
};
|
|
7687
|
+
const replacements = [];
|
|
7688
|
+
const setNode = (path, value) => {
|
|
7689
|
+
const node = document.getIn(path, true);
|
|
7690
|
+
if (node === null || typeof node !== "object" || !("range" in node) || !Array.isArray(node.range) || typeof node.range[0] !== "number" || typeof node.range[1] !== "number") throw new Error("Profile YAML model selector has no source range.");
|
|
7691
|
+
replacements.push({
|
|
7692
|
+
end: node.range[1],
|
|
7693
|
+
start: node.range[0],
|
|
7694
|
+
value
|
|
7695
|
+
});
|
|
7696
|
+
};
|
|
7697
|
+
replaceModelRoles(parsed, catalog, plan, setNode);
|
|
7698
|
+
replaceEnabledModels(parsed, catalog, plan, setNode);
|
|
7699
|
+
replaceRetryChains(parsed, catalog, plan, setNode);
|
|
7700
|
+
return {
|
|
7701
|
+
catalogRevision: catalog.revision,
|
|
7702
|
+
changes: plan.changes,
|
|
7703
|
+
unchanged: plan.unchanged,
|
|
7704
|
+
yaml: applyReplacements(yaml, replacements)
|
|
7705
|
+
};
|
|
7706
|
+
}
|
|
7707
|
+
//#endregion
|
|
7383
7708
|
//#region ../../packages/core/src/validation.ts
|
|
7384
7709
|
/**
|
|
7385
7710
|
* Structural validation and secret scanning for OMP profile artifacts.
|
|
@@ -7664,6 +7989,63 @@ function finishLocation(gistId, owner, revision, raw) {
|
|
|
7664
7989
|
};
|
|
7665
7990
|
}
|
|
7666
7991
|
/**
|
|
7992
|
+
* Read a raw profile body under the size cap, refusing oversized payloads
|
|
7993
|
+
* before the full body is materialized. Prefers the `content-length` header;
|
|
7994
|
+
* when absent, streams the body and aborts once accumulated bytes exceed the
|
|
7995
|
+
* cap. In-cap content is decoded byte-exactly, so hashing is unchanged.
|
|
7996
|
+
*/
|
|
7997
|
+
async function readRawYaml(rawResponse, gistId, filename) {
|
|
7998
|
+
const rawContentLength = rawResponse.headers instanceof Headers ? rawResponse.headers.get("content-length") : rawResponse.headers?.["content-length"];
|
|
7999
|
+
const contentLength = Number(rawContentLength);
|
|
8000
|
+
if (Number.isFinite(contentLength) && contentLength > 1048576) throw oversizedError(gistId, filename);
|
|
8001
|
+
const body = rawResponse.body;
|
|
8002
|
+
if (body === void 0 || body === null) return rawResponse.text();
|
|
8003
|
+
const reader = body.getReader();
|
|
8004
|
+
const chunks = [];
|
|
8005
|
+
let total = 0;
|
|
8006
|
+
for (;;) {
|
|
8007
|
+
const { done, value } = await reader.read();
|
|
8008
|
+
if (done) break;
|
|
8009
|
+
total += value.byteLength;
|
|
8010
|
+
if (total > 1048576) {
|
|
8011
|
+
await reader.cancel().catch(() => {});
|
|
8012
|
+
throw oversizedError(gistId, filename);
|
|
8013
|
+
}
|
|
8014
|
+
chunks.push(value);
|
|
8015
|
+
}
|
|
8016
|
+
const bytes = new Uint8Array(total);
|
|
8017
|
+
let offset = 0;
|
|
8018
|
+
for (const chunk of chunks) {
|
|
8019
|
+
bytes.set(chunk, offset);
|
|
8020
|
+
offset += chunk.byteLength;
|
|
8021
|
+
}
|
|
8022
|
+
return new TextDecoder().decode(bytes);
|
|
8023
|
+
}
|
|
8024
|
+
/** Value-free refusal shared by all oversized-payload rejection paths. */
|
|
8025
|
+
function oversizedError(gistId, filename) {
|
|
8026
|
+
return /* @__PURE__ */ new Error(`Gist "${gistId}" file "${filename}" exceeds the maximum supported artifact size.`);
|
|
8027
|
+
}
|
|
8028
|
+
/**
|
|
8029
|
+
* A Gist fetch that failed at the HTTP layer, carrying the response status.
|
|
8030
|
+
*
|
|
8031
|
+
* The status is what lets a caller tell "this source is gone" (404) from "we
|
|
8032
|
+
* could not ask right now" (403 rate limit, 429, 5xx). That distinction matters
|
|
8033
|
+
* to anything that records a judgement about a source: GitHub's unauthenticated
|
|
8034
|
+
* API allows 60 requests per hour per IP, and a Cloudflare Worker egresses from
|
|
8035
|
+
* shared addresses, so a 403 says nothing whatsoever about the Gist.
|
|
8036
|
+
*
|
|
8037
|
+
* Messages are unchanged from the plain `Error`s this replaced, so existing
|
|
8038
|
+
* message-based classification keeps working.
|
|
8039
|
+
*/
|
|
8040
|
+
var GistHttpError = class extends Error {
|
|
8041
|
+
status;
|
|
8042
|
+
constructor(message, status) {
|
|
8043
|
+
super(message);
|
|
8044
|
+
this.name = "GistHttpError";
|
|
8045
|
+
this.status = status;
|
|
8046
|
+
}
|
|
8047
|
+
};
|
|
8048
|
+
/**
|
|
7667
8049
|
* Fetch a public Gist and return its single canonical profile YAML source.
|
|
7668
8050
|
*
|
|
7669
8051
|
* The Gist metadata is read from `https://api.github.com/gists/<id>` (pinned
|
|
@@ -7672,8 +8054,10 @@ function finishLocation(gistId, owner, revision, raw) {
|
|
|
7672
8054
|
* an unsupported filename is rejected. The file's exact bytes are then read
|
|
7673
8055
|
* from its canonical `raw_url` and hashed.
|
|
7674
8056
|
*
|
|
7675
|
-
* @throws
|
|
7676
|
-
*
|
|
8057
|
+
* @throws {GistHttpError} for HTTP-level failures, carrying the status (404 for
|
|
8058
|
+
* a missing or private Gist).
|
|
8059
|
+
* @throws Error for unsupported references, ambiguous or unsupported YAML
|
|
8060
|
+
* files, oversized payloads, and transport failures.
|
|
7677
8061
|
*/
|
|
7678
8062
|
async function fetchPublicGist(source, options) {
|
|
7679
8063
|
const location = parseGistLocation(source);
|
|
@@ -7681,16 +8065,16 @@ async function fetchPublicGist(source, options) {
|
|
|
7681
8065
|
if (doFetch === void 0) throw new Error("No fetch implementation is available; inject one via options.fetch.");
|
|
7682
8066
|
const metaResponse = await doFetch(location.revision === null ? `https://api.github.com/gists/${location.gistId}` : `https://api.github.com/gists/${location.gistId}/${location.revision}`, { headers: GITHUB_API_HEADERS });
|
|
7683
8067
|
if (!metaResponse.ok) {
|
|
7684
|
-
if (metaResponse.status === 404) throw new
|
|
7685
|
-
throw new
|
|
8068
|
+
if (metaResponse.status === 404) throw new GistHttpError(`Public Gist "${location.gistId}" was not found. It may be private, deleted, or the ID may be wrong.`, 404);
|
|
8069
|
+
throw new GistHttpError(`Failed to fetch Gist "${location.gistId}": HTTP ${metaResponse.status}.`, metaResponse.status);
|
|
7686
8070
|
}
|
|
7687
8071
|
const meta = parseGistMetadata(await metaResponse.text(), location.gistId);
|
|
7688
8072
|
const yamlFile = selectYamlFile(meta.files, location.gistId);
|
|
7689
8073
|
let content;
|
|
7690
8074
|
if (yamlFile.rawUrl !== null) {
|
|
7691
8075
|
const rawResponse = await doFetch(yamlFile.rawUrl, { headers: { "User-Agent": "oompf" } });
|
|
7692
|
-
if (!rawResponse.ok) throw new
|
|
7693
|
-
content = await rawResponse.
|
|
8076
|
+
if (!rawResponse.ok) throw new GistHttpError(`Failed to fetch raw content for "${yamlFile.filename}" in Gist "${location.gistId}": HTTP ${rawResponse.status}.`, rawResponse.status);
|
|
8077
|
+
content = await readRawYaml(rawResponse, location.gistId, yamlFile.filename);
|
|
7694
8078
|
} else if (yamlFile.content === null) throw new Error(`Gist "${location.gistId}" file "${yamlFile.filename}" exposed no raw URL or content.`);
|
|
7695
8079
|
else content = yamlFile.content;
|
|
7696
8080
|
return {
|
|
@@ -7885,12 +8269,81 @@ async function createPublicProfileGist(input, options) {
|
|
|
7885
8269
|
};
|
|
7886
8270
|
}
|
|
7887
8271
|
/** Find the first `gist.github.com` URL in `gh`'s stdout, or `null`. */
|
|
8272
|
+
/**
|
|
8273
|
+
* Patch one file in an existing public Gist through `gh api`.
|
|
8274
|
+
*
|
|
8275
|
+
* The helper never creates a new Gist: preserving the source URL is what
|
|
8276
|
+
* preserves the stable OOMPF profile identity during `upgrade`.
|
|
8277
|
+
*/
|
|
8278
|
+
async function updatePublicProfileGist(input, options) {
|
|
8279
|
+
const ghCommand = options?.ghCommand ?? DEFAULT_GH_COMMAND;
|
|
8280
|
+
const runner = options?.runner ?? nodeCommandRunner;
|
|
8281
|
+
const body = JSON.stringify({ files: { [input.filename]: { content: input.content } } });
|
|
8282
|
+
let result;
|
|
8283
|
+
try {
|
|
8284
|
+
result = await runner({
|
|
8285
|
+
args: [
|
|
8286
|
+
"api",
|
|
8287
|
+
"--method",
|
|
8288
|
+
"PATCH",
|
|
8289
|
+
`gists/${input.gistId}`,
|
|
8290
|
+
"--input",
|
|
8291
|
+
"-"
|
|
8292
|
+
],
|
|
8293
|
+
command: ghCommand,
|
|
8294
|
+
stdin: body
|
|
8295
|
+
});
|
|
8296
|
+
} catch (error) {
|
|
8297
|
+
if (isMissingExecutable(error)) throw new Error(`GitHub CLI (\`${ghCommand}\`) was not found on your PATH. Install it from https://cli.github.com/ and run \`gh auth login\`.`);
|
|
8298
|
+
throw error;
|
|
8299
|
+
}
|
|
8300
|
+
if (result.exitCode !== 0) {
|
|
8301
|
+
const detail = result.stderr.trim() || `exit code ${result.exitCode}`;
|
|
8302
|
+
throw new Error(`GitHub CLI failed to update the public Gist: ${detail}`);
|
|
8303
|
+
}
|
|
8304
|
+
let parsed;
|
|
8305
|
+
try {
|
|
8306
|
+
parsed = JSON.parse(result.stdout);
|
|
8307
|
+
} catch {
|
|
8308
|
+
throw new Error("GitHub CLI returned invalid JSON after updating the public Gist.");
|
|
8309
|
+
}
|
|
8310
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || !("id" in parsed) || typeof parsed.id !== "string" || !("html_url" in parsed) || typeof parsed.html_url !== "string") throw new Error("GitHub CLI response after updating the public Gist was incomplete.");
|
|
8311
|
+
const first = ("history" in parsed && Array.isArray(parsed.history) ? parsed.history : [])[0];
|
|
8312
|
+
const revision = first !== null && typeof first === "object" && !Array.isArray(first) && "version" in first && typeof first.version === "string" ? first.version : null;
|
|
8313
|
+
return {
|
|
8314
|
+
gistId: parsed.id,
|
|
8315
|
+
htmlUrl: parsed.html_url,
|
|
8316
|
+
revision
|
|
8317
|
+
};
|
|
8318
|
+
}
|
|
7888
8319
|
function extractGistUrl(stdout) {
|
|
7889
8320
|
const match = stdout.match(/https?:\/\/gist\.github\.com\/\S+/);
|
|
7890
8321
|
if (match === null) return null;
|
|
7891
|
-
return match[0].replace(/[)
|
|
8322
|
+
return match[0].replace(/[).,\]]+$/, "");
|
|
7892
8323
|
}
|
|
7893
8324
|
//#endregion
|
|
8325
|
+
//#region src/profile-selector.ts
|
|
8326
|
+
function isInteractiveProfileSession(options) {
|
|
8327
|
+
return options.ci === void 0 && options.stdinIsTTY === true && options.stdoutIsTTY === true;
|
|
8328
|
+
}
|
|
8329
|
+
const defaultProfileSelector = {
|
|
8330
|
+
isInteractive: () => isInteractiveProfileSession({
|
|
8331
|
+
ci: process.env.CI,
|
|
8332
|
+
stdinIsTTY: process.stdin.isTTY,
|
|
8333
|
+
stdoutIsTTY: process.stdout.isTTY
|
|
8334
|
+
}),
|
|
8335
|
+
selectProfile: async (names) => {
|
|
8336
|
+
const selected = await select({
|
|
8337
|
+
message: "Select a profile to publish",
|
|
8338
|
+
options: names.map((name) => ({
|
|
8339
|
+
label: name,
|
|
8340
|
+
value: name
|
|
8341
|
+
}))
|
|
8342
|
+
});
|
|
8343
|
+
return isCancel(selected) ? null : selected;
|
|
8344
|
+
}
|
|
8345
|
+
};
|
|
8346
|
+
//#endregion
|
|
7894
8347
|
//#region src/deps.ts
|
|
7895
8348
|
/**
|
|
7896
8349
|
* Injectable seams shared by every OOMPF CLI command.
|
|
@@ -7924,6 +8377,10 @@ function toCliError(error, err) {
|
|
|
7924
8377
|
code: err.code,
|
|
7925
8378
|
message: err.message
|
|
7926
8379
|
});
|
|
8380
|
+
if (err instanceof AgentRuntimeUnavailableError) return error({
|
|
8381
|
+
code: "agent_not_found",
|
|
8382
|
+
message: err.message
|
|
8383
|
+
});
|
|
7927
8384
|
return error({
|
|
7928
8385
|
code: "error",
|
|
7929
8386
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -7964,6 +8421,8 @@ function resolveDeps(deps = {}) {
|
|
|
7964
8421
|
gistFetch: deps.gistFetch,
|
|
7965
8422
|
httpFetch: deps.httpFetch ?? defaultHttpFetch,
|
|
7966
8423
|
ompCommand: deps.ompCommand,
|
|
8424
|
+
profileSelector: deps.profileSelector ?? defaultProfileSelector,
|
|
8425
|
+
resolveAgentRuntime: deps.resolveAgentRuntime ?? resolveAgentRuntime,
|
|
7967
8426
|
resolveInstallTarget: deps.resolveInstallTarget ?? resolveInstallTarget,
|
|
7968
8427
|
resolveProfileConfig: deps.resolveProfileConfig ?? resolveProfileConfig,
|
|
7969
8428
|
runner: deps.runner
|
|
@@ -8020,8 +8479,10 @@ async function fetchProfileMetadata(baseUrl, id, fetchImpl) {
|
|
|
8020
8479
|
return parseJson(response, "not_found");
|
|
8021
8480
|
}
|
|
8022
8481
|
/** Free-text search over the OOMPF index. */
|
|
8023
|
-
async function searchProfiles(baseUrl, query, fetchImpl) {
|
|
8024
|
-
|
|
8482
|
+
async function searchProfiles(baseUrl, query, fetchImpl, cursor) {
|
|
8483
|
+
let url = joinUrl(baseUrl, `/api/v1/search?q=${encodeURIComponent(query)}`);
|
|
8484
|
+
if (cursor) url += `&cursor=${encodeURIComponent(cursor)}`;
|
|
8485
|
+
const response = await request(fetchImpl, url, { method: "GET" });
|
|
8025
8486
|
if (!response.ok) throw await envelopeError(response, "search_failed");
|
|
8026
8487
|
return parseJson(response, "search_failed");
|
|
8027
8488
|
}
|
|
@@ -8077,12 +8538,50 @@ const publishOutput = z.object({
|
|
|
8077
8538
|
structural: z.enum(["valid", "invalid"]),
|
|
8078
8539
|
warnings: z.array(z.string())
|
|
8079
8540
|
});
|
|
8541
|
+
/** `upgrade` result: a reviewed model diff and optional new Gist revision. */
|
|
8542
|
+
const upgradeOutput = z.object({
|
|
8543
|
+
catalogRevision: z.string(),
|
|
8544
|
+
changes: z.array(z.object({
|
|
8545
|
+
from: z.string(),
|
|
8546
|
+
path: z.string(),
|
|
8547
|
+
reason: z.literal("successor"),
|
|
8548
|
+
role: z.string().nullable(),
|
|
8549
|
+
to: z.string()
|
|
8550
|
+
})),
|
|
8551
|
+
currentRevision: z.string(),
|
|
8552
|
+
oompfUrl: z.string(),
|
|
8553
|
+
unchanged: z.array(z.object({
|
|
8554
|
+
model: z.string(),
|
|
8555
|
+
path: z.string(),
|
|
8556
|
+
reason: z.enum([
|
|
8557
|
+
"already_current",
|
|
8558
|
+
"no_successor",
|
|
8559
|
+
"slot_mismatch",
|
|
8560
|
+
"unavailable",
|
|
8561
|
+
"unclassified"
|
|
8562
|
+
]),
|
|
8563
|
+
role: z.string().nullable()
|
|
8564
|
+
})),
|
|
8565
|
+
updatedRevision: z.string().nullable()
|
|
8566
|
+
});
|
|
8567
|
+
/** A single machine-local prerequisite the installed profile needs. */
|
|
8568
|
+
const prerequisite = z.object({
|
|
8569
|
+
kind: z.enum([
|
|
8570
|
+
"provider",
|
|
8571
|
+
"environment",
|
|
8572
|
+
"project-overlay",
|
|
8573
|
+
"extension"
|
|
8574
|
+
]),
|
|
8575
|
+
name: z.string(),
|
|
8576
|
+
reason: z.string()
|
|
8577
|
+
});
|
|
8080
8578
|
/** `add` result: the installed profile and how to run it. */
|
|
8081
8579
|
const addOutput = z.object({
|
|
8082
8580
|
command: z.string().describe("Command to run OMP with this profile"),
|
|
8083
8581
|
hash: z.string().describe("SHA-256 of the installed artifact"),
|
|
8084
8582
|
name: z.string().describe("Local OMP profile name installed"),
|
|
8085
8583
|
path: z.string().describe("Config file written"),
|
|
8584
|
+
prerequisites: z.array(prerequisite).optional().describe("Profiles' machine-local prerequisites (names and kinds only, never secret values); informational, install succeeds regardless"),
|
|
8086
8585
|
revision: z.string().nullable(),
|
|
8087
8586
|
source: z.string().describe("Resolved canonical source"),
|
|
8088
8587
|
warnings: z.array(z.string())
|
|
@@ -8092,7 +8591,7 @@ const inspectOutput = z.object({
|
|
|
8092
8591
|
aliases: z.array(z.string()).describe("Named model aliases (@-prefixed)"),
|
|
8093
8592
|
errors: z.array(z.string()),
|
|
8094
8593
|
hash: z.string(),
|
|
8095
|
-
installCommand: z.string(),
|
|
8594
|
+
installCommand: z.string().optional(),
|
|
8096
8595
|
metadata: profileMetadataOutput,
|
|
8097
8596
|
models: z.array(z.string()),
|
|
8098
8597
|
name: z.string(),
|
|
@@ -8121,6 +8620,7 @@ const searchResult = z.object({
|
|
|
8121
8620
|
/** `search` result: the query plus its compact matches. */
|
|
8122
8621
|
const searchOutput = z.object({
|
|
8123
8622
|
count: z.number(),
|
|
8623
|
+
nextCursor: z.string().nullable().describe("Opaque cursor for the next page, or null on the last page"),
|
|
8124
8624
|
query: z.string(),
|
|
8125
8625
|
results: z.array(searchResult)
|
|
8126
8626
|
});
|
|
@@ -8130,8 +8630,9 @@ const searchOutput = z.object({
|
|
|
8130
8630
|
* `oompf add <ref> [--name <name>]` — install a shared profile as a native OMP
|
|
8131
8631
|
* profile.
|
|
8132
8632
|
*
|
|
8133
|
-
* The reference
|
|
8134
|
-
*
|
|
8633
|
+
* The reference must be an OOMPF URL or profile id. OOMPF resolves the
|
|
8634
|
+
* canonical Gist revision and verifies its fingerprint before anything is
|
|
8635
|
+
* written. The
|
|
8135
8636
|
* local name defaults to `<github-owner>-<profile-name>` (overridable with
|
|
8136
8637
|
* `--name`) and is checked against OMP's own naming rules. The install target
|
|
8137
8638
|
* directory is resolved by asking OMP itself (`omp --profile <name> config
|
|
@@ -8147,22 +8648,27 @@ function filenameStem$1(filename) {
|
|
|
8147
8648
|
/** Register the `add` command on the given CLI. */
|
|
8148
8649
|
function registerAdd(cli, deps) {
|
|
8149
8650
|
cli.command("add", {
|
|
8150
|
-
args: z.object({ ref: z.string().describe("OOMPF URL
|
|
8651
|
+
args: z.object({ ref: z.string().describe("OOMPF URL or profile id") }),
|
|
8151
8652
|
description: "Install a shared profile as a native OMP profile",
|
|
8152
8653
|
env: cliEnv,
|
|
8153
8654
|
examples: [{
|
|
8154
|
-
args: { ref: "https://
|
|
8155
|
-
description: "Install a profile
|
|
8655
|
+
args: { ref: "https://oompf.run/p/prof_0123" },
|
|
8656
|
+
description: "Install a profile with pinned verification"
|
|
8156
8657
|
}, {
|
|
8157
8658
|
args: { ref: "https://oompf.run/p/prof_0123" },
|
|
8158
8659
|
description: "Install under an explicit local name",
|
|
8159
8660
|
options: { name: "work" }
|
|
8160
8661
|
}],
|
|
8161
|
-
options: z.object({
|
|
8662
|
+
options: z.object({
|
|
8663
|
+
agent: z.enum(["omp", "pi"]).optional().describe("Agent runtime to use (default: omp)"),
|
|
8664
|
+
name: z.string().optional().describe("Local profile name (defaults to <owner>-<profile>)")
|
|
8665
|
+
}),
|
|
8162
8666
|
output: addOutput,
|
|
8163
8667
|
async run(c) {
|
|
8164
8668
|
try {
|
|
8165
8669
|
const oompfId = parseOompfRef(c.args.ref);
|
|
8670
|
+
if (oompfId === null) throw new CommandError("unverifiable_artifact", "Install requires an OOMPF URL or profile id so OOMPF can verify the pinned revision and fingerprint.");
|
|
8671
|
+
const ompCommand = deps.ompCommand ?? (await deps.resolveAgentRuntime({ requested: c.options.agent })).command;
|
|
8166
8672
|
let sourceUrl = c.args.ref;
|
|
8167
8673
|
let fetchUrl = sourceUrl;
|
|
8168
8674
|
let expectedHash = null;
|
|
@@ -8176,13 +8682,14 @@ function registerAdd(cli, deps) {
|
|
|
8176
8682
|
const gist = await fetchPublicGist(fetchUrl, { fetch: deps.gistFetch });
|
|
8177
8683
|
if (expectedHash !== null && gist.contentHash !== expectedHash) throw new CommandError("fingerprint_mismatch", "The pinned profile bytes do not match the indexed fingerprint. Refusing to install.");
|
|
8178
8684
|
const validation = validateArtifact({ yaml: gist.content });
|
|
8179
|
-
if (validation.structural === "invalid") throw new CommandError("invalid_artifact", `Refusing to install an invalid artifact: ${validation.errors.join("; ")}`);
|
|
8685
|
+
if (validation.structural === "invalid" || validation.facts === null) throw new CommandError("invalid_artifact", `Refusing to install an invalid artifact: ${validation.errors.join("; ")}`);
|
|
8686
|
+
if (validation.blocking.length > 0) throw new CommandError("blocking_secrets", `Refusing to install: high-confidence secrets detected at ${validation.blocking.map((f) => f.path).join(", ")}. Remove them and retry.`);
|
|
8180
8687
|
const stem = filenameStem$1(gist.filename);
|
|
8181
8688
|
const candidate = c.options.name ?? (gist.owner === null ? stem : `${gist.owner.toLowerCase()}-${stem}`);
|
|
8182
8689
|
const nameCheck = validateProfileName(candidate);
|
|
8183
8690
|
if (!nameCheck.ok) throw new CommandError("invalid_name", `Profile name "${candidate}" is invalid: ${nameCheck.reason} Pass --name <name>.`);
|
|
8184
8691
|
const name = nameCheck.value;
|
|
8185
|
-
const agentDir = await deps.resolveInstallTarget(name, { ompCommand
|
|
8692
|
+
const agentDir = await deps.resolveInstallTarget(name, { ompCommand });
|
|
8186
8693
|
for (const filename of CONFIG_FILENAMES) {
|
|
8187
8694
|
const existing = join(agentDir, filename);
|
|
8188
8695
|
if (await deps.fs.exists(existing)) throw new CommandError("target_exists", `Profile "${name}" already has a config at ${existing}. Refusing to overwrite.`);
|
|
@@ -8191,6 +8698,7 @@ function registerAdd(cli, deps) {
|
|
|
8191
8698
|
const target = join(agentDir, "config.yml");
|
|
8192
8699
|
await deps.fs.writeFile(target, gist.content, 384);
|
|
8193
8700
|
const command = `omp --profile ${name}`;
|
|
8701
|
+
const prerequisites = validation.facts.prerequisites;
|
|
8194
8702
|
return c.ok({
|
|
8195
8703
|
command,
|
|
8196
8704
|
hash: gist.contentHash,
|
|
@@ -8198,11 +8706,9 @@ function registerAdd(cli, deps) {
|
|
|
8198
8706
|
path: target,
|
|
8199
8707
|
revision: gist.revision,
|
|
8200
8708
|
source: sourceUrl,
|
|
8201
|
-
warnings: [...validation.warnings]
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
description: "Run it with:"
|
|
8205
|
-
} });
|
|
8709
|
+
warnings: [...validation.warnings],
|
|
8710
|
+
...prerequisites.length > 0 ? { prerequisites: [...prerequisites] } : {}
|
|
8711
|
+
});
|
|
8206
8712
|
} catch (error) {
|
|
8207
8713
|
return toCliError(c.error, error);
|
|
8208
8714
|
}
|
|
@@ -8217,8 +8723,8 @@ function registerAdd(cli, deps) {
|
|
|
8217
8723
|
* The reference may be an OOMPF URL/id or a public Gist URL/id. An OOMPF ref is
|
|
8218
8724
|
* answered from the index metadata; a Gist ref is fetched and validated live.
|
|
8219
8725
|
* Only displayable metadata — source, revision/hash, structural verdict, facts,
|
|
8220
|
-
* and the install command — is printed. The canonical artifact content is
|
|
8221
|
-
* emitted.
|
|
8726
|
+
* and the OOMPF install command — is printed. The canonical artifact content is
|
|
8727
|
+
* never emitted.
|
|
8222
8728
|
*/
|
|
8223
8729
|
/** Strip a recognised YAML extension from a Gist filename to get the stem. */
|
|
8224
8730
|
function filenameStem(filename) {
|
|
@@ -8261,7 +8767,7 @@ function registerInspect(cli, deps) {
|
|
|
8261
8767
|
structural: record.validation.structural,
|
|
8262
8768
|
warnings: [...record.validation.warnings]
|
|
8263
8769
|
}, { cta: {
|
|
8264
|
-
commands: [`oompf add ${c.args.ref}`],
|
|
8770
|
+
commands: [{ command: `oompf add ${c.args.ref}` }],
|
|
8265
8771
|
description: "Install it with:"
|
|
8266
8772
|
} });
|
|
8267
8773
|
}
|
|
@@ -8272,7 +8778,6 @@ function registerInspect(cli, deps) {
|
|
|
8272
8778
|
aliases: facts ? [...facts.aliases] : [],
|
|
8273
8779
|
errors: [...validation.errors],
|
|
8274
8780
|
hash: gist.contentHash,
|
|
8275
|
-
installCommand: `oompf add ${c.args.ref}`,
|
|
8276
8781
|
metadata: {
|
|
8277
8782
|
...validation.metadata,
|
|
8278
8783
|
links: [...validation.metadata.links],
|
|
@@ -8288,10 +8793,7 @@ function registerInspect(cli, deps) {
|
|
|
8288
8793
|
sourceType: "gist",
|
|
8289
8794
|
structural: validation.structural,
|
|
8290
8795
|
warnings: [...validation.warnings]
|
|
8291
|
-
}
|
|
8292
|
-
commands: [`oompf add ${c.args.ref}`],
|
|
8293
|
-
description: "Install it with:"
|
|
8294
|
-
} });
|
|
8796
|
+
});
|
|
8295
8797
|
} catch (error) {
|
|
8296
8798
|
return toCliError(c.error, error);
|
|
8297
8799
|
}
|
|
@@ -8312,37 +8814,57 @@ function registerInspect(cli, deps) {
|
|
|
8312
8814
|
* the single selected config artifact is sent, and high-confidence secrets
|
|
8313
8815
|
* abort the publish before anything leaves the machine.
|
|
8314
8816
|
*/
|
|
8817
|
+
/** Narrow a discovered profile to one that carries a publishable config. */
|
|
8818
|
+
function hasConfig(profile) {
|
|
8819
|
+
return profile.configPath !== null;
|
|
8820
|
+
}
|
|
8315
8821
|
/** Concatenate the base URL and a site-relative path from the register call. */
|
|
8316
|
-
function toOompfUrl(baseUrl, path) {
|
|
8822
|
+
function toOompfUrl$1(baseUrl, path) {
|
|
8317
8823
|
return `${baseUrl.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
8318
8824
|
}
|
|
8319
8825
|
/** Register the `publish` command on the given CLI. */
|
|
8320
8826
|
function registerPublish(cli, deps) {
|
|
8321
8827
|
cli.command("publish", {
|
|
8322
|
-
args: z.object({ profile: z.string().optional().describe("
|
|
8828
|
+
args: z.object({ profile: z.string().optional().describe("Native OMP profile name; omitted selects a publishable profile") }),
|
|
8323
8829
|
description: "Publish a local OMP profile as a public Gist and index it",
|
|
8324
8830
|
env: cliEnv,
|
|
8325
8831
|
examples: [{
|
|
8326
8832
|
args: { profile: "work" },
|
|
8327
8833
|
description: "Publish the 'work' profile"
|
|
8328
8834
|
}],
|
|
8835
|
+
options: z.object({ agent: z.enum(["omp", "pi"]).optional().describe("Agent runtime to use (default: omp)") }),
|
|
8329
8836
|
output: publishOutput,
|
|
8330
8837
|
async run(c) {
|
|
8331
8838
|
try {
|
|
8332
|
-
const ompOptions = { ompCommand: deps.ompCommand };
|
|
8839
|
+
const ompOptions = { ompCommand: deps.ompCommand ?? (await deps.resolveAgentRuntime({ requested: c.options.agent })).command };
|
|
8333
8840
|
let name;
|
|
8334
8841
|
let configPath;
|
|
8335
8842
|
if (c.args.profile === void 0) {
|
|
8336
|
-
const
|
|
8337
|
-
if (
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8843
|
+
const publishable = (await deps.discoverProfiles(ompOptions)).filter(hasConfig);
|
|
8844
|
+
if (publishable.length === 0) throw new CommandError("no_profile", "No publishable OMP profiles found. Create a profile with config.yml/config.yaml or pass an existing profile name.");
|
|
8845
|
+
let selected = publishable[0];
|
|
8846
|
+
if (publishable.length > 1) {
|
|
8847
|
+
const names = publishable.map((profile) => profile.name);
|
|
8848
|
+
if (c.formatExplicit || !deps.profileSelector.isInteractive()) throw new CommandError("ambiguous_profile", `Multiple publishable profiles found (${names.join(", ")}). Specify one: oompf publish <profile>.`);
|
|
8849
|
+
const selectedName = await deps.profileSelector.selectProfile(names);
|
|
8850
|
+
if (selectedName === null) throw new CommandError("selection_cancelled", "Profile selection was cancelled. Nothing was published.");
|
|
8851
|
+
const picked = publishable.find((profile) => profile.name === selectedName);
|
|
8852
|
+
if (picked === void 0) throw new CommandError("selection_invariant", "The profile selector returned a name that was not offered.");
|
|
8853
|
+
selected = picked;
|
|
8854
|
+
}
|
|
8855
|
+
name = selected.name;
|
|
8856
|
+
configPath = selected.configPath;
|
|
8342
8857
|
} else {
|
|
8343
|
-
const
|
|
8344
|
-
|
|
8345
|
-
|
|
8858
|
+
const validation = validateProfileName(c.args.profile);
|
|
8859
|
+
if (!validation.ok) throw new CommandError("invalid_profile", validation.reason);
|
|
8860
|
+
try {
|
|
8861
|
+
const resolved = await deps.resolveProfileConfig(validation.value, ompOptions);
|
|
8862
|
+
name = resolved.profile;
|
|
8863
|
+
configPath = resolved.configPath;
|
|
8864
|
+
} catch (error) {
|
|
8865
|
+
if (error instanceof OmpProfileNotFoundError) throw new CommandError("profile_not_found", `OMP profile "${validation.value}" was not found.`);
|
|
8866
|
+
throw error;
|
|
8867
|
+
}
|
|
8346
8868
|
}
|
|
8347
8869
|
if (configPath === null) throw new CommandError("missing_config", `Profile "${name}" has no config.yml/config.yaml to publish.`);
|
|
8348
8870
|
const yaml = await deps.fs.readFile(configPath);
|
|
@@ -8362,7 +8884,7 @@ function registerPublish(cli, deps) {
|
|
|
8362
8884
|
runner: deps.runner
|
|
8363
8885
|
});
|
|
8364
8886
|
const registration = await registerProfile(c.env.OOMPF_BASE_URL, { source: gist.htmlUrl }, deps.httpFetch);
|
|
8365
|
-
const oompfUrl = toOompfUrl(c.env.OOMPF_BASE_URL, registration.url);
|
|
8887
|
+
const oompfUrl = toOompfUrl$1(c.env.OOMPF_BASE_URL, registration.url);
|
|
8366
8888
|
const addCommand = `oompf add ${oompfUrl}`;
|
|
8367
8889
|
return c.ok({
|
|
8368
8890
|
addCommand,
|
|
@@ -8381,7 +8903,7 @@ function registerPublish(cli, deps) {
|
|
|
8381
8903
|
structural: registration.validation.structural,
|
|
8382
8904
|
warnings: [...validation.warnings, ...registration.validation.warnings]
|
|
8383
8905
|
}, { cta: {
|
|
8384
|
-
commands: [addCommand],
|
|
8906
|
+
commands: [{ command: addCommand }],
|
|
8385
8907
|
description: "Install it with:"
|
|
8386
8908
|
} });
|
|
8387
8909
|
} catch (error) {
|
|
@@ -8409,13 +8931,15 @@ function registerSearch(cli, deps) {
|
|
|
8409
8931
|
args: { query: "anthropic" },
|
|
8410
8932
|
description: "Search for a term"
|
|
8411
8933
|
}],
|
|
8934
|
+
options: z.object({ cursor: z.string().optional().describe("Opaque pagination cursor from a previous search") }),
|
|
8412
8935
|
output: searchOutput,
|
|
8413
8936
|
async run(c) {
|
|
8414
8937
|
try {
|
|
8415
8938
|
const query = c.args.query ?? "";
|
|
8416
|
-
const response = await searchProfiles(c.env.OOMPF_BASE_URL, query, deps.httpFetch);
|
|
8939
|
+
const response = await searchProfiles(c.env.OOMPF_BASE_URL, query, deps.httpFetch, c.options.cursor);
|
|
8417
8940
|
return c.ok({
|
|
8418
8941
|
count: response.results.length,
|
|
8942
|
+
nextCursor: response.nextCursor,
|
|
8419
8943
|
query: response.query,
|
|
8420
8944
|
results: response.results.map((r) => ({
|
|
8421
8945
|
id: r.id,
|
|
@@ -8437,15 +8961,100 @@ function registerSearch(cli, deps) {
|
|
|
8437
8961
|
});
|
|
8438
8962
|
}
|
|
8439
8963
|
//#endregion
|
|
8964
|
+
//#region src/commands/upgrade.ts
|
|
8965
|
+
/**
|
|
8966
|
+
* `oompf upgrade <ref>` — review and optionally apply model-only successors.
|
|
8967
|
+
*
|
|
8968
|
+
* The command plans from the current owned Gist head, not the indexed pinned
|
|
8969
|
+
* revision. It patches that same Gist only after confirmation, then registers
|
|
8970
|
+
* the unchanged source URL so the stable OOMPF profile identity survives.
|
|
8971
|
+
*/
|
|
8972
|
+
function toOompfUrl(baseUrl, id) {
|
|
8973
|
+
return `${baseUrl.replace(/\/+$/, "")}/p/${id}`;
|
|
8974
|
+
}
|
|
8975
|
+
function currentGistUrl(gistId) {
|
|
8976
|
+
return `https://api.github.com/gists/${gistId}`;
|
|
8977
|
+
}
|
|
8978
|
+
/** Register the `upgrade` command on the given CLI. */
|
|
8979
|
+
function registerUpgrade(cli, deps) {
|
|
8980
|
+
cli.command("upgrade", {
|
|
8981
|
+
args: z.object({ ref: z.string().describe("OOMPF profile URL or stable profile id") }),
|
|
8982
|
+
description: "Review and optionally apply model upgrades to an OOMPF profile",
|
|
8983
|
+
env: cliEnv,
|
|
8984
|
+
examples: [{
|
|
8985
|
+
args: { ref: "https://oompf.run/p/prof_..." },
|
|
8986
|
+
description: "Preview model upgrades for an indexed profile"
|
|
8987
|
+
}],
|
|
8988
|
+
options: z.object({ yes: z.boolean().optional().describe("Apply the reviewed plan without prompting") }),
|
|
8989
|
+
output: upgradeOutput,
|
|
8990
|
+
async run(c) {
|
|
8991
|
+
try {
|
|
8992
|
+
const id = parseOompfRef(c.args.ref);
|
|
8993
|
+
if (id === null) throw new CommandError("invalid_ref", "Upgrade requires an OOMPF profile URL or stable profile id.");
|
|
8994
|
+
const record = await fetchProfileMetadata(c.env.OOMPF_BASE_URL, id, deps.httpFetch);
|
|
8995
|
+
if (record.gistId === null || record.gistId === void 0) throw new CommandError("unverifiable_artifact", "The indexed profile has no source Gist and cannot be upgraded.");
|
|
8996
|
+
const fetchCurrent = () => fetchPublicGist(currentGistUrl(record.gistId), { fetch: deps.gistFetch });
|
|
8997
|
+
const current = await fetchCurrent();
|
|
8998
|
+
if (current.owner === null) throw new CommandError("unowned_gist", "The source Gist has no public owner and cannot be patched.");
|
|
8999
|
+
const plan = proposeUpgrade(current.content, MODEL_CATALOG);
|
|
9000
|
+
const output = {
|
|
9001
|
+
catalogRevision: plan.catalogRevision,
|
|
9002
|
+
changes: [...plan.changes],
|
|
9003
|
+
currentRevision: current.revision ?? record.revision ?? "unknown",
|
|
9004
|
+
oompfUrl: toOompfUrl(c.env.OOMPF_BASE_URL, record.id),
|
|
9005
|
+
unchanged: [...plan.unchanged],
|
|
9006
|
+
updatedRevision: null
|
|
9007
|
+
};
|
|
9008
|
+
const shouldPrompt = c.options.yes !== true && !c.formatExplicit && deps.profileSelector.isInteractive();
|
|
9009
|
+
if (!(c.options.yes || shouldPrompt)) return c.ok(output);
|
|
9010
|
+
if (shouldPrompt) {
|
|
9011
|
+
const answer = await confirm({ message: plan.changes.length === 0 ? "No model changes are proposed. Exit without writing?" : `Apply ${plan.changes.length} model change(s) to the current Gist?` });
|
|
9012
|
+
if (isCancel(answer) || answer !== true) return c.ok(output);
|
|
9013
|
+
}
|
|
9014
|
+
if (plan.changes.length === 0) return c.ok(output);
|
|
9015
|
+
const latest = await fetchCurrent();
|
|
9016
|
+
if (latest.contentHash !== current.contentHash) throw new CommandError("head_changed", "The source Gist changed while the upgrade was being reviewed. Run upgrade again to plan from the new head.");
|
|
9017
|
+
const identity = await getGithubIdentity({
|
|
9018
|
+
ghCommand: deps.ghCommand,
|
|
9019
|
+
runner: deps.runner
|
|
9020
|
+
});
|
|
9021
|
+
if (latest.owner?.toLowerCase() !== identity.login.toLowerCase()) throw new CommandError("unowned_gist", "The authenticated GitHub user does not own the source Gist; refusing to patch it.");
|
|
9022
|
+
const validation = validateArtifact({ yaml: plan.yaml });
|
|
9023
|
+
if (validation.structural === "invalid" || validation.facts === null) throw new CommandError("invalid_artifact", `The upgraded profile is structurally invalid: ${validation.errors.join("; ")}`);
|
|
9024
|
+
if (validation.blocking.length > 0) throw new CommandError("blocking_secrets", "Refusing to patch the Gist: the upgraded profile contains high-confidence secrets.");
|
|
9025
|
+
const updated = await updatePublicProfileGist({
|
|
9026
|
+
content: plan.yaml,
|
|
9027
|
+
filename: latest.filename,
|
|
9028
|
+
gistId: latest.gistId
|
|
9029
|
+
}, {
|
|
9030
|
+
ghCommand: deps.ghCommand,
|
|
9031
|
+
runner: deps.runner
|
|
9032
|
+
});
|
|
9033
|
+
try {
|
|
9034
|
+
await registerProfile(c.env.OOMPF_BASE_URL, { source: record.sourceUrl }, deps.httpFetch);
|
|
9035
|
+
} catch (error) {
|
|
9036
|
+
throw new CommandError("index_update_failed", `The Gist was updated but OOMPF could not refresh its index. Re-register the unchanged source URL after the index recovers; this command will not patch the Gist again. Details: ${error instanceof Error ? error.message : String(error)}`);
|
|
9037
|
+
}
|
|
9038
|
+
return c.ok({
|
|
9039
|
+
...output,
|
|
9040
|
+
updatedRevision: updated.revision
|
|
9041
|
+
});
|
|
9042
|
+
} catch (error) {
|
|
9043
|
+
return toCliError(c.error, error);
|
|
9044
|
+
}
|
|
9045
|
+
}
|
|
9046
|
+
});
|
|
9047
|
+
}
|
|
9048
|
+
//#endregion
|
|
8440
9049
|
//#region src/index.ts
|
|
8441
9050
|
/**
|
|
8442
9051
|
* OOMPF CLI entrypoint (binary name: `oompf`).
|
|
8443
9052
|
*
|
|
8444
|
-
* Builds the Incur router CLI, wiring the
|
|
8445
|
-
* `inspect`, `search`) against injectable seams. {@link createCli}
|
|
8446
|
-
* optional {@link CliDeps} bundle so focused tests can drive every
|
|
8447
|
-
* fake `gh`/Gist/HTTP/`omp`/filesystem implementations; production
|
|
8448
|
-
* gets the real Bun/Node-backed seams.
|
|
9053
|
+
* Builds the Incur router CLI, wiring the five commands (`publish`, `add`,
|
|
9054
|
+
* `inspect`, `search`, `upgrade`) against injectable seams. {@link createCli}
|
|
9055
|
+
* takes an optional {@link CliDeps} bundle so focused tests can drive every
|
|
9056
|
+
* command with fake `gh`/Gist/HTTP/`omp`/filesystem implementations; production
|
|
9057
|
+
* omits it and gets the real Bun/Node-backed seams.
|
|
8449
9058
|
*/
|
|
8450
9059
|
/**
|
|
8451
9060
|
* OOMPF CLI version, surfaced by `oompf --version`.
|
|
@@ -8468,6 +9077,7 @@ function createCli(deps = {}) {
|
|
|
8468
9077
|
registerAdd(cli, resolved);
|
|
8469
9078
|
registerInspect(cli, resolved);
|
|
8470
9079
|
registerSearch(cli, resolved);
|
|
9080
|
+
registerUpgrade(cli, resolved);
|
|
8471
9081
|
return cli;
|
|
8472
9082
|
}
|
|
8473
9083
|
if (import.meta.main) await createCli().serve();
|