@azure-id/orc 0.56.1 → 1.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/CHANGELOG.md +219 -0
- package/README-id.md +49 -107
- package/README.md +694 -719
- package/bin/build-agents.js +18 -11
- package/bin/cli.js +33699 -29855
- package/bin/onboarding-content.js +0 -3
- package/bin/test-run.js +491 -0
- package/bin/verify-contracts.js +4330 -3277
- package/bin/verify-package.js +76 -6
- package/bin/webui/api.js +1201 -1163
- package/bin/webui/app.html +210 -198
- package/bin/webui/css/06-responsive.css +120 -108
- package/bin/webui/css/panels/extra.css +29 -0
- package/bin/webui/css/panels/lanes.css +134 -0
- package/bin/webui/css/panels/settings.css +79 -3
- package/bin/webui/css/panels/wait.css +123 -0
- package/bin/webui/fixtures/extra.js +2036 -1934
- package/bin/webui/fixtures/flow.js +2 -2
- package/bin/webui/fixtures/index.js +510 -486
- package/bin/webui/fixtures/lanes.js +218 -0
- package/bin/webui/fixtures/settings.js +261 -34
- package/bin/webui/fixtures/stats.js +3 -3
- package/bin/webui/fixtures/wait.js +97 -0
- package/bin/webui/i18n/TERMS.md +135 -134
- package/bin/webui/i18n/en/extra.json +345 -313
- package/bin/webui/i18n/en/lanes.json +29 -0
- package/bin/webui/i18n/en/nav.json +21 -19
- package/bin/webui/i18n/en/overview.json +2 -0
- package/bin/webui/i18n/en/settings.json +16 -5
- package/bin/webui/i18n/en/wait.json +41 -0
- package/bin/webui/i18n/id/extra.json +345 -313
- package/bin/webui/i18n/id/lanes.json +29 -0
- package/bin/webui/i18n/id/nav.json +21 -19
- package/bin/webui/i18n/id/overview.json +2 -0
- package/bin/webui/i18n/id/settings.json +15 -4
- package/bin/webui/i18n/id/wait.json +41 -0
- package/bin/webui/js/01-i18n.js +151 -149
- package/bin/webui/js/panels/extra.js +3199 -3011
- package/bin/webui/js/panels/lanes.js +259 -0
- package/bin/webui/js/panels/overview.js +10 -0
- package/bin/webui/js/panels/settings.js +149 -3
- package/bin/webui/js/panels/wait.js +253 -0
- package/package.json +39 -39
- package/templates/agents/MODEL-MAPPING.md +23 -35
- package/templates/agents/orc-executor-opus-4-7-high.md +1 -1
- package/templates/agents/orc-executor-opus-4-7-med.md +1 -1
- package/templates/agents/orc-executor-opus-4-8-high.md +1 -1
- package/templates/agents/orc-executor-opus-5-high.md +1 -1
- package/templates/agents/orc-executor-opus-5-low.md +1 -1
- package/templates/agents/orc-executor-opus-5-med.md +1 -1
- package/templates/agents/orc-retro-opus-5-med.md +73 -73
- package/templates/agents/orc-retro-sonnet-5-high.md +72 -72
- package/templates/agents/orc-trace-writer-haiku-4-5.md +107 -107
- package/templates/commands/orc-route.md +1 -1
- package/templates/commands/orc-wait.md +19 -0
- package/templates/commands/orc.md +1 -1
- package/templates/hooks/orc-statusline.js +39 -0
- package/templates/skills/_shared/README.md +7 -3
- package/templates/skills/_shared/config-precedence.md +198 -0
- package/templates/skills/_shared/drift-recovery.md +1 -1
- package/templates/skills/_shared/extra-dispatch.md +159 -7
- package/templates/skills/_shared/interview.md +1 -1
- package/templates/skills/_shared/opus5-only.md +9 -7
- package/templates/skills/_shared/phases/README.md +82 -0
- package/templates/skills/{orc/references → _shared/phases}/analyst-gates.md +15 -3
- package/templates/skills/_shared/phases/execution.md +143 -0
- package/templates/skills/{orc/references → _shared/phases}/house-rules.md +13 -1
- package/templates/skills/{orc/references → _shared/phases}/intake.md +15 -2
- package/templates/skills/_shared/phases/mock-example.md +56 -0
- package/templates/skills/{orc/references → _shared/phases}/plan-handoff.md +16 -3
- package/templates/skills/_shared/phases/planning.md +135 -0
- package/templates/skills/_shared/phases/preflight.md +98 -0
- package/templates/skills/_shared/phases/review.md +53 -0
- package/templates/skills/_shared/phases/scoring.md +65 -0
- package/templates/skills/{orc/references → _shared/phases}/security-checklist.md +12 -1
- package/templates/skills/_shared/phases/security.md +55 -0
- package/templates/skills/_shared/phases/ship.md +88 -0
- package/templates/skills/{orc/references/stop-and-resume.md → _shared/phases/stop-resume.md} +169 -160
- package/templates/skills/_shared/phases/summary.md +48 -0
- package/templates/skills/_shared/phases/testgen.md +51 -0
- package/templates/skills/{orc/references/trace-protocol.md → _shared/phases/trace.md} +49 -6
- package/templates/skills/_shared/phases/verify.md +70 -0
- package/templates/skills/{orc/references → _shared/phases}/wave-grouping.md +14 -2
- package/templates/skills/{orc/references → _shared/phases}/wiki-consult.md +13 -1
- package/templates/skills/_shared/read-ladder.md +48 -0
- package/templates/skills/_shared/wait.md +240 -0
- package/templates/skills/context-combiner/SKILL.md +214 -187
- package/templates/skills/orc/README.md +148 -150
- package/templates/skills/orc/SKILL.md +75 -358
- package/templates/skills/orc/config.md +137 -389
- package/templates/skills/orc/examples/full-run-mock.md +1 -1
- package/templates/skills/orc/references/effort-and-mode.md +16 -18
- package/templates/skills/orc/references/phases/intake.md +47 -0
- package/templates/skills/orc/references/phases/integration.md +19 -0
- package/templates/skills/orc/references/preflight-report.md +4 -4
- package/templates/skills/orc/references/ultra-mode.md +117 -123
- package/templates/skills/orc/schemas/planning-output.md +279 -279
- package/templates/skills/orc/subskills/orc-planner/SKILL.md +260 -264
- package/templates/skills/orc/subskills/orc-pr/stack-gate.md +4 -1
- package/templates/skills/orc-aftermath/SKILL.md +158 -136
- package/templates/skills/orc-analyze/SKILL.md +252 -220
- package/templates/skills/orc-analyze/references/branching.md +51 -51
- package/templates/skills/orc-analyze/references/thin-input.md +1 -1
- package/templates/skills/orc-analyze-mini/SKILL.md +128 -112
- package/templates/skills/orc-boundary/SKILL.md +249 -208
- package/templates/skills/orc-brainstorm/SKILL.md +377 -339
- package/templates/skills/orc-budget/SKILL.md +248 -225
- package/templates/skills/orc-challenge/README.md +1 -1
- package/templates/skills/orc-challenge/SKILL.md +298 -255
- package/templates/skills/orc-challenge/examples/council-full-roster.md +1 -1
- package/templates/skills/orc-claude/SKILL.md +221 -199
- package/templates/skills/orc-diy/README.md +2 -1
- package/templates/skills/orc-diy/SKILL.md +132 -68
- package/templates/skills/orc-diy/references/blocks/analyze.md +1 -1
- package/templates/skills/orc-diy/references/compile.md +21 -7
- package/templates/skills/orc-diy/references/flow-schema.md +2 -2
- package/templates/skills/orc-doc/SKILL.md +49 -7
- package/templates/skills/orc-explain/SKILL.md +24 -0
- package/templates/skills/orc-export/SKILL.md +31 -5
- package/templates/skills/orc-fast/SKILL.md +218 -185
- package/templates/skills/orc-grill/SKILL.md +238 -207
- package/templates/skills/orc-handoff/SKILL.md +223 -204
- package/templates/skills/orc-learn/SKILL.md +181 -156
- package/templates/skills/orc-mini/SKILL.md +47 -24
- package/templates/skills/orc-pact/SKILL.md +255 -218
- package/templates/skills/orc-pattern/SKILL.md +154 -124
- package/templates/skills/orc-poly/SKILL.md +242 -216
- package/templates/skills/orc-poly/references/gather.md +1 -1
- package/templates/skills/orc-pr-driver/SKILL.md +196 -167
- package/templates/skills/orc-pr-driver/references/orc-run-split.md +1 -1
- package/templates/skills/orc-pr-setup/SKILL.md +212 -184
- package/templates/skills/orc-quick/README.md +2 -2
- package/templates/skills/orc-quick/SKILL.md +51 -13
- package/templates/skills/orc-quick/references/dispatch-gate.md +2 -3
- package/templates/skills/orc-retro/SKILL.md +249 -222
- package/templates/skills/orc-retro/examples/retro-mock.md +171 -171
- package/templates/skills/orc-route/SKILL.md +188 -165
- package/templates/skills/orc-verify/SKILL.md +103 -84
- package/templates/skills/orc-wait/SKILL.md +163 -0
- package/templates/skills/orc-wiki/SKILL.md +60 -206
- package/templates/skills/orc-wiki/references/phases/phase-0.md +71 -0
- package/templates/skills/orc-wiki/references/phases/phase-1.md +35 -0
- package/templates/skills/orc-wiki/references/phases/phase-2.md +52 -0
- package/templates/skills/orc-wiki/references/phases/phase-3.md +57 -0
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +36 -0
- package/templates/agents/orc-advisor-fable-5.md +0 -50
- package/templates/agents/orc-analyst-fable-5.md +0 -115
- package/templates/agents/orc-judge-fable-5.md +0 -79
- package/templates/agents/orc-planner-fable-5.md +0 -152
- package/templates/agents/orc-reviewer-fable-5.md +0 -57
- package/templates/skills/_shared/fable5-override.md +0 -56
- package/templates/skills/orc-diy/references/blocks/execution.md +0 -42
- package/templates/skills/orc-diy/references/blocks/mock-example.md +0 -22
- package/templates/skills/orc-diy/references/blocks/planning.md +0 -34
- package/templates/skills/orc-diy/references/blocks/review.md +0 -18
- package/templates/skills/orc-diy/references/blocks/scoring.md +0 -16
- package/templates/skills/orc-diy/references/blocks/security.md +0 -25
- package/templates/skills/orc-diy/references/blocks/ship.md +0 -25
- package/templates/skills/orc-diy/references/blocks/summary.md +0 -18
- package/templates/skills/orc-diy/references/blocks/testgen.md +0 -17
- package/templates/skills/orc-diy/references/blocks/trace.md +0 -28
- package/templates/skills/orc-diy/references/blocks/verify.md +0 -25
package/bin/webui/api.js
CHANGED
|
@@ -1,1163 +1,1201 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* api.js — the /api router for `orc ui`.
|
|
4
|
-
*
|
|
5
|
-
* THE ONE ARCHITECTURAL RULE: this file never re-implements CLI logic. Every
|
|
6
|
-
* endpoint spawns `node bin/cli.js <cmd> --json` and forwards the parsed
|
|
7
|
-
* object. That makes UI/CLI drift structurally impossible — the UI *is* the
|
|
8
|
-
* CLI — and it means every write inherits the CLI's validators, the LEGACY_KEYS
|
|
9
|
-
* aliasing
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* The alternative (requiring cli.js as a library) is off the table: cli.js ends
|
|
13
|
-
* with a bare IIFE, has no require.main guard, prints and process.exit()s
|
|
14
|
-
* directly, and is pinned by contract tokens with binFiles: ["bin/cli.js"].
|
|
15
|
-
*
|
|
16
|
-
* It also never RUNS a lane and never calls a model API. Since v0.43.4 there is
|
|
17
|
-
* exactly one deliberate exception to "never spawns claude": the Experiment
|
|
18
|
-
* panel can open a TERMINAL with `claude` in it and then forget about it (see
|
|
19
|
-
* `launchClaude`). No model output ever flows back through this server, no
|
|
20
|
-
* session is proxied, no API key is held — the panel is still not an AI client.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const { spawn, spawnSync } = require("child_process");
|
|
24
|
-
const fs = require("fs");
|
|
25
|
-
const os = require("os");
|
|
26
|
-
const path = require("path");
|
|
27
|
-
const fixtures = require("./fixtures/index.js");
|
|
28
|
-
|
|
29
|
-
const CLI = path.join(__dirname, "..", "cli.js");
|
|
30
|
-
|
|
31
|
-
// A single-user localhost panel re-reads the same command several times while
|
|
32
|
-
// one page renders. A short TTL collapses that without ever showing stale data
|
|
33
|
-
// across a user action: every mutation clears the cache outright.
|
|
34
|
-
const READ_TTL_MS = 2500;
|
|
35
|
-
const chr10 = String.fromCharCode(10);
|
|
36
|
-
const CLI_TIMEOUT_MS = 30_000;
|
|
37
|
-
|
|
38
|
-
const cache = new Map();
|
|
39
|
-
|
|
40
|
-
function cacheKey(argv) {
|
|
41
|
-
return argv.join("\u0000");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function clearCache() {
|
|
45
|
-
cache.clear();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ── running the CLI ─────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
// Several commands use a NON-ZERO exit as a normal answer, not a failure:
|
|
51
|
-
// `pattern status` (1 = absent, 2 = unknown key), `gotcha list` (1 = none),
|
|
52
|
-
// `wiki impact` (2 = delta, 3 = full), `pr stack status` (1 = not ready),
|
|
53
|
-
// `doctor` (1 = issues found), `resume` (1 = nothing waiting). So the exit code
|
|
54
|
-
// is DATA here, never an error condition — a run counts as failed only when it
|
|
55
|
-
// produced no parseable object.
|
|
56
|
-
// `input` (v0.50.0) is stdin for the child, and it exists for exactly one
|
|
57
|
-
// caller: the connection test, which takes a pasted API key on line 1 and an
|
|
58
|
-
// optional passphrase on line 2. It is a parameter rather than a second spawn
|
|
59
|
-
// helper because a secret must travel the SAME path everything else does —
|
|
60
|
-
// never argv (world-readable in a process list), never a temp file, never a log
|
|
61
|
-
// line. Nothing here ever echoes it back, and `command` below is built from
|
|
62
|
-
// argv alone.
|
|
63
|
-
function runCli(argv, ctx, { json = false, input = undefined } = {}) {
|
|
64
|
-
const args = [...argv];
|
|
65
|
-
if (json) args.push("--json");
|
|
66
|
-
// Always target the project explicitly: the server's cwd is not a reliable
|
|
67
|
-
// way to reach the same .claude the launching command resolved.
|
|
68
|
-
if (ctx.projectRoot) args.push("--dir", ctx.projectRoot);
|
|
69
|
-
// ORC_NO_UPDATE_CHECK exists here to protect the --json contract: most
|
|
70
|
-
// commands end with `maybeNudge()`, which prints an "update available" line to
|
|
71
|
-
// STDOUT and would sit beside the object this parses.
|
|
72
|
-
//
|
|
73
|
-
// `version` and `changelog` are the exceptions, and forcing the flag on them
|
|
74
|
-
// was a real bug: neither nudges, and for both the check IS the payload — so
|
|
75
|
-
// the panel asked whether an update existed with the check switched off and
|
|
76
|
-
// was told `check_disabled: true` forever. A blanket env var silenced the one
|
|
77
|
-
// command whose entire job is to answer that question.
|
|
78
|
-
const CHECKS_UPDATES = argv[0] === "version" || argv[0] === "changelog";
|
|
79
|
-
const env = { ...process.env, NO_COLOR: "1" };
|
|
80
|
-
if (!CHECKS_UPDATES) env.ORC_NO_UPDATE_CHECK = "1";
|
|
81
|
-
const r = spawnSync(process.execPath, [CLI, ...args], {
|
|
82
|
-
encoding: "utf8",
|
|
83
|
-
timeout: CLI_TIMEOUT_MS,
|
|
84
|
-
windowsHide: true,
|
|
85
|
-
env,
|
|
86
|
-
input,
|
|
87
|
-
});
|
|
88
|
-
const stdout = r.stdout || "";
|
|
89
|
-
let data = null;
|
|
90
|
-
try {
|
|
91
|
-
data = JSON.parse(stdout);
|
|
92
|
-
} catch (_) {}
|
|
93
|
-
return {
|
|
94
|
-
ok: data !== null,
|
|
95
|
-
exit_code: r.status === null ? -1 : r.status,
|
|
96
|
-
data,
|
|
97
|
-
stderr: (r.stderr || "").trim(),
|
|
98
|
-
stdout: data === null ? stdout.trim() : "",
|
|
99
|
-
command: "orc " + argv.join(" "),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// The one-line reason a read failed, in the CLI's own words. `crashed` is the
|
|
104
|
-
// envelope the CLI itself emits when a `--json` route throws (v0.49.2); anything
|
|
105
|
-
// else is a command that wrote nothing parseable at all.
|
|
106
|
-
function readFailReason(out) {
|
|
107
|
-
const first = (out.stderr || out.stdout || "")
|
|
108
|
-
.split(chr10)
|
|
109
|
-
.map((l) => l.trim())
|
|
110
|
-
.filter(Boolean)[0];
|
|
111
|
-
const tail = first ? " \u2014 " + first : "";
|
|
112
|
-
return `${out.command} produced no JSON (exit ${out.exit_code})${tail}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function readCli(argv, ctx) {
|
|
116
|
-
const key = cacheKey([ctx.projectRoot || "", ...argv]);
|
|
117
|
-
const hit = cache.get(key);
|
|
118
|
-
if (hit && Date.now() - hit.at < READ_TTL_MS) return hit.value;
|
|
119
|
-
const value = runCli(argv, ctx, { json: true });
|
|
120
|
-
cache.set(key, { at: Date.now(), value });
|
|
121
|
-
return value;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── jobs (the mutation half) ────────────────────────────────────────────────
|
|
125
|
-
// SINGLE-FLIGHT: one mutation at a time, process-wide. The client goes
|
|
126
|
-
// read-only while a job runs, so a second one is a bug, not a race to win.
|
|
127
|
-
// Output accumulates in memory and the client polls /api/job — which is what
|
|
128
|
-
// makes `orc update` and `orc upgrade` show their real output as it happens
|
|
129
|
-
// instead of a spinner and a verdict.
|
|
130
|
-
|
|
131
|
-
let job = null;
|
|
132
|
-
let jobSeq = 0;
|
|
133
|
-
|
|
134
|
-
function jobView() {
|
|
135
|
-
if (!job) return { id: null, running: false };
|
|
136
|
-
return {
|
|
137
|
-
id: job.id,
|
|
138
|
-
command: job.command,
|
|
139
|
-
running: job.running,
|
|
140
|
-
exit_code: job.exit_code,
|
|
141
|
-
output: job.output,
|
|
142
|
-
started_ms: job.started_ms,
|
|
143
|
-
// THE PANEL IS SERVING CODE THAT THIS JOB MAY HAVE JUST REPLACED. Reported
|
|
144
|
-
// only on a job that SUCCEEDED and whose action is declared as touching the
|
|
145
|
-
// install — a failed upgrade changed nothing, so restarting after one would
|
|
146
|
-
// be motion with no reason. The client acts on it; the server does not
|
|
147
|
-
// restart itself, so the job's output survives long enough to be read.
|
|
148
|
-
restart_pending: !!(job.restart_ui && !job.running && job.exit_code === 0),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function startJob(argv, ctx, opts) {
|
|
153
|
-
if (job && job.running) return { error: "busy", job: jobView() };
|
|
154
|
-
const args = [...argv];
|
|
155
|
-
if (ctx.projectRoot) args.push("--dir", ctx.projectRoot);
|
|
156
|
-
const id = ++jobSeq;
|
|
157
|
-
job = {
|
|
158
|
-
id,
|
|
159
|
-
command: "orc " + argv.join(" "),
|
|
160
|
-
running: true,
|
|
161
|
-
exit_code: null,
|
|
162
|
-
output: "",
|
|
163
|
-
started_ms: Date.now(),
|
|
164
|
-
restart_ui: !!(opts && opts.restartUi),
|
|
165
|
-
};
|
|
166
|
-
const child = spawn(process.execPath, [CLI, ...args], {
|
|
167
|
-
windowsHide: true,
|
|
168
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
169
|
-
});
|
|
170
|
-
const append = (buf) => {
|
|
171
|
-
job.output += buf.toString("utf8");
|
|
172
|
-
// A runaway job must not grow the server's heap without bound.
|
|
173
|
-
if (job.output.length > 400_000) job.output = job.output.slice(-400_000);
|
|
174
|
-
};
|
|
175
|
-
child.stdout.on("data", append);
|
|
176
|
-
child.stderr.on("data", append);
|
|
177
|
-
child.on("error", (e) => {
|
|
178
|
-
job.output += "\n" + String(e.message);
|
|
179
|
-
job.exit_code = -1;
|
|
180
|
-
job.running = false;
|
|
181
|
-
});
|
|
182
|
-
child.on("close", (code) => {
|
|
183
|
-
job.exit_code = code === null ? -1 : code;
|
|
184
|
-
job.running = false;
|
|
185
|
-
clearCache(); // every panel refetches against the new truth
|
|
186
|
-
});
|
|
187
|
-
return { job: jobView() };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ── the endpoint table ──────────────────────────────────────────────────────
|
|
191
|
-
// READS map a route to CLI argv. WRITES are separate and POST-only — a GET can
|
|
192
|
-
// never mutate, so a prefetch, a bookmark or a browser retry is always safe.
|
|
193
|
-
|
|
194
|
-
const READS = {
|
|
195
|
-
"/api/version": () => ["version"],
|
|
196
|
-
"/api/changelog": () => ["changelog"],
|
|
197
|
-
"/api/where": () => ["where"],
|
|
198
|
-
"/api/doctor": () => ["doctor"],
|
|
199
|
-
"/api/config": () => ["config", "list"],
|
|
200
|
-
"/api/config/profiles": () => ["config", "profile"],
|
|
201
|
-
"/api/config/recommend": () => ["config", "recommend"],
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
"/api/
|
|
211
|
-
"/api/
|
|
212
|
-
"/api/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
// /
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"/api/wiki/
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
"/api/
|
|
230
|
-
"/api/
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
"/api/
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
"/api/
|
|
247
|
-
"/api/
|
|
248
|
-
"/api/
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"/api/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
"/api/
|
|
279
|
-
"/api/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"/api/doc/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"/api/
|
|
323
|
-
"/api/
|
|
324
|
-
"/api/
|
|
325
|
-
"/api/extra
|
|
326
|
-
// v0.
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"/api/
|
|
332
|
-
"/api/
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
"/api/extra/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
"/api/extra/
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
"/api/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"/api/
|
|
364
|
-
"/api/
|
|
365
|
-
"/api/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"/api/
|
|
380
|
-
"/api/
|
|
381
|
-
"/api/
|
|
382
|
-
"/api/
|
|
383
|
-
"/api/diy
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
"/api/
|
|
406
|
-
"/api/
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
],
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
"/api/
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"/api/
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
"/api/doc/
|
|
462
|
-
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
"/api/doc/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"/api/
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"/api/
|
|
503
|
-
// v0.
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// the
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (b.
|
|
521
|
-
if (b.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
"/api/extra/
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
"/api/
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (b.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
},
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
"
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (route === "/api/
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
});
|
|
1092
|
-
clearCache();
|
|
1093
|
-
return json(res, out.ok ? 200 : 500, out.ok
|
|
1094
|
-
? { ok: true, exit_code: out.exit_code, data: out.data, command: out.command }
|
|
1095
|
-
: { ...out, error: readFailReason(out) });
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
//
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* api.js — the /api router for `orc ui`.
|
|
4
|
+
*
|
|
5
|
+
* THE ONE ARCHITECTURAL RULE: this file never re-implements CLI logic. Every
|
|
6
|
+
* endpoint spawns `node bin/cli.js <cmd> --json` and forwards the parsed
|
|
7
|
+
* object. That makes UI/CLI drift structurally impossible — the UI *is* the
|
|
8
|
+
* CLI — and it means every write inherits the CLI's validators, the LEGACY_KEYS
|
|
9
|
+
* aliasing and the shadowing announcements for free, with zero duplicated
|
|
10
|
+
* logic.
|
|
11
|
+
*
|
|
12
|
+
* The alternative (requiring cli.js as a library) is off the table: cli.js ends
|
|
13
|
+
* with a bare IIFE, has no require.main guard, prints and process.exit()s
|
|
14
|
+
* directly, and is pinned by contract tokens with binFiles: ["bin/cli.js"].
|
|
15
|
+
*
|
|
16
|
+
* It also never RUNS a lane and never calls a model API. Since v0.43.4 there is
|
|
17
|
+
* exactly one deliberate exception to "never spawns claude": the Experiment
|
|
18
|
+
* panel can open a TERMINAL with `claude` in it and then forget about it (see
|
|
19
|
+
* `launchClaude`). No model output ever flows back through this server, no
|
|
20
|
+
* session is proxied, no API key is held — the panel is still not an AI client.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { spawn, spawnSync } = require("child_process");
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const os = require("os");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const fixtures = require("./fixtures/index.js");
|
|
28
|
+
|
|
29
|
+
const CLI = path.join(__dirname, "..", "cli.js");
|
|
30
|
+
|
|
31
|
+
// A single-user localhost panel re-reads the same command several times while
|
|
32
|
+
// one page renders. A short TTL collapses that without ever showing stale data
|
|
33
|
+
// across a user action: every mutation clears the cache outright.
|
|
34
|
+
const READ_TTL_MS = 2500;
|
|
35
|
+
const chr10 = String.fromCharCode(10);
|
|
36
|
+
const CLI_TIMEOUT_MS = 30_000;
|
|
37
|
+
|
|
38
|
+
const cache = new Map();
|
|
39
|
+
|
|
40
|
+
function cacheKey(argv) {
|
|
41
|
+
return argv.join("\u0000");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clearCache() {
|
|
45
|
+
cache.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── running the CLI ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
// Several commands use a NON-ZERO exit as a normal answer, not a failure:
|
|
51
|
+
// `pattern status` (1 = absent, 2 = unknown key), `gotcha list` (1 = none),
|
|
52
|
+
// `wiki impact` (2 = delta, 3 = full), `pr stack status` (1 = not ready),
|
|
53
|
+
// `doctor` (1 = issues found), `resume` (1 = nothing waiting). So the exit code
|
|
54
|
+
// is DATA here, never an error condition — a run counts as failed only when it
|
|
55
|
+
// produced no parseable object.
|
|
56
|
+
// `input` (v0.50.0) is stdin for the child, and it exists for exactly one
|
|
57
|
+
// caller: the connection test, which takes a pasted API key on line 1 and an
|
|
58
|
+
// optional passphrase on line 2. It is a parameter rather than a second spawn
|
|
59
|
+
// helper because a secret must travel the SAME path everything else does —
|
|
60
|
+
// never argv (world-readable in a process list), never a temp file, never a log
|
|
61
|
+
// line. Nothing here ever echoes it back, and `command` below is built from
|
|
62
|
+
// argv alone.
|
|
63
|
+
function runCli(argv, ctx, { json = false, input = undefined } = {}) {
|
|
64
|
+
const args = [...argv];
|
|
65
|
+
if (json) args.push("--json");
|
|
66
|
+
// Always target the project explicitly: the server's cwd is not a reliable
|
|
67
|
+
// way to reach the same .claude the launching command resolved.
|
|
68
|
+
if (ctx.projectRoot) args.push("--dir", ctx.projectRoot);
|
|
69
|
+
// ORC_NO_UPDATE_CHECK exists here to protect the --json contract: most
|
|
70
|
+
// commands end with `maybeNudge()`, which prints an "update available" line to
|
|
71
|
+
// STDOUT and would sit beside the object this parses.
|
|
72
|
+
//
|
|
73
|
+
// `version` and `changelog` are the exceptions, and forcing the flag on them
|
|
74
|
+
// was a real bug: neither nudges, and for both the check IS the payload — so
|
|
75
|
+
// the panel asked whether an update existed with the check switched off and
|
|
76
|
+
// was told `check_disabled: true` forever. A blanket env var silenced the one
|
|
77
|
+
// command whose entire job is to answer that question.
|
|
78
|
+
const CHECKS_UPDATES = argv[0] === "version" || argv[0] === "changelog";
|
|
79
|
+
const env = { ...process.env, NO_COLOR: "1" };
|
|
80
|
+
if (!CHECKS_UPDATES) env.ORC_NO_UPDATE_CHECK = "1";
|
|
81
|
+
const r = spawnSync(process.execPath, [CLI, ...args], {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout: CLI_TIMEOUT_MS,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
env,
|
|
86
|
+
input,
|
|
87
|
+
});
|
|
88
|
+
const stdout = r.stdout || "";
|
|
89
|
+
let data = null;
|
|
90
|
+
try {
|
|
91
|
+
data = JSON.parse(stdout);
|
|
92
|
+
} catch (_) {}
|
|
93
|
+
return {
|
|
94
|
+
ok: data !== null,
|
|
95
|
+
exit_code: r.status === null ? -1 : r.status,
|
|
96
|
+
data,
|
|
97
|
+
stderr: (r.stderr || "").trim(),
|
|
98
|
+
stdout: data === null ? stdout.trim() : "",
|
|
99
|
+
command: "orc " + argv.join(" "),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The one-line reason a read failed, in the CLI's own words. `crashed` is the
|
|
104
|
+
// envelope the CLI itself emits when a `--json` route throws (v0.49.2); anything
|
|
105
|
+
// else is a command that wrote nothing parseable at all.
|
|
106
|
+
function readFailReason(out) {
|
|
107
|
+
const first = (out.stderr || out.stdout || "")
|
|
108
|
+
.split(chr10)
|
|
109
|
+
.map((l) => l.trim())
|
|
110
|
+
.filter(Boolean)[0];
|
|
111
|
+
const tail = first ? " \u2014 " + first : "";
|
|
112
|
+
return `${out.command} produced no JSON (exit ${out.exit_code})${tail}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readCli(argv, ctx) {
|
|
116
|
+
const key = cacheKey([ctx.projectRoot || "", ...argv]);
|
|
117
|
+
const hit = cache.get(key);
|
|
118
|
+
if (hit && Date.now() - hit.at < READ_TTL_MS) return hit.value;
|
|
119
|
+
const value = runCli(argv, ctx, { json: true });
|
|
120
|
+
cache.set(key, { at: Date.now(), value });
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── jobs (the mutation half) ────────────────────────────────────────────────
|
|
125
|
+
// SINGLE-FLIGHT: one mutation at a time, process-wide. The client goes
|
|
126
|
+
// read-only while a job runs, so a second one is a bug, not a race to win.
|
|
127
|
+
// Output accumulates in memory and the client polls /api/job — which is what
|
|
128
|
+
// makes `orc update` and `orc upgrade` show their real output as it happens
|
|
129
|
+
// instead of a spinner and a verdict.
|
|
130
|
+
|
|
131
|
+
let job = null;
|
|
132
|
+
let jobSeq = 0;
|
|
133
|
+
|
|
134
|
+
function jobView() {
|
|
135
|
+
if (!job) return { id: null, running: false };
|
|
136
|
+
return {
|
|
137
|
+
id: job.id,
|
|
138
|
+
command: job.command,
|
|
139
|
+
running: job.running,
|
|
140
|
+
exit_code: job.exit_code,
|
|
141
|
+
output: job.output,
|
|
142
|
+
started_ms: job.started_ms,
|
|
143
|
+
// THE PANEL IS SERVING CODE THAT THIS JOB MAY HAVE JUST REPLACED. Reported
|
|
144
|
+
// only on a job that SUCCEEDED and whose action is declared as touching the
|
|
145
|
+
// install — a failed upgrade changed nothing, so restarting after one would
|
|
146
|
+
// be motion with no reason. The client acts on it; the server does not
|
|
147
|
+
// restart itself, so the job's output survives long enough to be read.
|
|
148
|
+
restart_pending: !!(job.restart_ui && !job.running && job.exit_code === 0),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function startJob(argv, ctx, opts) {
|
|
153
|
+
if (job && job.running) return { error: "busy", job: jobView() };
|
|
154
|
+
const args = [...argv];
|
|
155
|
+
if (ctx.projectRoot) args.push("--dir", ctx.projectRoot);
|
|
156
|
+
const id = ++jobSeq;
|
|
157
|
+
job = {
|
|
158
|
+
id,
|
|
159
|
+
command: "orc " + argv.join(" "),
|
|
160
|
+
running: true,
|
|
161
|
+
exit_code: null,
|
|
162
|
+
output: "",
|
|
163
|
+
started_ms: Date.now(),
|
|
164
|
+
restart_ui: !!(opts && opts.restartUi),
|
|
165
|
+
};
|
|
166
|
+
const child = spawn(process.execPath, [CLI, ...args], {
|
|
167
|
+
windowsHide: true,
|
|
168
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
169
|
+
});
|
|
170
|
+
const append = (buf) => {
|
|
171
|
+
job.output += buf.toString("utf8");
|
|
172
|
+
// A runaway job must not grow the server's heap without bound.
|
|
173
|
+
if (job.output.length > 400_000) job.output = job.output.slice(-400_000);
|
|
174
|
+
};
|
|
175
|
+
child.stdout.on("data", append);
|
|
176
|
+
child.stderr.on("data", append);
|
|
177
|
+
child.on("error", (e) => {
|
|
178
|
+
job.output += "\n" + String(e.message);
|
|
179
|
+
job.exit_code = -1;
|
|
180
|
+
job.running = false;
|
|
181
|
+
});
|
|
182
|
+
child.on("close", (code) => {
|
|
183
|
+
job.exit_code = code === null ? -1 : code;
|
|
184
|
+
job.running = false;
|
|
185
|
+
clearCache(); // every panel refetches against the new truth
|
|
186
|
+
});
|
|
187
|
+
return { job: jobView() };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── the endpoint table ──────────────────────────────────────────────────────
|
|
191
|
+
// READS map a route to CLI argv. WRITES are separate and POST-only — a GET can
|
|
192
|
+
// never mutate, so a prefetch, a bookmark or a browser retry is always safe.
|
|
193
|
+
|
|
194
|
+
const READS = {
|
|
195
|
+
"/api/version": () => ["version"],
|
|
196
|
+
"/api/changelog": () => ["changelog"],
|
|
197
|
+
"/api/where": () => ["where"],
|
|
198
|
+
"/api/doctor": () => ["doctor"],
|
|
199
|
+
"/api/config": () => ["config", "list"],
|
|
200
|
+
"/api/config/profiles": () => ["config", "profile"],
|
|
201
|
+
"/api/config/recommend": () => ["config", "recommend"],
|
|
202
|
+
// v1.0.0 W16 — the `orc lane` noun, rendered. All three are READS and all
|
|
203
|
+
// three are answers the CLI already computes in full: which lanes exist and
|
|
204
|
+
// how many keys each reads, which SHARED phases a lane runs and in what
|
|
205
|
+
// order, and the whole call catalogue. The panel draws them and decides
|
|
206
|
+
// nothing about them — the Flow-stepper rule, applied to the lane model.
|
|
207
|
+
// `lane phases` and `lane config` both exit 2 on an unknown lane, which is
|
|
208
|
+
// DATA here exactly like `pattern status` and `wiki impact` above.
|
|
209
|
+
"/api/lanes": () => ["lane", "list"],
|
|
210
|
+
"/api/lane/phases": (q) => ["lane", "phases", String(q.lane || "")],
|
|
211
|
+
"/api/lane/calls": (q) => (q.lane ? ["lane", "calls", String(q.lane)] : ["lane", "calls", "--all"]),
|
|
212
|
+
"/api/runs": (q) => ["run", "list", "--limit", String(Math.min(200, Number(q.limit) || 40))],
|
|
213
|
+
"/api/run": (q) => ["run", "show", String(q.slug || "")],
|
|
214
|
+
"/api/wiki": () => ["wiki", "status"],
|
|
215
|
+
"/api/wiki/impact": () => ["wiki", "impact"],
|
|
216
|
+
// v0.46.0. Every one is a READ with an exit-code contract, so the exit code is
|
|
217
|
+
// DATA here exactly like `pattern status` and `wiki impact` above: pact 0/1/2/3,
|
|
218
|
+
// boundary 0/1/2/3, handoff 0/1, budget 0/1/2/3, aftermath 0/1/2/3,
|
|
219
|
+
// wiki plan 0/1/2/3, wiki debt 0/1/3, export --check 0/1.
|
|
220
|
+
"/api/wiki/plan": () => ["wiki", "plan"],
|
|
221
|
+
"/api/wiki/debt": () => ["wiki", "debt"],
|
|
222
|
+
"/api/wiki/usage": () => ["wiki", "usage"],
|
|
223
|
+
// v0.49.1 — the wiki's CONTENTS, not only its temperature. All three are
|
|
224
|
+
// READS whose exit code is DATA (docs 0/1/3, show 0/2/3, coverage 0/1), and
|
|
225
|
+
// `coverage` deliberately has no threshold: nothing branches on it, here or
|
|
226
|
+
// anywhere else. `--body` is opt-in and one artifact at a time — the
|
|
227
|
+
// /api/doc/section precedent.
|
|
228
|
+
"/api/wiki/docs": () => ["wiki", "docs"],
|
|
229
|
+
"/api/wiki/show": (q) => ["wiki", "show", String(q.doc || ""), ...(q.body ? ["--body"] : [])],
|
|
230
|
+
"/api/wiki/coverage": () => ["wiki", "coverage"],
|
|
231
|
+
// The pattern file is injected LITERALLY into every executor slice, and until
|
|
232
|
+
// now nothing would show you a line of it. Same 0/1/2 contract `pattern
|
|
233
|
+
// status` has had since v0.34.8.
|
|
234
|
+
"/api/pattern/show": (q) => ["pattern", "show", String(q.lang || ""), ...(q.body ? ["--body"] : [])],
|
|
235
|
+
"/api/gotcha/show": (q) => ["gotcha", "show", String(q.id || "")],
|
|
236
|
+
"/api/gotchas/archived": () => ["gotcha", "list", "--archived"],
|
|
237
|
+
// Preview-then-apply: A COUNT IS NOT CONSENT. The Apply button stays disabled
|
|
238
|
+
// until this has been fetched, and it names every entry eviction would touch.
|
|
239
|
+
"/api/gotcha/prune/preview": () => ["gotcha", "prune", "--dry-run"],
|
|
240
|
+
// v1.1.0 — the wait. Three READS, and every one of them is a state the panel
|
|
241
|
+
// renders and never derives: `usage check` 0/1/2 (and `unknown` is a state,
|
|
242
|
+
// not a failure), the lane table, and the run's block. The panel CANNOT start
|
|
243
|
+
// a wait — a wait lives in a Claude Code session, and `orc ui` never runs a
|
|
244
|
+
// lane. It configures the defaults, shows a wait, and cancels one.
|
|
245
|
+
"/api/usage": () => ["usage", "check"],
|
|
246
|
+
"/api/wait/lanes": () => ["wait", "lanes"],
|
|
247
|
+
"/api/wait/status": (q) => (q.slug ? ["wait", "status", String(q.slug)] : ["wait", "status"]),
|
|
248
|
+
"/api/pact": () => ["pact", "status"],
|
|
249
|
+
"/api/boundary": (q) => (q.path ? ["boundary", "status", String(q.path)] : ["boundary", "status"]),
|
|
250
|
+
"/api/handoff": () => ["handoff", "surfaces"],
|
|
251
|
+
"/api/budget/rates": () => ["budget", "rates"],
|
|
252
|
+
// A forecast takes a PLAN PATH. The browser sends a path the folder picker
|
|
253
|
+
// produced; the server passes it straight to the CLI and never opens the file
|
|
254
|
+
// itself — /api/fs/list stays a directory LISTER, and widening it to read a
|
|
255
|
+
// plan would be the one change that turns this panel into a file reader.
|
|
256
|
+
"/api/budget/forecast": (q) => ["budget", "forecast", String(q.plan || "")],
|
|
257
|
+
"/api/budget/actual": (q) => ["budget", "actual", String(q.slug || "")],
|
|
258
|
+
"/api/aftermath": (q) => (q.since ? ["aftermath", "status", "--since", String(q.since)] : ["aftermath", "status"]),
|
|
259
|
+
"/api/export": () => ["export", "--check"],
|
|
260
|
+
// v0.47.0 — /orc-challenge. Same shape: every one is a READ whose exit code is
|
|
261
|
+
// DATA (list 0/1/3, status 0/1/2/3, diff 0/1/2/3, lint 0/1/2), and the panel
|
|
262
|
+
// derives nothing from them — not the state word, not the iteration order, not
|
|
263
|
+
// the pass decision, not the expected revision path.
|
|
264
|
+
"/api/challenge": () => ["challenge", "list"],
|
|
265
|
+
"/api/challenge/one": (q) => ["challenge", "status", String(q.slug || "")],
|
|
266
|
+
"/api/challenge/show": (q) => [
|
|
267
|
+
"challenge",
|
|
268
|
+
"show",
|
|
269
|
+
String(q.slug || ""),
|
|
270
|
+
...(q.iteration ? ["--iteration", String(q.iteration)] : []),
|
|
271
|
+
],
|
|
272
|
+
"/api/challenge/diff": (q) => ["challenge", "diff", String(q.slug || "")],
|
|
273
|
+
// v0.49.1 — the council. `roles` is STATIC (it works with no cycle at all),
|
|
274
|
+
// and it is the ONE catalogue: the panel names no lens, no class and no
|
|
275
|
+
// disposition itself, exactly as it names no flow step. `council` is 0 set /
|
|
276
|
+
// 1 unset / 3 unknown, and UNSET is an ANSWER, not an error.
|
|
277
|
+
"/api/challenge/roles": (q) => ["challenge", "roles", ...(q.kind ? ["--kind", String(q.kind)] : [])],
|
|
278
|
+
"/api/challenge/council": (q) => ["challenge", "council", String(q.slug || "")],
|
|
279
|
+
"/api/challenge/lint": (q) => [
|
|
280
|
+
"challenge",
|
|
281
|
+
"lint",
|
|
282
|
+
String(q.path || ""),
|
|
283
|
+
...(q.template ? ["--template", String(q.template)] : []),
|
|
284
|
+
],
|
|
285
|
+
// v0.48.0 — /orc-doc. Same shape again: every one is a READ whose exit code is
|
|
286
|
+
// DATA (list 0, status 0/1/2, map 0/2, plan 0/1, lint 0/1/2), and the panel
|
|
287
|
+
// derives nothing from them — not the section order, not a line range, not a
|
|
288
|
+
// state word, not the batching, not a lint rule name. It draws what the CLI
|
|
289
|
+
// computed.
|
|
290
|
+
//
|
|
291
|
+
// `/api/doc/section` is the ONE route that returns any of the document's
|
|
292
|
+
// prose, it returns exactly ONE section, and only on an explicit Reveal click.
|
|
293
|
+
// The rule this lane lives by is that nothing HOLDS the document — not that
|
|
294
|
+
// the text is secret — and the panel renders it as DOM through `renderMd`,
|
|
295
|
+
// never as HTML.
|
|
296
|
+
"/api/doc": () => ["doc", "list"],
|
|
297
|
+
"/api/doc/one": (q) => ["doc", "status", String(q.slug || "")],
|
|
298
|
+
"/api/doc/show": (q) => ["doc", "show", String(q.slug || "")],
|
|
299
|
+
"/api/doc/section": (q) => ["doc", "show", String(q.slug || ""), "--section", String(q.section || "")],
|
|
300
|
+
"/api/doc/map": (q) => ["doc", "map", String(q.slug || "")],
|
|
301
|
+
"/api/doc/lint": (q) => [
|
|
302
|
+
"doc",
|
|
303
|
+
"lint",
|
|
304
|
+
String(q.slug || ""),
|
|
305
|
+
...(q.target ? ["--target", String(q.target)] : []),
|
|
306
|
+
],
|
|
307
|
+
"/api/doc/plan": (q) => ["doc", "plan", String(q.slug || ""), "--role", String(q.role || "write")],
|
|
308
|
+
"/api/doc/templates": () => ["doc", "templates"],
|
|
309
|
+
"/api/doc/targets": () => ["doc", "targets"],
|
|
310
|
+
// v0.48.1 — the score, the drift report and the memory surface. Every one is
|
|
311
|
+
// a subprocess of the real command: the panel decides nothing about the
|
|
312
|
+
// pipeline order, nothing about which drift classes exist, and nothing about
|
|
313
|
+
// which journal rows are the user's own words.
|
|
314
|
+
//
|
|
315
|
+
// There is deliberately NO route for `orc doc log`: the SKILL records a
|
|
316
|
+
// request, because the skill is what took one. A panel that could write a
|
|
317
|
+
// journal entry could write one nobody said.
|
|
318
|
+
// v0.49.0 — the SECTION FILES. This is the one read that works before a
|
|
319
|
+
// single compile has ever run, because the files ARE the progress.
|
|
320
|
+
"/api/doc/parts": (q) => ["doc", "parts", String(q.slug || "")],
|
|
321
|
+
"/api/doc/next": (q) => ["doc", "next", String(q.slug || "")],
|
|
322
|
+
"/api/doc/audit": (q) => ["doc", "audit", String(q.slug || "")],
|
|
323
|
+
"/api/doc/journal": (q) => ["doc", "journal", String(q.slug || "")],
|
|
324
|
+
"/api/doc/context": (q) => ["doc", "context", String(q.slug || "")],
|
|
325
|
+
"/api/doc/extra": (q) => ["doc", "extra", String(q.slug || "")],
|
|
326
|
+
// v0.49.2 — the project's own house rules, the frozen set of one document,
|
|
327
|
+
// the run map and the cost report. All four are READS; the panel decides
|
|
328
|
+
// nothing about a priority, an order, a wave shape or a number.
|
|
329
|
+
"/api/doc/rules": () => ["doc", "rules"],
|
|
330
|
+
"/api/doc/rules/one": (q) => ["doc", "rules", String(q.slug || "")],
|
|
331
|
+
"/api/doc/forecast": (q) => ["doc", "forecast", String(q.slug || "")],
|
|
332
|
+
"/api/doc/cost": (q) => ["doc", "cost", String(q.slug || "")],
|
|
333
|
+
// v0.50.0 — `orc extra`. Every one is a READ, and every one is a subprocess of
|
|
334
|
+
// the real command: the panel decides nothing about a provider, a model id, a
|
|
335
|
+
// verification state, a band or a price. It renders what the CLI computed.
|
|
336
|
+
//
|
|
337
|
+
// There is deliberately NO read that returns a credential. `orc extra list`
|
|
338
|
+
// and `orc extra show` go through `redactProfile`, which is an ALLOW-LIST, so
|
|
339
|
+
// a field added later that carries a secret has to be let through on purpose.
|
|
340
|
+
"/api/extra": () => ["extra", "list"],
|
|
341
|
+
"/api/extra/providers": () => ["extra", "providers"],
|
|
342
|
+
"/api/extra/show": (q) => ["extra", "show", String(q.profile || "")],
|
|
343
|
+
"/api/extra/models": (q) => ["extra", "models", String(q.profile || "")],
|
|
344
|
+
// v0.51.0 — the LOCAL TOOLS read, and the credential-route read. Both are the
|
|
345
|
+
// real commands, so the panel decides nothing about an install command, a
|
|
346
|
+
// platform, a version floor or which of three credential routes applies.
|
|
347
|
+
// `tools` exits 1 when no tool is ready, which is exit-code-as-DATA like
|
|
348
|
+
// `pattern status` — never an error.
|
|
349
|
+
"/api/extra/tools": () => ["extra", "tools"],
|
|
350
|
+
"/api/extra/keyhelp": (q) => ["extra", "keyhelp", String(q.profile || "")],
|
|
351
|
+
"/api/extra/route": () => ["extra", "route"],
|
|
352
|
+
// v0.55.0 — THE POSITIONS, the non-scored half of routing. It exits 1 when
|
|
353
|
+
// nothing routes, which is exit-code-as-DATA like `pattern status` — an empty
|
|
354
|
+
// result is an ANSWER and it still returns its whole object.
|
|
355
|
+
"/api/extra/role": () => ["extra", "role", "list"],
|
|
356
|
+
// v0.52.0 (D6) — WHICH LANE a band governs, computed through the same
|
|
357
|
+
// resolver every dispatch uses. The panel renders it and derives nothing:
|
|
358
|
+
// a band with no lane attached is not a routing decision.
|
|
359
|
+
"/api/extra/lanes": () => ["extra", "lanes"],
|
|
360
|
+
// `stats` exits 1 with a real object when no foreign dispatch has been traced
|
|
361
|
+
// yet, and `rates` exits 1 when a pair has no price — both are exit-code-as-
|
|
362
|
+
// DATA, like `pattern status` and `wiki impact`, never an error.
|
|
363
|
+
"/api/extra/stats": (q) => (q.since ? ["extra", "stats", "--since", String(q.since)] : ["extra", "stats"]),
|
|
364
|
+
"/api/extra/rates": () => ["extra", "rates"],
|
|
365
|
+
"/api/extra/doctor": () => ["extra", "doctor"],
|
|
366
|
+
// v0.54.0 — RECOVERY. Both are FREE reads (zero model tokens), which is why
|
|
367
|
+
// both are real buttons; `resume-slice` and the dispatch that follows it cost
|
|
368
|
+
// money and are copy-able commands instead. `reconcile` exits 0-4 as an
|
|
369
|
+
// exit-code-as-DATA contract like `pattern status`, so no state here is an
|
|
370
|
+
// error — including `in-flight`, which is a REFUSAL the panel must render as
|
|
371
|
+
// one rather than as a dead control.
|
|
372
|
+
"/api/extra/journal": () => ["extra", "journal", "list"],
|
|
373
|
+
// v1.0.0 W16 — the RUN DEMOTION, read. Exit 0 armed · 1 demoted · 2 unknown
|
|
374
|
+
// run, which is exit-code-as-DATA like every other gate command here. The
|
|
375
|
+
// run is optional: with none given the CLI reads the trace pointer, which is
|
|
376
|
+
// exactly what a panel wants — "the run that is open right now".
|
|
377
|
+
"/api/extra/demotion": (q) => ["extra", "demotion", ...(q.run ? [String(q.run)] : [])],
|
|
378
|
+
"/api/extra/reconcile": (q) => ["extra", "reconcile", String(q.task || "")],
|
|
379
|
+
"/api/extra/journal/prune/preview": () => ["extra", "journal", "prune", "--dry-run"],
|
|
380
|
+
"/api/patterns": () => ["pattern", "status"],
|
|
381
|
+
"/api/gotchas": () => ["gotcha", "list"],
|
|
382
|
+
"/api/stats": (q) => (q.since ? ["stats", "--since", String(q.since)] : ["stats"]),
|
|
383
|
+
"/api/diy": () => ["diy", "show"],
|
|
384
|
+
"/api/crosslink": () => ["crosslink", "list"],
|
|
385
|
+
"/api/crosslink/kinds": () => ["crosslink", "kinds"],
|
|
386
|
+
"/api/mocks": () => ["mock", "list"],
|
|
387
|
+
"/api/mock": (q) => ["mock", "show", String(q.slug || "")],
|
|
388
|
+
"/api/stack": (q) => (q.slug ? ["pr", "stack", "status", String(q.slug)] : ["pr", "stack", "status"]),
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// v0.46.0 writes. THE LINE THIS PANEL DOES NOT CROSS: a button exists only for an
|
|
392
|
+
// action that costs NO model tokens. `orc pact check` runs the ledger's own cheap
|
|
393
|
+
// proofs (a test, a command, a grep the user wrote); `orc handoff set` edits one
|
|
394
|
+
// graded surface; `orc export` compiles files already on disk. Every one is
|
|
395
|
+
// deterministic and every one is a real CLI command.
|
|
396
|
+
//
|
|
397
|
+
// The conversational half of each lane — reconciling a promise, deciding a
|
|
398
|
+
// verdict, walking somebody through a change — costs model tokens, so the panel
|
|
399
|
+
// COPIES those commands and never runs them. A test greps this object to make
|
|
400
|
+
// sure no lane name ever appears inside it.
|
|
401
|
+
const WRITES = {
|
|
402
|
+
"/api/config/set": (b) => ["config", "set", String(b.key), String(b.value)],
|
|
403
|
+
"/api/config/reset": (b) => (b.key ? ["config", "reset", String(b.key)] : ["config", "reset"]),
|
|
404
|
+
"/api/config/profile": (b) => ["config", "profile", String(b.name)],
|
|
405
|
+
"/api/diy/set": (b) => ["diy", "set", String(b.key), String(b.value)],
|
|
406
|
+
"/api/diy/compile": () => ["diy", "compile"],
|
|
407
|
+
// The bootstrap the TTY composer offers as its first question, and the one
|
|
408
|
+
// piece of DIY the panel had no way to reach (v0.44.0). `--force` is what
|
|
409
|
+
// makes it an ANSWER rather than an error on an already-configured project:
|
|
410
|
+
// `orc diy init` refuses to overwrite without it. That is destructive, so the
|
|
411
|
+
// UI confirms with the preset's own diff and the exact command on screen.
|
|
412
|
+
// An empty name is the wizard's "full-lane defaults" — a real invocation,
|
|
413
|
+
// not a synthesised one.
|
|
414
|
+
"/api/diy/preset": (b) => {
|
|
415
|
+
const argv = ["diy", "init", "--force"];
|
|
416
|
+
if (b.name) argv.push("--preset", String(b.name));
|
|
417
|
+
return argv;
|
|
418
|
+
},
|
|
419
|
+
// v1.1.0 — the only two wait mutations this panel may make, and neither
|
|
420
|
+
// starts one. `unblock` restores a gate the user vetoed; `cancel` ends a wait
|
|
421
|
+
// already running. A BLOCK cannot be created here on purpose: it needs a
|
|
422
|
+
// reason typed in the moment, and a reason typed into a settings page days
|
|
423
|
+
// later is not the record that makes the risk demonstrably the user's.
|
|
424
|
+
"/api/wait/unblock": (b) => (b.slug ? ["wait", "unblock", String(b.slug)] : ["wait", "unblock"]),
|
|
425
|
+
"/api/wait/cancel": (b) => (b.slug ? ["wait", "cancel", String(b.slug)] : ["wait", "cancel"]),
|
|
426
|
+
"/api/wiki/sync": () => ["wiki", "sync"],
|
|
427
|
+
"/api/wiki/usage/rebuild": () => ["wiki", "usage", "--rebuild"],
|
|
428
|
+
"/api/gotcha/prune": () => ["gotcha", "prune"],
|
|
429
|
+
"/api/pact/check": (b) => (b.id ? ["pact", "check", String(b.id)] : ["pact", "check"]),
|
|
430
|
+
"/api/pact/sync": () => ["pact", "sync"],
|
|
431
|
+
// The surface id, the key and the value all come from the browser — and all
|
|
432
|
+
// three are validated by the CLI, which refuses a RED surface, an unknown id,
|
|
433
|
+
// a key that does not already exist, and a write at all when handoff_write is
|
|
434
|
+
// false. There is no second idea of a safe edit anywhere in this panel.
|
|
435
|
+
"/api/handoff/set": (b) => ["handoff", "set", String(b.id), String(b.key), String(b.value)],
|
|
436
|
+
"/api/budget/calibrate": () => ["budget", "calibrate"],
|
|
437
|
+
// v0.47.0. All three are FREE and deterministic, so all three get a button.
|
|
438
|
+
// Running an ITERATION costs model tokens, so it is a copy-able command and
|
|
439
|
+
// there is deliberately no route for it here. `accept` and `rebut` both refuse
|
|
440
|
+
// without a reason — the CLI decides that, not the form.
|
|
441
|
+
"/api/challenge/accept": (b) => ["challenge", "accept", String(b.slug), String(b.id), String(b.reason || "")],
|
|
442
|
+
"/api/challenge/rebut": (b) => ["challenge", "rebut", String(b.slug), String(b.id), String(b.reason || "")],
|
|
443
|
+
"/api/challenge/report": (b) => ["challenge", "report", String(b.slug)],
|
|
444
|
+
// v0.49.1. Both are FREE and both REFUSE without a reason, so both are
|
|
445
|
+
// buttons. There is deliberately NO route for `council --set`: changing the
|
|
446
|
+
// roster mid-cycle is a decision with a recorded reason that the LANE takes in
|
|
447
|
+
// the conversation — the same reasoning that keeps `orc doc log` and
|
|
448
|
+
// `orc doc mode` off the panel. Adopting a premise needs a goals FILE, which
|
|
449
|
+
// the panel must not invent, so that one stays a copy-able command too.
|
|
450
|
+
"/api/challenge/premise": (b) => [
|
|
451
|
+
"challenge", "premise", String(b.slug), String(b.id), "--dismiss", "--reason", String(b.reason || ""),
|
|
452
|
+
],
|
|
453
|
+
"/api/challenge/opportunity": (b) => [
|
|
454
|
+
"challenge", "opportunity", String(b.slug), String(b.id), b.take ? "--take" : "--drop", "--reason", String(b.reason || ""),
|
|
455
|
+
],
|
|
456
|
+
// v0.48.0. `assemble` — now `compile` — is the ONE /orc-doc write that costs
|
|
457
|
+
// nothing: it concatenates section files that are already on disk, in an order
|
|
458
|
+
// the outline already fixed. Writing a section, checking one and editing one
|
|
459
|
+
// all cost model tokens, so they are copy-able commands and there is
|
|
460
|
+
// deliberately no route for any of them.
|
|
461
|
+
"/api/doc/assemble": (b) => ["doc", "assemble", String(b.slug)],
|
|
462
|
+
// v0.49.0. Both free, both non-destructive: `compile` rebuilds the artifact
|
|
463
|
+
// from disk, and `migrate` never deletes document.md and refuses what it
|
|
464
|
+
// cannot parse. `orc doc mode` deliberately has NO route — it is a USER
|
|
465
|
+
// decision the skill asks (the `orc doc log` precedent).
|
|
466
|
+
"/api/doc/compile": (b) => {
|
|
467
|
+
const argv = ["doc", "compile", String(b.slug)];
|
|
468
|
+
if (b.partial) argv.push("--partial");
|
|
469
|
+
return argv;
|
|
470
|
+
},
|
|
471
|
+
"/api/doc/migrate": (b) => ["doc", "migrate", String(b.slug)],
|
|
472
|
+
// v0.48.1. Shipping is a DECISION, so it is a write — and `--where` has no
|
|
473
|
+
// default here either, because the CLI refuses without it and the panel must
|
|
474
|
+
// never invent an argument the human path demands.
|
|
475
|
+
"/api/doc/ship": (b) => {
|
|
476
|
+
const argv = ["doc", "ship", String(b.slug), "--where", String(b.where || "")];
|
|
477
|
+
if (b.note) argv.push("--note", String(b.note));
|
|
478
|
+
if (b.force) argv.push("--force", "--reason", String(b.reason || ""));
|
|
479
|
+
return argv;
|
|
480
|
+
},
|
|
481
|
+
"/api/doc/unship": (b) => ["doc", "unship", String(b.slug), "--reason", String(b.reason || "")],
|
|
482
|
+
// v0.49.5 — the house-rule ledger is a PLAIN TEXT config, so the panel writes
|
|
483
|
+
// it the way a text config is written: the whole file, once, from one
|
|
484
|
+
// textarea. `set-all` is the only write route it needs — the per-priority
|
|
485
|
+
// `set`/`add`/`clear` commands stay a CLI convenience, and `--reset` still has
|
|
486
|
+
// no route because throwing away a project's standing rules is a CLI act.
|
|
487
|
+
//
|
|
488
|
+
// Every validator is still the CLI's. The panel has no second idea of what a
|
|
489
|
+
// house rule looks like, and the argv is a plain array, so a multi-line value
|
|
490
|
+
// needs no escaping and no temp file.
|
|
491
|
+
"/api/doc/rules/setAll": (b) => ["doc", "rules", "set-all", "--text", String(b.text || "")],
|
|
492
|
+
"/api/doc/rules/sync": (b) => ["doc", "rules", String(b.slug), "--sync"],
|
|
493
|
+
// v0.52.0 (D9) — per document, because a document's voice is the deliverable.
|
|
494
|
+
// The CLI owns the resolution order and the shadowing announcement; the panel
|
|
495
|
+
// renders both and decides neither.
|
|
496
|
+
"/api/doc/extra/set": (b) => ["doc", "extra", String(b.slug), "--set", String(b.mode)],
|
|
497
|
+
// v0.49.2. Closing a run is FREE, deterministic, reversible, and it DELETES
|
|
498
|
+
// NOTHING — `RESUME.md` is moved aside, not removed. The CLI refuses without a
|
|
499
|
+
// reason, so the form does not have to: there is one idea of a valid close and
|
|
500
|
+
// it lives in `bin/cli.js`.
|
|
501
|
+
"/api/run/close": (b) => ["run", "close", String(b.slug), "--reason", String(b.reason || "")],
|
|
502
|
+
"/api/run/reopen": (b) => ["run", "reopen", String(b.slug)],
|
|
503
|
+
// v0.50.0 — `orc extra`. Adding a connection writes a PROFILE and nothing
|
|
504
|
+
// else: no key, no route, no change to how anything builds. Removing one
|
|
505
|
+
// REFUSES without a reason — the same rule the promise ledger retires an
|
|
506
|
+
// invariant under — and names the route rows it drops, so the form does not
|
|
507
|
+
// have to: there is one idea of a valid removal and it lives in bin/cli.js.
|
|
508
|
+
//
|
|
509
|
+
// The routing table (W12). Both are STAGED in the panel and applied one at a
|
|
510
|
+
// time in staged order, so a refused row never aborts the rest — and the CLI
|
|
511
|
+
// is still what refuses an overlap, an unverified profile or a bad band spec,
|
|
512
|
+
// BY NAME. There is deliberately no `--key <value>` anywhere, because the CLI
|
|
513
|
+
// refuses it BY NAME — argv is world-readable. A pasted key travels on the
|
|
514
|
+
// connection test's STDIN and nowhere else.
|
|
515
|
+
"/api/extra/add": (b) => {
|
|
516
|
+
const argv = ["extra", "add", String(b.name), "--provider", String(b.provider), "--engine", String(b.engine)];
|
|
517
|
+
if (b.region && b.region !== "default") argv.push("--region", String(b.region));
|
|
518
|
+
if (b.base_url) argv.push("--base-url", String(b.base_url));
|
|
519
|
+
if (b.anthropic_base_url) argv.push("--anthropic-base-url", String(b.anthropic_base_url));
|
|
520
|
+
if (b.cli_bin) argv.push("--cli", String(b.cli_bin));
|
|
521
|
+
if (b.cli_agent) argv.push("--cli-agent", String(b.cli_agent));
|
|
522
|
+
// v0.52.0 — the THIRD credential source, checked FIRST. A local tool that
|
|
523
|
+
// already holds its own credential needs no key from ORC at all, and the
|
|
524
|
+
// panel forcing such a profile into the vault is what locked a run that had
|
|
525
|
+
// no business having a passphrase. ORC never writes another tool's
|
|
526
|
+
// credential store; `--tool-auth` is how that is said out loud.
|
|
527
|
+
if (b.tool_auth) argv.push("--tool-auth");
|
|
528
|
+
else if (b.vault) argv.push("--key-stdin");
|
|
529
|
+
else if (b.env_key) argv.push("--env-key", String(b.env_key));
|
|
530
|
+
return argv;
|
|
531
|
+
},
|
|
532
|
+
"/api/extra/remove": (b) => ["extra", "remove", String(b.name), "--reason", String(b.reason || "")],
|
|
533
|
+
// v0.52.0 — forgetting a saved passphrase carries no secret, so it is an
|
|
534
|
+
// ordinary argv write. SAVING one does, and has its own handler below: the
|
|
535
|
+
// passphrase travels on STDIN and `--passphrase <value>` is refused BY NAME in
|
|
536
|
+
// the CLI, so there is no argv path on either side.
|
|
537
|
+
"/api/extra/session/forget": (b) => ["extra", "session", String(b.profile), "--forget"],
|
|
538
|
+
"/api/extra/route/set": (b) => {
|
|
539
|
+
const argv = ["extra", "route", "set", String(b.band), String(b.target)];
|
|
540
|
+
if (b.small_model) argv.push("--small-model", String(b.small_model));
|
|
541
|
+
if (b.max_turns) argv.push("--max-turns", String(b.max_turns));
|
|
542
|
+
return argv;
|
|
543
|
+
},
|
|
544
|
+
"/api/extra/route/rm": (b) => ["extra", "route", "rm", String(b.band)],
|
|
545
|
+
// A slot is a POINT, not an interval, so there is no overlap to refuse and
|
|
546
|
+
// `set` on an occupied one REPLACES — which is why the panel confirms it and
|
|
547
|
+
// names what it replaces. A count is not consent.
|
|
548
|
+
"/api/extra/role/set": (b) => {
|
|
549
|
+
const argv = ["extra", "role", "set", String(b.slot), String(b.target)];
|
|
550
|
+
if (b.small_model) argv.push("--small-model", String(b.small_model));
|
|
551
|
+
if (b.max_turns) argv.push("--max-turns", String(b.max_turns));
|
|
552
|
+
return argv;
|
|
553
|
+
},
|
|
554
|
+
"/api/extra/role/rm": (b) => ["extra", "role", "rm", String(b.slot)],
|
|
555
|
+
// v0.51.0 — running the install in the USER'S OWN TERMINAL. It is a POST
|
|
556
|
+
// because it launches something; it writes no config, stores no state and
|
|
557
|
+
// never elevates. A launch that could not happen comes back exit 0 carrying
|
|
558
|
+
// the command to paste (the `openBrowser` rule), so there is no failure path
|
|
559
|
+
// that leaves the card without an answer.
|
|
560
|
+
"/api/extra/install": (b) => {
|
|
561
|
+
const argv = ["extra", "install", String(b.provider)];
|
|
562
|
+
if (b.manager) argv.push("--manager", String(b.manager));
|
|
563
|
+
return argv;
|
|
564
|
+
},
|
|
565
|
+
// A live model list is a FREE re-read of the provider's own catalogue, and a
|
|
566
|
+
// per-model test is the PAID rung scoped to one id — the only thing that tells
|
|
567
|
+
// a LISTED model from a WORKING one.
|
|
568
|
+
"/api/extra/models/refresh": (b) => ["extra", "models", String(b.profile), "--refresh"],
|
|
569
|
+
// Preview-then-apply, and the preview NAMES EVERY DIRECTORY — a count is not
|
|
570
|
+
// consent. Only a journal whose every attempt closed `done` 30+ days ago is
|
|
571
|
+
// ever a candidate, so this can never delete the record of a dispatch that
|
|
572
|
+
// never reported back.
|
|
573
|
+
"/api/extra/journal/prune": () => ["extra", "journal", "prune"],
|
|
574
|
+
// A PROMOTE IS A HUMAN ACTION AND A REASON IS REQUIRED (v1.0.0 W5). The CLI
|
|
575
|
+
// refuses without one — exit 2, `reason-required` — so the panel does not
|
|
576
|
+
// validate it a second time; it collects it and lets the CLI decide, which is
|
|
577
|
+
// the same contract every other write on this server keeps. There is
|
|
578
|
+
// deliberately NO demote route: demoting by hand is a diagnostic somebody
|
|
579
|
+
// reaches for at a terminal, and a button for it would invite muting a
|
|
580
|
+
// provider instead of fixing it.
|
|
581
|
+
"/api/extra/promote": (b) => ["extra", "promote", String(b.run || ""), "--reason", String(b.reason || "")],
|
|
582
|
+
"/api/crosslink/remove": (b) => ["crosslink", "remove", String(b.name)],
|
|
583
|
+
// The UI assembles no YAML. It hands the CLI the same arguments the
|
|
584
|
+
// interactive prompt collects, and every rejection the user sees is the
|
|
585
|
+
// CLI's own validator speaking — there is no second idea of a valid slug,
|
|
586
|
+
// a valid kind or a valid edge target anywhere in this panel.
|
|
587
|
+
"/api/crosslink/add": (b) => {
|
|
588
|
+
const argv = ["crosslink", "add", String(b.name), String(b.repo_path), "--kinds", String(b.kinds)];
|
|
589
|
+
if (b.direction) argv.push("--direction", String(b.direction));
|
|
590
|
+
if (b.via) argv.push("--via", String(b.via));
|
|
591
|
+
if (b.target) argv.push("--target", String(b.target));
|
|
592
|
+
return argv;
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// Maintenance: the safety-critical panel. Each action is a PAIR — a read-only
|
|
597
|
+
// preview and the apply that the preview is consent for. The apply route can
|
|
598
|
+
// never be reached without the UI having fetched the preview first, and the
|
|
599
|
+
// exact command is part of the preview payload so it is always visible in the
|
|
600
|
+
// confirmation, and always typeable by hand instead.
|
|
601
|
+
// `restarts_ui` — DOES THIS ACTION REPLACE WHAT THE PANEL IS SERVING?
|
|
602
|
+
//
|
|
603
|
+
// `orc upgrade` installs a new package over the one this process is running
|
|
604
|
+
// from, and `orc update` / `--prune` / `doctor --fix` rewrite the payload every
|
|
605
|
+
// panel reads. Node loaded bin/webui at require time and `STATIC` is a one-time
|
|
606
|
+
// walk at boot, so neither is visible until the server is replaced — which used
|
|
607
|
+
// to mean stop the server, re-run `orc ui`, open the new URL. The flag is
|
|
608
|
+
// DECLARED per action rather than inferred, because "this command changed the
|
|
609
|
+
// code under me" is not something a command's output can be read for.
|
|
610
|
+
//
|
|
611
|
+
// `update-global` is deliberately FALSE: it re-copies the payload into
|
|
612
|
+
// ~/.claude, which is not what this server is running and not what any panel
|
|
613
|
+
// here reads.
|
|
614
|
+
const MAINTENANCE = {
|
|
615
|
+
update: {
|
|
616
|
+
apply: ["update"],
|
|
617
|
+
restarts_ui: true,
|
|
618
|
+
label: "Re-copy this package's payload over the installed one",
|
|
619
|
+
// `orc doctor --json` already itemises exactly what an update would change
|
|
620
|
+
// (version skew, missing files, orphans) — a preview with no second engine.
|
|
621
|
+
preview: ["doctor"],
|
|
622
|
+
},
|
|
623
|
+
prune: {
|
|
624
|
+
apply: ["update", "--prune"],
|
|
625
|
+
restarts_ui: true,
|
|
626
|
+
label: "Update AND delete ORC-named orphans from a pre-manifest install",
|
|
627
|
+
preview: ["doctor"],
|
|
628
|
+
// A count is not consent for a deletion: the UI must name every file, and
|
|
629
|
+
// doctor's findings carry `paths` for exactly the two orphan findings.
|
|
630
|
+
names_files: true,
|
|
631
|
+
},
|
|
632
|
+
fix: {
|
|
633
|
+
apply: ["doctor", "--fix"],
|
|
634
|
+
restarts_ui: true,
|
|
635
|
+
label: "Apply every fix orc doctor found (= update + prune + settings re-merge)",
|
|
636
|
+
preview: ["doctor"],
|
|
637
|
+
},
|
|
638
|
+
upgrade: {
|
|
639
|
+
apply: ["upgrade"],
|
|
640
|
+
restarts_ui: true,
|
|
641
|
+
label: "Fetch the LATEST package from the network, then apply it",
|
|
642
|
+
preview: ["version"],
|
|
643
|
+
network: true,
|
|
644
|
+
},
|
|
645
|
+
// v0.46.0 — the portable export. It belongs on Maintenance by the v0.43.6 rule
|
|
646
|
+
// (a caution routes to the panel that can CLEAR it): `export-stale` is cleared
|
|
647
|
+
// by `orc export`, which is a CLI write this panel can run. Its preview is the
|
|
648
|
+
// `--check` report, which names WHICH source drifted — a count of stale sources
|
|
649
|
+
// would not be consent to overwrite a committed file.
|
|
650
|
+
export: {
|
|
651
|
+
apply: ["export"],
|
|
652
|
+
label: "Recompile AGENTS.md from the wiki, patterns, PACT.md and boundary cards",
|
|
653
|
+
preview: ["export", "--check"],
|
|
654
|
+
},
|
|
655
|
+
// ADVANCED (v0.44.0) — the one action on this panel that does not target the
|
|
656
|
+
// project. Every other route pins `--dir <projectRoot>`; `--global` outranks
|
|
657
|
+
// `--dir` in `resolveClaudeDir`, so this pair reaches ~/.claude and its
|
|
658
|
+
// preview reports on the same place it would write.
|
|
659
|
+
//
|
|
660
|
+
// It is here because a stale GLOBAL install is a real failure this panel
|
|
661
|
+
// already REPORTS (the persistent banner, and doctor's global-skew finding)
|
|
662
|
+
// and previously could not act on — the fix was "go type it in a terminal".
|
|
663
|
+
// It stays boxed off as advanced, and it is still the only global reach the
|
|
664
|
+
// panel has: config is never written globally, because config does not merge
|
|
665
|
+
// and a global write would silently outrank the file every panel here edits.
|
|
666
|
+
"update-global": {
|
|
667
|
+
apply: ["update", "--global"],
|
|
668
|
+
label: "Re-copy this package's payload over the GLOBAL install in ~/.claude",
|
|
669
|
+
preview: ["doctor", "--global"],
|
|
670
|
+
advanced: true,
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// ── the Experiment panel ────────────────────────────────────────────────────
|
|
675
|
+
//
|
|
676
|
+
// THE BOUNDARY MOVES EXACTLY ONE STEP, AND NO FURTHER. `orc ui` still renders
|
|
677
|
+
// no model output, proxies no session and holds no API key — it does not become
|
|
678
|
+
// an AI client. What it gains is a HANDOFF: it can open a terminal, in this
|
|
679
|
+
// project, with `claude` running in it, and then forget about it. The spawned
|
|
680
|
+
// process is not a child this server manages, reads or reports on; it is the
|
|
681
|
+
// same detached-launch mechanism `openBrowser()` has always used.
|
|
682
|
+
//
|
|
683
|
+
// The lanes are a SERVER-SIDE catalog and the browser sends only an id. No
|
|
684
|
+
// string typed in a browser ever reaches a shell — the cwd is always the
|
|
685
|
+
// server's own projectRoot, and an unknown id is a 400. That constraint is the
|
|
686
|
+
// only reason a launch button is safe on a write surface at all.
|
|
687
|
+
const LANES = [
|
|
688
|
+
{ id: "orc", cmd: "/orc", what: "Full pipeline: intake → plan → scored parallel waves → review → verify → ship." },
|
|
689
|
+
{ id: "orc-quick", cmd: "/orc-quick", what: "Ask for anything. Look → ask once → do, and it always asks which agent." },
|
|
690
|
+
{ id: "orc-mini", cmd: "/orc-mini", what: "One executor, smoke gate, ship. No full review or verify phase." },
|
|
691
|
+
{ id: "orc-fast", cmd: "/orc-fast", what: "Fastest lane. Needs a fresh wiki AND a cached pattern, or it falls back." },
|
|
692
|
+
{ id: "orc-ultra", cmd: "/orc-ultra", what: "Maximum rigor: an advisor plus three judgment gates. Cost accepted." },
|
|
693
|
+
{ id: "orc-plan", cmd: "/orc-plan", what: "Turn a request or analyst spec into a grounded task plan. Plan only." },
|
|
694
|
+
{ id: "orc-analyze", cmd: "/orc-analyze", what: "Turn a document or a vague requirement into code-grounded requirements." },
|
|
695
|
+
{ id: "orc-wiki", cmd: "/orc-wiki", what: "Build or refresh the project wiki. Expensive; always asks first." },
|
|
696
|
+
{ id: "orc-pattern", cmd: "/orc-pattern", what: "Learn this project's real code conventions and cache them per language." },
|
|
697
|
+
{ id: "orc-verify", cmd: "/orc-verify", what: "Verify the git-modified changes in the working tree. Read-only." },
|
|
698
|
+
{ id: "orc-learn", cmd: "/orc-learn", what: "Generate per-feature onboarding docs. Local and git-ignored." },
|
|
699
|
+
{ id: "orc-retro", cmd: "/orc-retro", what: "Mine the behavior traces for calibration. Read-only, report-only." },
|
|
700
|
+
];
|
|
701
|
+
|
|
702
|
+
// ── the folder picker ───────────────────────────────────────────────────────
|
|
703
|
+
//
|
|
704
|
+
// The THIRD endpoint with no CLI behind it (after /api/learn's shipped content
|
|
705
|
+
// and /api/experiment's lane catalog), and for the same reason: there is no
|
|
706
|
+
// `orc` command that lists directories, so there is nothing to shell. It exists
|
|
707
|
+
// because a crosslink repo path typed by hand is the one field in this panel
|
|
708
|
+
// where a typo is invisible until the edge silently resolves to nothing — the
|
|
709
|
+
// CLI then saves it as a PENDING edge and you find out much later.
|
|
710
|
+
//
|
|
711
|
+
// It is a DIRECTORY LISTER and nothing more, and the limits are the design:
|
|
712
|
+
// · directory names only — never a file list, never file contents, never a
|
|
713
|
+
// stat beyond "does .git / .claude/wiki exist here";
|
|
714
|
+
// · dotfolders are hidden (`.git`, `node_modules` and friends are noise here);
|
|
715
|
+
// · it reads, it never writes, and no path it is handed can reach a shell;
|
|
716
|
+
// · a path that cannot be read is an ANSWER (`error`), never a 500.
|
|
717
|
+
// Nothing is copied out of the folders it lists, so a wrong click costs a
|
|
718
|
+
// re-click. The browser is already loopback + token gated; this adds no reach
|
|
719
|
+
// beyond what the person at the keyboard already has.
|
|
720
|
+
const FS_LIST_MAX = 400;
|
|
721
|
+
|
|
722
|
+
function fsList(dir, ctx) {
|
|
723
|
+
const target = path.resolve(dir || ctx.projectRoot || os.homedir());
|
|
724
|
+
let entries;
|
|
725
|
+
try {
|
|
726
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
727
|
+
} catch (e) {
|
|
728
|
+
return { path: target, error: String(e.code || e.message), dirs: [] };
|
|
729
|
+
}
|
|
730
|
+
const dirs = [];
|
|
731
|
+
for (const e of entries) {
|
|
732
|
+
if (dirs.length >= FS_LIST_MAX) break;
|
|
733
|
+
if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
734
|
+
const full = path.join(target, e.name);
|
|
735
|
+
dirs.push({
|
|
736
|
+
name: e.name,
|
|
737
|
+
path: full,
|
|
738
|
+
// The two facts that decide whether a folder is worth linking at all.
|
|
739
|
+
// Both are a single existsSync — nothing inside either one is read.
|
|
740
|
+
is_repo: fs.existsSync(path.join(full, ".git")),
|
|
741
|
+
has_wiki: fs.existsSync(path.join(full, ".claude", "wiki")),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
dirs.sort((a, b) => a.name.localeCompare(b.name));
|
|
745
|
+
const parent = path.dirname(target);
|
|
746
|
+
return {
|
|
747
|
+
path: target,
|
|
748
|
+
parent: parent === target ? null : parent, // null AT a filesystem root
|
|
749
|
+
sep: path.sep,
|
|
750
|
+
home: os.homedir(),
|
|
751
|
+
project_root: ctx.projectRoot || null,
|
|
752
|
+
is_project_root: !!ctx.projectRoot && path.resolve(ctx.projectRoot) === target,
|
|
753
|
+
// What the crosslink config actually stores. Computed here rather than in
|
|
754
|
+
// the browser because only the server knows the real path separator, and a
|
|
755
|
+
// Windows path assembled with "/" is the kind of thing that works until it
|
|
756
|
+
// does not.
|
|
757
|
+
relative: ctx.projectRoot ? path.relative(ctx.projectRoot, target).split(path.sep).join("/") || "." : null,
|
|
758
|
+
truncated: dirs.length >= FS_LIST_MAX,
|
|
759
|
+
dirs,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Open a terminal running `claude` in the project root. Best-effort and never
|
|
764
|
+
// fatal — a failed launch is reported so the user can copy the command instead,
|
|
765
|
+
// which is why the command is always on screen anyway.
|
|
766
|
+
function launchClaude(ctx) {
|
|
767
|
+
const cwd = ctx.projectRoot;
|
|
768
|
+
let cmd, args;
|
|
769
|
+
if (process.platform === "win32") {
|
|
770
|
+
// `start` needs an empty title argument first, or a quoted path becomes it.
|
|
771
|
+
cmd = "cmd";
|
|
772
|
+
args = ["/c", "start", "", "cmd", "/k", "claude"];
|
|
773
|
+
} else if (process.platform === "darwin") {
|
|
774
|
+
cmd = "osascript";
|
|
775
|
+
args = ["-e", `tell application "Terminal" to do script "cd ${JSON.stringify(cwd).slice(1, -1)} && claude"`, "-e", 'tell application "Terminal" to activate'];
|
|
776
|
+
} else {
|
|
777
|
+
cmd = "x-terminal-emulator";
|
|
778
|
+
args = ["-e", "claude"];
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const child = spawn(cmd, args, { cwd, detached: true, stdio: "ignore", windowsHide: false });
|
|
782
|
+
child.unref();
|
|
783
|
+
return { ok: true };
|
|
784
|
+
} catch (e) {
|
|
785
|
+
return { ok: false, error: String(e && e.message) };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ── request handling ────────────────────────────────────────────────────────
|
|
790
|
+
|
|
791
|
+
function json(res, status, obj) {
|
|
792
|
+
const body = JSON.stringify(obj);
|
|
793
|
+
res.writeHead(status, {
|
|
794
|
+
"content-type": "application/json; charset=utf-8",
|
|
795
|
+
"cache-control": "no-store",
|
|
796
|
+
// Belt and braces on top of the loopback + token checks in serve.js.
|
|
797
|
+
"x-content-type-options": "nosniff",
|
|
798
|
+
});
|
|
799
|
+
res.end(body);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function readBody(req) {
|
|
803
|
+
return new Promise((resolve, reject) => {
|
|
804
|
+
let raw = "";
|
|
805
|
+
req.on("data", (c) => {
|
|
806
|
+
raw += c;
|
|
807
|
+
if (raw.length > 64_000) reject(new Error("body too large"));
|
|
808
|
+
});
|
|
809
|
+
req.on("end", () => {
|
|
810
|
+
if (!raw) return resolve({});
|
|
811
|
+
try {
|
|
812
|
+
resolve(JSON.parse(raw));
|
|
813
|
+
} catch (_) {
|
|
814
|
+
reject(new Error("body is not JSON"));
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
req.on("error", reject);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// The Overview panel needs four commands at once. Doing that in one request
|
|
822
|
+
// keeps the first paint to a single round trip.
|
|
823
|
+
function overview(ctx) {
|
|
824
|
+
const doctor = readCli(["doctor"], ctx);
|
|
825
|
+
const runs = readCli(["run", "list", "--limit", "200"], ctx);
|
|
826
|
+
const waiting = runs.data ? runs.data.runs.filter((r) => r.status === "waiting") : [];
|
|
827
|
+
return {
|
|
828
|
+
where: readCli(["where"], ctx).data,
|
|
829
|
+
doctor: doctor.data,
|
|
830
|
+
wiki: readCli(["wiki", "status"], ctx).data,
|
|
831
|
+
patterns: readCli(["pattern", "status"], ctx).data,
|
|
832
|
+
runs_total: runs.data ? runs.data.total : 0,
|
|
833
|
+
// Rows, not bare slugs (v0.49.2). The Overview card has an age column and
|
|
834
|
+
// rendered it empty because the payload never carried the number — and the
|
|
835
|
+
// "mark as done" button needs the slug beside a real timestamp to be worth
|
|
836
|
+
// showing at all.
|
|
837
|
+
waiting: waiting.map((r) => ({ slug: r.slug, updated_ms: r.updated_ms, lane: r.lane || null })),
|
|
838
|
+
diy: readCli(["diy", "show"], ctx).data,
|
|
839
|
+
// v0.46.0 chips. Each is the CLI's OWN answer — the panel repeats the state
|
|
840
|
+
// words and never derives them. A chip with nothing to say still renders its
|
|
841
|
+
// good state, so "healthy" and "not measured" never look the same.
|
|
842
|
+
pact: readCli(["pact", "status"], ctx).data,
|
|
843
|
+
boundary: readCli(["boundary", "status"], ctx).data,
|
|
844
|
+
wiki_debt: readCli(["wiki", "debt"], ctx).data,
|
|
845
|
+
// v0.54.0 — foreign dispatches that never reported back. Money spent and
|
|
846
|
+
// work half-done that nothing will look at again unless somebody is told.
|
|
847
|
+
// It is a FINDING, never a stop, and the Overview never resumes one.
|
|
848
|
+
extra_journal: readCli(["extra", "journal", "list"], ctx).data,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function handleApi(req, res, url, ctx) {
|
|
853
|
+
const route = url.pathname;
|
|
854
|
+
const q = Object.fromEntries(url.searchParams);
|
|
855
|
+
|
|
856
|
+
// Liveness. The page pings this every 15s; no ping from any client for the
|
|
857
|
+
// grace window and the server exits, so a closed tab does not leave a write
|
|
858
|
+
// surface holding a valid token.
|
|
859
|
+
if (route === "/api/ping") {
|
|
860
|
+
ctx.onHeartbeat();
|
|
861
|
+
return json(res, 200, { ok: true, job: jobView() });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// sendBeacon on beforeunload — a best-effort fast path to the same shutdown
|
|
865
|
+
// the heartbeat timeout would reach a minute later.
|
|
866
|
+
if (route === "/api/bye") {
|
|
867
|
+
ctx.onBye();
|
|
868
|
+
return json(res, 200, { ok: true });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (route === "/api/meta") {
|
|
872
|
+
return json(res, 200, {
|
|
873
|
+
project_root: ctx.projectRoot,
|
|
874
|
+
fixtures: ctx.fixtures,
|
|
875
|
+
version: ctx.version,
|
|
876
|
+
port: ctx.port,
|
|
877
|
+
idle_minutes: ctx.idleMinutes,
|
|
878
|
+
started_ms: ctx.startedMs,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (route === "/api/job") return json(res, 200, jobView());
|
|
883
|
+
|
|
884
|
+
// Fixture mode short-circuits every data route: canned JSON, no project, no
|
|
885
|
+
// spawn. This is what makes the STALE chip and the unhealthy doctor panel
|
|
886
|
+
// designable on a machine where everything is green (see the plan, §9).
|
|
887
|
+
if (ctx.fixtures) {
|
|
888
|
+
if (req.method !== "GET") {
|
|
889
|
+
// Almost every mutation answers "nothing ran", which is the honest reply
|
|
890
|
+
// in a mode that runs nothing. The ONE exception is the connection test:
|
|
891
|
+
// its two outcomes are states the Extra panel is largely about, and a
|
|
892
|
+
// state with no fixture is a state nobody has ever looked at. A canned
|
|
893
|
+
// answer carries `data` and NOT the `fixture` flag, so the panel renders
|
|
894
|
+
// the real result shape — the command string is what says it was canned.
|
|
895
|
+
let body = {};
|
|
896
|
+
try {
|
|
897
|
+
body = await readBody(req);
|
|
898
|
+
} catch (_) {}
|
|
899
|
+
const canned = fixtures.post(route, body);
|
|
900
|
+
if (canned)
|
|
901
|
+
return json(res, 200, {
|
|
902
|
+
ok: true,
|
|
903
|
+
exit_code: canned.exit_code,
|
|
904
|
+
data: canned.data,
|
|
905
|
+
command: "(fixtures — nothing ran)",
|
|
906
|
+
});
|
|
907
|
+
return json(res, 200, { ok: true, fixture: true, command: "(fixtures — nothing ran)" });
|
|
908
|
+
}
|
|
909
|
+
const canned = fixtures.get(route, q);
|
|
910
|
+
if (canned === undefined) return json(res, 404, { error: "no fixture for " + route });
|
|
911
|
+
return json(res, 200, { ok: true, exit_code: 0, data: canned, fixture: true });
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (req.method === "GET") {
|
|
915
|
+
if (route === "/api/overview") return json(res, 200, { ok: true, exit_code: 0, data: overview(ctx) });
|
|
916
|
+
if (route === "/api/learn") {
|
|
917
|
+
// The only endpoint with no CLI behind it: the onboarding topics are
|
|
918
|
+
// static content already shipped as a module, so spawning to read them
|
|
919
|
+
// would be ceremony with a cost.
|
|
920
|
+
const { SECTIONS } = require("../onboarding-content.js");
|
|
921
|
+
return json(res, 200, { ok: true, exit_code: 0, data: { sections: SECTIONS } });
|
|
922
|
+
}
|
|
923
|
+
// The mocked runs (v0.46.x). Same shape as /api/learn above and for the
|
|
924
|
+
// same reason: this is static content that ships inside this package, so
|
|
925
|
+
// spawning a subprocess to read files sitting next to this one would be
|
|
926
|
+
// ceremony with a cost. `orc mock-run` reads the identical module, so the
|
|
927
|
+
// terminal and the panel cannot disagree.
|
|
928
|
+
if (route === "/api/mockruns") {
|
|
929
|
+
return json(res, 200, { ok: true, exit_code: 0, data: require("../mockrun-catalog.js").catalogue() });
|
|
930
|
+
}
|
|
931
|
+
if (route === "/api/mockrun") {
|
|
932
|
+
const doc = require("../mockrun-catalog.js").get(String(q.slug || ""));
|
|
933
|
+
if (!doc) return json(res, 200, { ok: true, exit_code: 1, data: { slug: String(q.slug || ""), found: false } });
|
|
934
|
+
return json(res, 200, { ok: true, exit_code: 0, data: { ...doc, found: true } });
|
|
935
|
+
}
|
|
936
|
+
if (route === "/api/fs/list") {
|
|
937
|
+
return json(res, 200, { ok: true, exit_code: 0, data: fsList(q.path, ctx) });
|
|
938
|
+
}
|
|
939
|
+
if (route === "/api/experiment") {
|
|
940
|
+
return json(res, 200, {
|
|
941
|
+
ok: true,
|
|
942
|
+
exit_code: 0,
|
|
943
|
+
data: {
|
|
944
|
+
lanes: LANES,
|
|
945
|
+
project_root: ctx.projectRoot,
|
|
946
|
+
platform: process.platform,
|
|
947
|
+
// Fixture mode must never spawn a real terminal on a machine that has
|
|
948
|
+
// no project — the button says so instead of lying about it.
|
|
949
|
+
can_launch: !ctx.fixtures,
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
if (route === "/api/maintenance") {
|
|
954
|
+
const actions = Object.entries(MAINTENANCE).map(([id, m]) => ({
|
|
955
|
+
id,
|
|
956
|
+
label: m.label,
|
|
957
|
+
command: "orc " + m.apply.join(" "),
|
|
958
|
+
network: !!m.network,
|
|
959
|
+
names_files: !!m.names_files,
|
|
960
|
+
advanced: !!m.advanced,
|
|
961
|
+
restarts_ui: !!m.restarts_ui,
|
|
962
|
+
}));
|
|
963
|
+
return json(res, 200, { ok: true, exit_code: 0, data: { actions } });
|
|
964
|
+
}
|
|
965
|
+
if (route === "/api/maintenance/preview") {
|
|
966
|
+
const m = MAINTENANCE[String(q.action)];
|
|
967
|
+
if (!m) return json(res, 400, { error: "unknown action" });
|
|
968
|
+
const probe = readCli(m.preview, ctx);
|
|
969
|
+
return json(res, 200, {
|
|
970
|
+
ok: true,
|
|
971
|
+
exit_code: 0,
|
|
972
|
+
data: {
|
|
973
|
+
action: String(q.action),
|
|
974
|
+
label: m.label,
|
|
975
|
+
command: "orc " + m.apply.join(" "),
|
|
976
|
+
network: !!m.network,
|
|
977
|
+
names_files: !!m.names_files,
|
|
978
|
+
advanced: !!m.advanced,
|
|
979
|
+
// Said in the confirmation, not discovered afterwards. A panel that
|
|
980
|
+
// reloads itself without warning reads as a crash.
|
|
981
|
+
restarts_ui: !!m.restarts_ui,
|
|
982
|
+
preview_command: "orc " + m.preview.join(" "),
|
|
983
|
+
preview: probe.data,
|
|
984
|
+
// Only the UI can know a run is mid-flight; updating changes the
|
|
985
|
+
// skills that run would resume into.
|
|
986
|
+
waiting_runs: (readCli(["run", "list", "--limit", "200"], ctx).data || { runs: [] }).runs
|
|
987
|
+
.filter((r) => r.status === "waiting")
|
|
988
|
+
.map((r) => r.slug),
|
|
989
|
+
dirty_tree: m.network ? isDirtyTree(ctx) : false,
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
const build = READS[route];
|
|
994
|
+
if (!build) return json(res, 404, { error: "unknown endpoint " + route });
|
|
995
|
+
const out = readCli(build(q), ctx);
|
|
996
|
+
// v0.49.2 — a read that produced no parseable object still has to SAY why.
|
|
997
|
+
// The body already carried `stderr` and `stdout`; nothing named `error`, so
|
|
998
|
+
// the client fell through to "request failed (500)" and one corrupt ledger
|
|
999
|
+
// looked like a broken panel. The reason the CLI printed is what is shown.
|
|
1000
|
+
return json(res, out.ok ? 200 : 500, out.ok ? out : { ...out, error: readFailReason(out) });
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
1004
|
+
|
|
1005
|
+
let body;
|
|
1006
|
+
try {
|
|
1007
|
+
body = await readBody(req);
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
return json(res, 400, { error: e.message });
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// The handoff. It takes NO command from the browser: the lane id is looked up
|
|
1013
|
+
// in the server's own catalog and is used only to echo back what to type. The
|
|
1014
|
+
// process spawned is always a bare `claude` in the server's own projectRoot,
|
|
1015
|
+
// so there is no path by which browser input reaches a shell.
|
|
1016
|
+
if (route === "/api/experiment/launch") {
|
|
1017
|
+
if (ctx.fixtures) return json(res, 400, { error: "fixture mode never launches anything real" });
|
|
1018
|
+
const lane = body.lane ? LANES.find((l) => l.id === String(body.lane)) : null;
|
|
1019
|
+
if (body.lane && !lane) return json(res, 400, { error: "unknown lane" });
|
|
1020
|
+
const r = launchClaude(ctx);
|
|
1021
|
+
if (!r.ok) return json(res, 500, { error: "could not open a terminal: " + r.error });
|
|
1022
|
+
return json(res, 200, {
|
|
1023
|
+
ok: true,
|
|
1024
|
+
// What to type once it is open. The UI shows this; the server never runs it.
|
|
1025
|
+
type_this: lane ? lane.cmd : null,
|
|
1026
|
+
cwd: ctx.projectRoot,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// v0.50.0 — THE CONNECTION TEST, and the one place this panel does something
|
|
1031
|
+
// model-shaped. It is a DIAGNOSTIC in the same family as `orc doctor`: rung 1
|
|
1032
|
+
// lists models and costs nothing, rung 2 sends a one-token completion and
|
|
1033
|
+
// costs a fraction of a cent, and the CLI decides which — never this file.
|
|
1034
|
+
//
|
|
1035
|
+
// It is POST because it MUTATES: a green test writes `verified_at` onto the
|
|
1036
|
+
// profile, and a red one on a never-verified profile REMOVES that profile
|
|
1037
|
+
// (the CLI's own test-first-then-store lifecycle). A GET that did that would
|
|
1038
|
+
// be reachable by a prefetch.
|
|
1039
|
+
//
|
|
1040
|
+
// A pasted key arrives in the BODY and leaves on the child's STDIN — line 1
|
|
1041
|
+
// the key, an optional line 2 the passphrase that encrypts it. It is never in
|
|
1042
|
+
// argv, never written here, and never echoed back: the response is the CLI's
|
|
1043
|
+
// own `--json` object, which carries no credential by construction.
|
|
1044
|
+
if (route === "/api/extra/ping") {
|
|
1045
|
+
if (job && job.running) return json(res, 409, { error: "busy", job: jobView() });
|
|
1046
|
+
const profile = String(body.profile || "");
|
|
1047
|
+
if (!profile) return json(res, 400, { error: "missing argument" });
|
|
1048
|
+
const argv = ["extra", "ping", profile];
|
|
1049
|
+
// v0.51.0 — the PAID rung, opt-in and never a default. The panel quotes what
|
|
1050
|
+
// it costs before the button; the CLI is what decides the rung and what it
|
|
1051
|
+
// reports back.
|
|
1052
|
+
if (body.live) argv.push("--live");
|
|
1053
|
+
if (body.model) argv.push("--model", String(body.model));
|
|
1054
|
+
let input;
|
|
1055
|
+
if (body.key) {
|
|
1056
|
+
// A NEW key: line 1 the key, an optional line 2 the passphrase that stores
|
|
1057
|
+
// it after a green test.
|
|
1058
|
+
argv.push("--key-stdin");
|
|
1059
|
+
input = String(body.key) + chr10 + String(body.passphrase || "") + chr10;
|
|
1060
|
+
} else if (body.passphrase) {
|
|
1061
|
+
// A STORED key: the passphrase decrypts it into the CLI's memory for the
|
|
1062
|
+
// probe. The two flags are mutually exclusive and the CLI refuses them
|
|
1063
|
+
// together BY NAME, so this branch is an `else if` rather than a guess.
|
|
1064
|
+
argv.push("--passphrase-stdin");
|
|
1065
|
+
input = String(body.passphrase) + chr10;
|
|
1066
|
+
}
|
|
1067
|
+
const out = runCli(argv, ctx, { json: true, input });
|
|
1068
|
+
clearCache();
|
|
1069
|
+
// `ok` here is "did the CLI answer at all". Whether the CONNECTION worked is
|
|
1070
|
+
// `data.ok` and the exit code, which are the CLI's answer and are passed
|
|
1071
|
+
// through untouched — a failed probe is DATA, not a server error.
|
|
1072
|
+
return json(res, out.ok ? 200 : 500, out.ok
|
|
1073
|
+
? { ok: true, exit_code: out.exit_code, data: out.data, command: out.command }
|
|
1074
|
+
: { ...out, error: readFailReason(out) });
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// v0.51.0 — F5's answer, scoped to ONE model id. A model that is LISTED can
|
|
1078
|
+
// still be DEAD upstream, so a dropdown is a list of what is OFFERED and never
|
|
1079
|
+
// a list of what WORKS. This is POST because it spends money.
|
|
1080
|
+
if (route === "/api/extra/models/test") {
|
|
1081
|
+
if (job && job.running) return json(res, 409, { error: "busy", job: jobView() });
|
|
1082
|
+
const profile = String(body.profile || "");
|
|
1083
|
+
const model = String(body.model || "");
|
|
1084
|
+
if (!profile || !model) return json(res, 400, { error: "missing argument" });
|
|
1085
|
+
const argv = ["extra", "models", profile, "--test", model];
|
|
1086
|
+
let input;
|
|
1087
|
+
if (body.passphrase) {
|
|
1088
|
+
argv.push("--passphrase-stdin");
|
|
1089
|
+
input = String(body.passphrase) + chr10;
|
|
1090
|
+
}
|
|
1091
|
+
const out = runCli(argv, ctx, { json: true, input });
|
|
1092
|
+
clearCache();
|
|
1093
|
+
return json(res, out.ok ? 200 : 500, out.ok
|
|
1094
|
+
? { ok: true, exit_code: out.exit_code, data: out.data, command: out.command }
|
|
1095
|
+
: { ...out, error: readFailReason(out) });
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// v0.52.0 — SAVING THE PASSPHRASE WITH A DEADLINE. The CLI tests it against
|
|
1099
|
+
// the vault before it stores anything (test first, then store), validates the
|
|
1100
|
+
// TTL against the same closed set the config key publishes, and answers with
|
|
1101
|
+
// the DATE. This route composes nothing: it hands over a profile, a number of
|
|
1102
|
+
// days, and a passphrase on stdin.
|
|
1103
|
+
if (route === "/api/extra/session/save") {
|
|
1104
|
+
if (job && job.running) return json(res, 409, { error: "busy", job: jobView() });
|
|
1105
|
+
const profile = String(body.profile || "");
|
|
1106
|
+
const ttl = String(body.ttl_days || "");
|
|
1107
|
+
if (!profile || !ttl) return json(res, 400, { error: "missing argument" });
|
|
1108
|
+
const out = runCli(["extra", "session", profile, "--save", "--ttl", ttl], ctx, {
|
|
1109
|
+
json: true,
|
|
1110
|
+
input: String(body.passphrase || "") + chr10,
|
|
1111
|
+
});
|
|
1112
|
+
clearCache();
|
|
1113
|
+
return json(res, out.ok ? 200 : 500, out.ok
|
|
1114
|
+
? { ok: true, exit_code: out.exit_code, data: out.data, command: out.command }
|
|
1115
|
+
: { ...out, error: readFailReason(out) });
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// v0.50.0 — proving a passphrase, which is the ONE action that clears the
|
|
1119
|
+
// vault's countdown. It NEVER yields the key: `orc extra unlock` answers one
|
|
1120
|
+
// question with a yes or a no, and its `attempt N of 10` message is the whole
|
|
1121
|
+
// point of the feature, so it is passed back verbatim.
|
|
1122
|
+
if (route === "/api/extra/unlock") {
|
|
1123
|
+
if (job && job.running) return json(res, 409, { error: "busy", job: jobView() });
|
|
1124
|
+
const profile = String(body.profile || "");
|
|
1125
|
+
if (!profile) return json(res, 400, { error: "missing argument" });
|
|
1126
|
+
const out = runCli(["extra", "unlock", profile], ctx, {
|
|
1127
|
+
json: true,
|
|
1128
|
+
input: String(body.passphrase || "") + chr10,
|
|
1129
|
+
});
|
|
1130
|
+
clearCache();
|
|
1131
|
+
return json(res, out.ok ? 200 : 500, out.ok
|
|
1132
|
+
? { ok: true, exit_code: out.exit_code, data: out.data, command: out.command }
|
|
1133
|
+
: { ...out, error: readFailReason(out) });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Hand the panel over to a fresh process on the SAME port and token, so the
|
|
1137
|
+
// open tab only has to reload. POST-only like every other mutation, and it is
|
|
1138
|
+
// a mutation: the process answering the next request is not this one.
|
|
1139
|
+
//
|
|
1140
|
+
// The CLIENT asks for this — never the job's own close handler. The job's
|
|
1141
|
+
// output lives in this process's memory, so restarting the instant a command
|
|
1142
|
+
// finished would destroy the record of what it did before anyone read it.
|
|
1143
|
+
if (route === "/api/ui/restart") {
|
|
1144
|
+
if (ctx.fixtures)
|
|
1145
|
+
return json(res, 400, { ok: false, reason: "fixtures", error: "fixture mode serves canned data; there is nothing to restart into." });
|
|
1146
|
+
if (job && job.running) return json(res, 409, { ok: false, reason: "busy", error: "a command is still running.", job: jobView() });
|
|
1147
|
+
const out = typeof ctx.restart === "function" ? ctx.restart() : { ok: false, reason: "unsupported" };
|
|
1148
|
+
// A failed handover is NOT fatal and never takes the running panel down:
|
|
1149
|
+
// the old server keeps serving, and the client is told to do it by hand.
|
|
1150
|
+
return json(res, out.ok ? 200 : 500, out);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (route === "/api/maintenance/apply") {
|
|
1154
|
+
const m = MAINTENANCE[String(body.action)];
|
|
1155
|
+
if (!m) return json(res, 400, { error: "unknown action" });
|
|
1156
|
+
const started = startJob(m.apply, ctx, { restartUi: !!m.restarts_ui });
|
|
1157
|
+
if (started.error) return json(res, 409, started);
|
|
1158
|
+
return json(res, 200, { ok: true, ...started });
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const build = WRITES[route];
|
|
1162
|
+
if (!build) return json(res, 404, { error: "unknown endpoint " + route });
|
|
1163
|
+
if (job && job.running) return json(res, 409, { error: "busy", job: jobView() });
|
|
1164
|
+
let argv;
|
|
1165
|
+
try {
|
|
1166
|
+
argv = build(body);
|
|
1167
|
+
} catch (_) {
|
|
1168
|
+
return json(res, 400, { error: "bad request body" });
|
|
1169
|
+
}
|
|
1170
|
+
if (argv.some((a) => a === "undefined" || a === "null" || a === ""))
|
|
1171
|
+
return json(res, 400, { error: "missing argument" });
|
|
1172
|
+
const out = runCli(argv, ctx);
|
|
1173
|
+
clearCache();
|
|
1174
|
+
// A write's exit code is a REAL failure signal (validators exit 1), unlike a
|
|
1175
|
+
// read's — so it is reported as such, with the CLI's own message.
|
|
1176
|
+
return json(res, 200, {
|
|
1177
|
+
ok: out.exit_code === 0,
|
|
1178
|
+
exit_code: out.exit_code,
|
|
1179
|
+
command: out.command,
|
|
1180
|
+
// Writes print human text, not JSON — that IS the confirmation to show.
|
|
1181
|
+
output: (out.stdout + (out.stderr ? "\n" + out.stderr : "")).trim(),
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// `orc upgrade` replaces the package while your working tree may hold changes.
|
|
1186
|
+
// Worth a warning before, not a surprise after.
|
|
1187
|
+
function isDirtyTree(ctx) {
|
|
1188
|
+
try {
|
|
1189
|
+
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
1190
|
+
cwd: ctx.projectRoot,
|
|
1191
|
+
encoding: "utf8",
|
|
1192
|
+
windowsHide: true,
|
|
1193
|
+
timeout: 5000,
|
|
1194
|
+
});
|
|
1195
|
+
return r.status === 0 && !!(r.stdout || "").trim();
|
|
1196
|
+
} catch (_) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
module.exports = { handleApi, clearCache, READS, WRITES, MAINTENANCE };
|