catherd-cli 0.2.0 → 1.0.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 +114 -30
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/catalog/jev.json +89 -0
- package/catalog/models.json +236 -0
- package/catalog/scores.json +366 -0
- package/package.json +24 -8
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.mcp.json +1 -1
- package/plugin/commands/catherd.md +1 -1
- package/plugin/skills/catherd/SKILL.md +97 -51
- package/plugin/skills/catherd-setup/SKILL.md +34 -24
- package/src/adapters/all.ts +8 -0
- package/src/adapters/backend.ts +144 -0
- package/src/adapters/claude-code/events.ts +86 -0
- package/src/adapters/claude-code/index.ts +223 -0
- package/src/adapters/claude-code/models-api.ts +65 -0
- package/src/adapters/claude-code/models.ts +26 -0
- package/src/adapters/cli.ts +74 -0
- package/src/adapters/codex/events.ts +65 -0
- package/src/adapters/codex/home.ts +47 -0
- package/src/adapters/codex/index.ts +227 -0
- package/src/adapters/discovery.ts +87 -0
- package/src/adapters/opencode/agents.ts +84 -0
- package/src/adapters/opencode/events.ts +109 -0
- package/src/adapters/opencode/index.ts +316 -0
- package/src/adapters/registry.ts +14 -0
- package/src/cli.ts +159 -19
- package/src/domain/agents.ts +60 -0
- package/src/domain/budget.ts +45 -0
- package/src/domain/catalog.ts +262 -0
- package/src/domain/changes.ts +52 -0
- package/src/domain/cost.ts +89 -0
- package/src/domain/errors.ts +58 -0
- package/src/domain/hints.ts +22 -0
- package/src/domain/ids.ts +97 -0
- package/src/domain/jev.ts +283 -0
- package/src/domain/lane.ts +69 -0
- package/src/domain/profile-rules.ts +205 -0
- package/src/domain/profile.ts +383 -0
- package/src/domain/record.ts +84 -0
- package/src/{profile → domain}/role-prompts.ts +42 -25
- package/src/domain/roles.ts +25 -0
- package/src/domain/route.ts +126 -0
- package/src/domain/runtime.ts +21 -0
- package/src/domain/sanitize.ts +29 -0
- package/src/domain/secrets.ts +57 -0
- package/src/domain/select.ts +136 -0
- package/src/domain/state.ts +33 -0
- package/src/domain/util.ts +18 -0
- package/src/entry/capture-fixtures-command.ts +57 -0
- package/src/entry/catalog-command.ts +106 -0
- package/src/entry/cli-kit.ts +36 -0
- package/src/entry/deps.ts +16 -0
- package/src/entry/doctor-command.ts +45 -0
- package/src/entry/glyphs.ts +33 -0
- package/src/entry/init-command.ts +142 -0
- package/src/entry/lock-command.ts +123 -0
- package/src/entry/mcp/dispatch-tools.ts +82 -0
- package/src/entry/mcp/handshake.ts +48 -0
- package/src/entry/mcp/lane-tools.ts +83 -0
- package/src/entry/mcp/result.ts +51 -0
- package/src/entry/mcp/run-tools.ts +144 -0
- package/src/entry/mcp/server.ts +72 -0
- package/src/entry/mcp/setup-tools.ts +81 -0
- package/src/entry/profile-command.ts +320 -0
- package/src/entry/prompt.ts +103 -0
- package/src/entry/runs-command.ts +217 -0
- package/src/entry/supervise-bin.ts +18 -0
- package/src/entry/supervise-command.ts +37 -0
- package/src/entry/tui/commands.ts +545 -0
- package/src/entry/tui/effects.ts +270 -0
- package/src/entry/tui/fixtures.ts +318 -0
- package/src/entry/tui/profile-edits.ts +138 -0
- package/src/entry/tui/profile-tree.ts +419 -0
- package/src/entry/tui/providers/app.tsx +162 -0
- package/src/entry/tui/providers/boundary.tsx +57 -0
- package/src/entry/tui/providers/data.tsx +133 -0
- package/src/entry/tui/providers/keymap.tsx +166 -0
- package/src/entry/tui/providers/root.tsx +45 -0
- package/src/entry/tui/providers/theme.tsx +17 -0
- package/src/entry/tui/providers/toast.tsx +77 -0
- package/src/entry/tui/run.tsx +98 -0
- package/src/entry/tui/state.ts +336 -0
- package/src/entry/tui/stories.ts +75 -0
- package/src/entry/tui/text.ts +118 -0
- package/src/entry/tui/theme.ts +139 -0
- package/src/entry/tui/views/app.tsx +208 -0
- package/src/entry/tui/views/dialogs.tsx +41 -0
- package/src/entry/tui/views/profile-actions.ts +321 -0
- package/src/entry/tui/views/profiles.tsx +440 -0
- package/src/entry/tui/views/runs.tsx +281 -0
- package/src/entry/tui/views/save-dialog.tsx +332 -0
- package/src/entry/tui/views/status.tsx +206 -0
- package/src/entry/tui/widgets/chrome.tsx +114 -0
- package/src/entry/tui/widgets/dialog-confirm.tsx +58 -0
- package/src/entry/tui/widgets/dialog-prompt.tsx +52 -0
- package/src/entry/tui/widgets/dialog-select.tsx +212 -0
- package/src/entry/tui/widgets/dialog.tsx +68 -0
- package/src/entry/tui/widgets/line.tsx +92 -0
- package/src/entry/tui/widgets/list.tsx +196 -0
- package/src/entry/tui/widgets/spinner.tsx +49 -0
- package/src/entry/tui/widgets/toast.tsx +62 -0
- package/src/infra/assets.ts +4 -0
- package/src/infra/dispatch-dir.ts +278 -0
- package/src/infra/env.ts +43 -0
- package/src/infra/filelock.ts +159 -0
- package/src/infra/git.ts +91 -0
- package/src/infra/heavy-lock.ts +36 -0
- package/src/infra/jev-client.ts +162 -0
- package/src/infra/launch.ts +25 -0
- package/src/infra/log.ts +84 -0
- package/src/infra/paths.ts +38 -0
- package/src/infra/proc.ts +110 -0
- package/src/infra/store.ts +229 -0
- package/src/infra/supervisor.ts +257 -0
- package/src/infra/version.ts +6 -0
- package/src/services/admission.ts +277 -0
- package/src/services/agent-links.ts +139 -0
- package/src/services/backends.ts +44 -0
- package/src/services/budget.ts +47 -0
- package/src/services/capture.ts +233 -0
- package/src/services/catalog-service.ts +382 -0
- package/src/services/dispatch-service.ts +479 -0
- package/src/services/dispatches.ts +145 -0
- package/src/services/doctor-backends.ts +185 -0
- package/src/services/doctor-checks.ts +103 -0
- package/src/services/doctor.ts +358 -0
- package/src/services/finalize.ts +359 -0
- package/src/services/jev-service.ts +183 -0
- package/src/services/lane-service.ts +248 -0
- package/src/services/ports.ts +132 -0
- package/src/services/preflight.ts +207 -0
- package/src/services/profile-service.ts +292 -0
- package/src/services/profile-store.ts +141 -0
- package/src/services/reconcile.ts +101 -0
- package/src/services/routing-service.ts +190 -0
- package/src/services/run-debug.ts +79 -0
- package/src/services/run-service.ts +161 -0
- package/src/services/run-store.ts +316 -0
- package/src/services/setup.ts +87 -0
- package/src/services/state.ts +104 -0
- package/src/services/summary.ts +227 -0
- package/catalog/catalog.json +0 -112
- package/catalog/models-dev.json +0 -56453
- package/src/core/codex-events.ts +0 -49
- package/src/core/codex.ts +0 -166
- package/src/core/harness.ts +0 -89
- package/src/core/lanes.ts +0 -67
- package/src/core/lock.ts +0 -111
- package/src/core/opencode.ts +0 -207
- package/src/core/reconcile.ts +0 -26
- package/src/core/reply.ts +0 -54
- package/src/core/runstore.ts +0 -163
- package/src/core/spawn.ts +0 -89
- package/src/core/status.ts +0 -193
- package/src/files.ts +0 -19
- package/src/mcp/dispatch.ts +0 -154
- package/src/mcp/lane-tools.ts +0 -200
- package/src/mcp/out.ts +0 -6
- package/src/mcp/preflight.ts +0 -69
- package/src/mcp/run-tools.ts +0 -121
- package/src/mcp/runs.ts +0 -118
- package/src/mcp/server.ts +0 -26
- package/src/mcp/setup-tools.ts +0 -175
- package/src/paths.ts +0 -20
- package/src/profile/agents.ts +0 -130
- package/src/profile/profile.ts +0 -228
- package/src/routing/catalog.ts +0 -227
- package/src/routing/commands.ts +0 -22
- package/src/routing/jev.ts +0 -185
- package/src/routing/route.ts +0 -155
- package/src/routing/select.ts +0 -90
- package/src/tui/backends.ts +0 -57
- package/src/tui/commands.ts +0 -53
- package/src/tui/dashboard.tsx +0 -245
- package/src/tui/editor.tsx +0 -214
- package/src/tui/init.tsx +0 -208
- package/src/tui/matrix-model.ts +0 -279
- package/src/tui/matrix.tsx +0 -512
- package/src/tui/profiles.ts +0 -38
- package/src/tui/theme.ts +0 -177
- package/src/tui/ui.tsx +0 -267
- package/src/tui/watch-model.ts +0 -83
- package/src/tui/watch.tsx +0 -236
- package/src/types.ts +0 -184
- package/src/version.ts +0 -7
- /package/src/{mcp/commands.ts → entry/mcp/command.ts} +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BackendAdapter, Probe } from "../adapters/backend.ts";
|
|
2
|
+
import { adapterFor } from "../adapters/registry.ts";
|
|
3
|
+
import "../adapters/all.ts";
|
|
4
|
+
import { CatherdError } from "../domain/errors.ts";
|
|
5
|
+
import { formatRung, tryParseRung } from "../domain/ids.ts";
|
|
6
|
+
|
|
7
|
+
const READY_TTL_MS = 10 * 60_000;
|
|
8
|
+
const ready = new Map<string, { at: number; probe: Probe }>();
|
|
9
|
+
|
|
10
|
+
/** Forget every cached probe (tests, and after the user fixes a backend). */
|
|
11
|
+
export const resetReadiness = (): void => ready.clear();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Spec §4.4: the adapter for `backend` once its last probe says it is ready. A ready probe is kept
|
|
15
|
+
* for 10 minutes; a failing one is never kept, so a fixed backend works on the next dispatch.
|
|
16
|
+
*/
|
|
17
|
+
export async function readyAdapter(backend: string): Promise<{ adapter: BackendAdapter; probe: Probe }> {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const adapter = adapterFor(backend);
|
|
20
|
+
if (!adapter)
|
|
21
|
+
throw new CatherdError("E_BACKEND_MISSING", `catherd has no ${backend} adapter yet`, {
|
|
22
|
+
fix: "use a rung on a backend catherd supports; catalog_query lists them",
|
|
23
|
+
});
|
|
24
|
+
const hit = ready.get(backend);
|
|
25
|
+
if (hit && now - hit.at < READY_TTL_MS) return { adapter, probe: hit.probe };
|
|
26
|
+
const probe = await adapter.probe();
|
|
27
|
+
const problem = probe.problems[0];
|
|
28
|
+
if (problem) throw new CatherdError(problem.code, problem.message, { fix: problem.fix });
|
|
29
|
+
ready.set(backend, { at: now, probe });
|
|
30
|
+
return { adapter, probe };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Spec §4.5: a rung's stand-in on a usage limit: the profile's, else its backend's default (for `repo`,
|
|
35
|
+
* when given), else none.
|
|
36
|
+
*/
|
|
37
|
+
export function standInFor(failover: Record<string, string>, rung: string, repo?: string): string | null {
|
|
38
|
+
const own = failover[rung];
|
|
39
|
+
if (own) return own;
|
|
40
|
+
const r = tryParseRung(rung);
|
|
41
|
+
if (!r) return null;
|
|
42
|
+
const byAdapter = adapterFor(r.backend)?.failoverFor?.(r, repo) ?? null;
|
|
43
|
+
return byAdapter && formatRung(byAdapter);
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { adapterFor } from "../adapters/registry.ts";
|
|
3
|
+
import "../adapters/all.ts";
|
|
4
|
+
import { type Budget, type BudgetStatus, budgetStatus, type Spend } from "../domain/budget.ts";
|
|
5
|
+
import type { RunRecord } from "../domain/record.ts";
|
|
6
|
+
import { dispatchPaths } from "../infra/dispatch-dir.ts";
|
|
7
|
+
import { type Dispatch, liveDispatches } from "./dispatches.ts";
|
|
8
|
+
import { readAgentRuns, readRecords, type Run } from "./run-store.ts";
|
|
9
|
+
|
|
10
|
+
/** Tokens a running dispatch has used so far, summed from the events its CLI has written. */
|
|
11
|
+
export function liveTokens(d: Dispatch): number {
|
|
12
|
+
const a = adapterFor(d.admit.backend);
|
|
13
|
+
const file = dispatchPaths(d.dir).events;
|
|
14
|
+
if (!a || !existsSync(file)) return 0;
|
|
15
|
+
let n = 0;
|
|
16
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
17
|
+
if (!line.trim()) continue;
|
|
18
|
+
try {
|
|
19
|
+
const t = a.parse(line).tokens;
|
|
20
|
+
if (t) n += t.input + t.output;
|
|
21
|
+
} catch {
|
|
22
|
+
// a line the adapter cannot read adds nothing
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return n;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Spec §4.6: minutes are wall clock since run_start; tokens and dollars sum every record (every
|
|
30
|
+
* backend), every native subagent run the orchestrator reported, and what live roles used so far.
|
|
31
|
+
*/
|
|
32
|
+
export function spendOf(run: Run, records: RunRecord[], live: Dispatch[], now: number): Spend {
|
|
33
|
+
const agents = readAgentRuns(run);
|
|
34
|
+
const sum = <T>(xs: T[], f: (x: T) => number) => xs.reduce((n, x) => n + f(x), 0);
|
|
35
|
+
return {
|
|
36
|
+
minutes: Math.max(0, (now - Date.parse(run.meta.createdAt)) / 60_000),
|
|
37
|
+
tokens:
|
|
38
|
+
sum(records, (r) => r.tokens.input + r.tokens.output) +
|
|
39
|
+
sum(agents, (a) => a.totalTokens) +
|
|
40
|
+
sum(live, liveTokens),
|
|
41
|
+
usd: sum(records, (r) => r.costUsd ?? 0) + sum(agents, (a) => a.costUsd ?? 0),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function budgetOf(run: Run, budget: Budget, now: number): BudgetStatus | null {
|
|
46
|
+
return budgetStatus(spendOf(run, readRecords(run).records, liveDispatches(run, now), now), budget);
|
|
47
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { BackendAdapter, FinishedRun } from "../adapters/backend.ts";
|
|
5
|
+
import { isCatherdError } from "../domain/errors.ts";
|
|
6
|
+
import { parseRung } from "../domain/ids.ts";
|
|
7
|
+
import type { Access } from "../domain/record.ts";
|
|
8
|
+
import { sanitize, type Scrub } from "../domain/sanitize.ts";
|
|
9
|
+
import { secretEnvValues } from "../domain/secrets.ts";
|
|
10
|
+
import { workerEnv } from "../infra/env.ts";
|
|
11
|
+
import { git } from "../infra/git.ts";
|
|
12
|
+
import { killGroup } from "../infra/proc.ts";
|
|
13
|
+
import { writeJsonAtomic } from "../infra/store.ts";
|
|
14
|
+
import { readyAdapter } from "./backends.ts";
|
|
15
|
+
import { settled } from "./finalize.ts";
|
|
16
|
+
import { collect, DRAIN_MS } from "./preflight.ts";
|
|
17
|
+
|
|
18
|
+
interface CaptureCase {
|
|
19
|
+
backend: string;
|
|
20
|
+
name: string;
|
|
21
|
+
rung: string;
|
|
22
|
+
access: Access;
|
|
23
|
+
brief: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SAY_HELLO =
|
|
27
|
+
"Reply with the single word hello. The last line of your reply is exactly: STATUS: complete — said hello";
|
|
28
|
+
const TRY_WRITE =
|
|
29
|
+
"Create a file named out.txt containing the word hi. If you cannot, say why. The last line of your reply is: STATUS: complete|blocked — <one line why>";
|
|
30
|
+
const HAIKU = "claude-code:claude-haiku-4-5-20251001#default";
|
|
31
|
+
const BUNNY = "opencode:opencode/space-bunny-free#default";
|
|
32
|
+
|
|
33
|
+
/** Spec §11.7–8: one cheap run per backend, plus a read-only role trying to write where enforcement is advisory. */
|
|
34
|
+
const CAPTURE_CASES: CaptureCase[] = [
|
|
35
|
+
{ backend: "codex", name: "ok", rung: "codex:gpt-6-luna#low", access: "read-only", brief: SAY_HELLO },
|
|
36
|
+
{ backend: "claude-code", name: "ok", rung: HAIKU, access: "read-only", brief: SAY_HELLO },
|
|
37
|
+
{ backend: "claude-code", name: "read-only-write", rung: HAIKU, access: "read-only", brief: TRY_WRITE },
|
|
38
|
+
{ backend: "opencode", name: "ok", rung: BUNNY, access: "read-only", brief: SAY_HELLO },
|
|
39
|
+
{ backend: "opencode", name: "read-only-write", rung: BUNNY, access: "read-only", brief: TRY_WRITE },
|
|
40
|
+
];
|
|
41
|
+
export const CAPTURE_BACKENDS = [...new Set(CAPTURE_CASES.map((c) => c.backend))];
|
|
42
|
+
|
|
43
|
+
export type Captured =
|
|
44
|
+
| {
|
|
45
|
+
backend: string;
|
|
46
|
+
name: string;
|
|
47
|
+
status: "captured";
|
|
48
|
+
dir: string;
|
|
49
|
+
cliVersion: string;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
}
|
|
52
|
+
| { backend: string; name: string; status: "skipped"; reason: string };
|
|
53
|
+
|
|
54
|
+
async function scratchRepo(): Promise<{ work: string; repo: string }> {
|
|
55
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "catherd-capture-")));
|
|
56
|
+
const repo = join(work, "repo");
|
|
57
|
+
mkdirSync(repo);
|
|
58
|
+
await git(repo, ["init", "-q"]);
|
|
59
|
+
await git(repo, [
|
|
60
|
+
"-c",
|
|
61
|
+
"user.name=catherd",
|
|
62
|
+
"-c",
|
|
63
|
+
"user.email=capture@catherd.invalid",
|
|
64
|
+
"commit",
|
|
65
|
+
"-q",
|
|
66
|
+
"--allow-empty",
|
|
67
|
+
"-m",
|
|
68
|
+
"init",
|
|
69
|
+
]);
|
|
70
|
+
return { work, repo };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every string in `v`, cleaned: meta values are sanitized before JSON escapes them, not after. */
|
|
74
|
+
function cleanStrings(v: unknown, clean: (s: string) => string): unknown {
|
|
75
|
+
if (typeof v === "string") return clean(v);
|
|
76
|
+
if (Array.isArray(v)) return v.map((x) => cleanStrings(x, clean));
|
|
77
|
+
if (v && typeof v === "object")
|
|
78
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, cleanStrings(x, clean)]));
|
|
79
|
+
return v;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Runs one case the way an isolated dispatch would (prepare, plan, the worker env), without the
|
|
84
|
+
* supervisor. Isolated, so the stream names none of the owner's own hooks, plugins, MCP servers or config:
|
|
85
|
+
* the fixtures are committed.
|
|
86
|
+
*/
|
|
87
|
+
async function captureOne(
|
|
88
|
+
adapter: BackendAdapter,
|
|
89
|
+
version: string,
|
|
90
|
+
c: CaptureCase,
|
|
91
|
+
outDir: string,
|
|
92
|
+
timeoutMs: number,
|
|
93
|
+
): Promise<Captured> {
|
|
94
|
+
const { work, repo } = await scratchRepo();
|
|
95
|
+
try {
|
|
96
|
+
const briefPath = join(work, "brief.md");
|
|
97
|
+
writeFileSync(briefPath, c.brief);
|
|
98
|
+
const rung = parseRung(c.rung);
|
|
99
|
+
await adapter.prepare?.({ rung, access: c.access, isolated: true, repo });
|
|
100
|
+
const request = {
|
|
101
|
+
rung,
|
|
102
|
+
access: c.access,
|
|
103
|
+
thread: null,
|
|
104
|
+
isolated: true,
|
|
105
|
+
repo,
|
|
106
|
+
briefPath,
|
|
107
|
+
replyPath: join(work, "reply.md"),
|
|
108
|
+
dispatchDir: work,
|
|
109
|
+
};
|
|
110
|
+
const plan = adapter.plan(request);
|
|
111
|
+
const startedAtMs = Date.now();
|
|
112
|
+
const p = Bun.spawn([plan.cmd, ...plan.args], {
|
|
113
|
+
cwd: plan.cwd,
|
|
114
|
+
env: workerEnv(process.env, plan.env, plan.cwd),
|
|
115
|
+
stdin: Bun.file(briefPath),
|
|
116
|
+
stdout: "pipe",
|
|
117
|
+
stderr: "pipe",
|
|
118
|
+
// its own group, so a timeout stops whatever the CLI started too, as the supervisor does
|
|
119
|
+
detached: true,
|
|
120
|
+
});
|
|
121
|
+
let timedOut = false;
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
timedOut = true;
|
|
124
|
+
killGroup(p.pid, "SIGKILL");
|
|
125
|
+
}, timeoutMs);
|
|
126
|
+
// as preflight does: a process the CLI left behind (in its own session) may hold the pipes open
|
|
127
|
+
const out = collect(p.stdout);
|
|
128
|
+
const err = collect(p.stderr);
|
|
129
|
+
const code = await p.exited;
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
const drained = await Promise.race([
|
|
132
|
+
Promise.all([out.done, err.done]).then(() => true),
|
|
133
|
+
Bun.sleep(DRAIN_MS).then(() => false),
|
|
134
|
+
]);
|
|
135
|
+
if (!drained) killGroup(p.pid, "SIGKILL"); // leftovers still in the CLI's group
|
|
136
|
+
out.stop();
|
|
137
|
+
err.stop();
|
|
138
|
+
const events = out.text();
|
|
139
|
+
const stderr = err.text();
|
|
140
|
+
const run: FinishedRun = {
|
|
141
|
+
request,
|
|
142
|
+
eventLines: events.split("\n").filter((l) => l.trim()),
|
|
143
|
+
reply: "",
|
|
144
|
+
stderr,
|
|
145
|
+
exit: {
|
|
146
|
+
code: p.signalCode ? null : code,
|
|
147
|
+
signal: p.signalCode ?? null,
|
|
148
|
+
reason: timedOut ? "wall-timeout" : "exited",
|
|
149
|
+
endedAt: new Date().toISOString(),
|
|
150
|
+
},
|
|
151
|
+
startedAtMs,
|
|
152
|
+
};
|
|
153
|
+
// as a dispatch records it: the backend's own totals and limits, on a fresh thread (nothing prior)
|
|
154
|
+
const o = await settled(adapter, adapter.finalize(run), run, () => ({
|
|
155
|
+
tokens: { input: 0, cached: 0, output: 0 },
|
|
156
|
+
costUsd: 0,
|
|
157
|
+
}));
|
|
158
|
+
const scrub: Scrub = {
|
|
159
|
+
secrets: secretEnvValues(process.env),
|
|
160
|
+
paths: [
|
|
161
|
+
{ from: repo, to: "<repo>" },
|
|
162
|
+
{ from: work, to: "<tmp>" },
|
|
163
|
+
{ from: realpathSync(tmpdir()), to: "<tmp>" },
|
|
164
|
+
{ from: homedir(), to: "~" },
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
const clean = (t: string) => sanitize(t, scrub);
|
|
168
|
+
const dir = join(outDir, c.backend, version);
|
|
169
|
+
mkdirSync(dir, { recursive: true });
|
|
170
|
+
writeFileSync(join(dir, `${c.name}.jsonl`), clean(events));
|
|
171
|
+
writeFileSync(join(dir, `${c.name}.stderr`), clean(stderr));
|
|
172
|
+
writeJsonAtomic(
|
|
173
|
+
join(dir, `${c.name}.json`),
|
|
174
|
+
cleanStrings(
|
|
175
|
+
{
|
|
176
|
+
schema: 1,
|
|
177
|
+
backend: c.backend,
|
|
178
|
+
cliVersion: version,
|
|
179
|
+
case: c.name,
|
|
180
|
+
rung: c.rung,
|
|
181
|
+
access: c.access,
|
|
182
|
+
isolated: true,
|
|
183
|
+
brief: c.brief,
|
|
184
|
+
exitCode: run.exit.code,
|
|
185
|
+
reason: run.exit.reason,
|
|
186
|
+
capturedAt: new Date().toISOString(),
|
|
187
|
+
outcome: {
|
|
188
|
+
status: o.status,
|
|
189
|
+
thread: o.thread,
|
|
190
|
+
tokens: o.tokens,
|
|
191
|
+
costUsd: o.costUsd,
|
|
192
|
+
error: o.error,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
clean,
|
|
196
|
+
),
|
|
197
|
+
{ mode: 0o644 },
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
backend: c.backend,
|
|
201
|
+
name: c.name,
|
|
202
|
+
status: "captured",
|
|
203
|
+
dir,
|
|
204
|
+
cliVersion: version,
|
|
205
|
+
exitCode: run.exit.code,
|
|
206
|
+
};
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(work, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Spec §11.8: records the capture cases of every ready backend (or only `backends`) under
|
|
214
|
+
* `<outDir>/<backend>/<cli-version>/`, with secrets and home paths stripped. A backend that is not
|
|
215
|
+
* ready is skipped with the reason.
|
|
216
|
+
*/
|
|
217
|
+
export async function captureFixtures(o: {
|
|
218
|
+
outDir: string;
|
|
219
|
+
backends?: string[];
|
|
220
|
+
timeoutMs?: number;
|
|
221
|
+
}): Promise<Captured[]> {
|
|
222
|
+
const out: Captured[] = [];
|
|
223
|
+
for (const c of CAPTURE_CASES.filter((x) => !o.backends || o.backends.includes(x.backend))) {
|
|
224
|
+
try {
|
|
225
|
+
const { adapter, probe } = await readyAdapter(c.backend);
|
|
226
|
+
out.push(await captureOne(adapter, probe.version ?? "unknown", c, o.outDir, o.timeoutMs ?? 300_000));
|
|
227
|
+
} catch (e) {
|
|
228
|
+
if (!isCatherdError(e)) throw e;
|
|
229
|
+
out.push({ backend: c.backend, name: c.name, status: "skipped", reason: `${e.code}: ${e.message}` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|