canary-test-cli 5.7.0 → 5.11.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.
@@ -0,0 +1,390 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.classifyCloneFailure = classifyCloneFailure;
37
+ exports.add = add;
38
+ exports.skillCount = skillCount;
39
+ exports.workingTreeStatus = workingTreeStatus;
40
+ exports.freshness = freshness;
41
+ exports.list = list;
42
+ exports.update = update;
43
+ exports.remove = remove;
44
+ const node_child_process_1 = require("node:child_process");
45
+ const fs = __importStar(require("node:fs"));
46
+ const os = __importStar(require("node:os"));
47
+ const path = __importStar(require("node:path"));
48
+ const source_spec_js_1 = require("./source-spec.js");
49
+ const registry = __importStar(require("./overlays-registry.js"));
50
+ const doctor_manifest_js_1 = require("./doctor-manifest.js");
51
+ const defaultGit = (args, opts = {}) => {
52
+ const r = (0, node_child_process_1.spawnSync)("git", args, { cwd: opts.cwd, encoding: "utf8" });
53
+ if (r.error) {
54
+ const code = r.error.code;
55
+ return { status: code === "ENOENT" ? 127 : 1, stdout: "", stderr: String(r.error.message) };
56
+ }
57
+ return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
58
+ };
59
+ function today() {
60
+ return new Date().toISOString().slice(0, 10);
61
+ }
62
+ /** Default consent prompt: reads a line from the TTY; declines when non-interactive. */
63
+ function defaultConfirm(question) {
64
+ if (!process.stdin.isTTY) {
65
+ return false;
66
+ }
67
+ process.stdout.write(question);
68
+ const buf = Buffer.alloc(256);
69
+ try {
70
+ const n = fs.readSync(0, buf, 0, buf.length, null);
71
+ const answer = buf.toString("utf8", 0, n).trim().toLowerCase();
72
+ return answer === "y" || answer === "yes";
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Consent for an overlay's `command-succeeds` doctor checks, collected at add
80
+ * time. Overlays with no such checks (or a malformed manifest) record no
81
+ * consent (`null`) — there is nothing to gate, and a bad manifest surfaces
82
+ * later in `doctor`. Otherwise the check list is printed and confirmed.
83
+ */
84
+ function collectConsent(dest, name, confirm, out) {
85
+ const load = (0, doctor_manifest_js_1.loadManifest)(dest);
86
+ if (!load.ok) {
87
+ return { consent: null, consentCommandsHash: null };
88
+ }
89
+ const hash = (0, doctor_manifest_js_1.commandSucceedsHash)(load.checks);
90
+ if (hash === null) {
91
+ return { consent: null, consentCommandsHash: null };
92
+ }
93
+ const cmds = load.checks.filter((c) => c.type === "command-succeeds");
94
+ out.write(`Overlay "${name}" ships ${cmds.length} command check(s) that 'canary doctor' can run:\n`);
95
+ for (const c of cmds) {
96
+ out.write(` - ${c.id}: ${(c.command ?? []).join(" ")}\n`);
97
+ }
98
+ const granted = confirm(`Allow 'canary doctor' to run these commands for "${name}"? [y/N] `);
99
+ if (!granted) {
100
+ out.write("Declined — 'canary doctor' will skip these command checks. Re-add the overlay to change this.\n");
101
+ }
102
+ return { consent: granted, consentCommandsHash: hash };
103
+ }
104
+ /** Ordered stderr-substring signatures for `git clone` failure classification. */
105
+ const CLONE_FAILURE_SIGNATURES = [
106
+ { reason: "git not found on PATH", needles: ["enoent", "not found: git"] },
107
+ {
108
+ reason: "network unreachable",
109
+ needles: ["could not resolve host", "network is unreachable", "failed to connect", "timed out"],
110
+ },
111
+ {
112
+ reason: "authentication denied",
113
+ needles: ["authentication failed", "permission denied", "could not read username", "access denied", "403 forbidden"],
114
+ },
115
+ { reason: "repository not found", needles: ["repository not found", "does not exist", "not found"] },
116
+ ];
117
+ /** Best-effort classification of a failed `git clone`, for a useful remedy. */
118
+ function classifyCloneFailure(res) {
119
+ const s = res.stderr.toLowerCase();
120
+ if (res.status === 127) {
121
+ return "git not found on PATH";
122
+ }
123
+ for (const { reason, needles } of CLONE_FAILURE_SIGNATURES) {
124
+ if (needles.some((needle) => s.includes(needle))) {
125
+ return reason;
126
+ }
127
+ }
128
+ return "unknown error";
129
+ }
130
+ /**
131
+ * `canary overlay add <source> [--ref <tag>]` — clone a tracked overlay into
132
+ * `~/.canary/overlays/<name>/` and register it. Returns a process exit code.
133
+ * Nothing is registered unless the clone succeeds.
134
+ */
135
+ function add(source, options = {}, deps = {}) {
136
+ const git = deps.git ?? defaultGit;
137
+ const homeDir = deps.homeDir ?? os.homedir();
138
+ const out = deps.out ?? process.stdout;
139
+ const err = deps.err ?? process.stderr;
140
+ const stamp = deps.now ?? today;
141
+ const ref = options.ref ?? null;
142
+ let parsed;
143
+ try {
144
+ parsed = (0, source_spec_js_1.parseSource)(source);
145
+ }
146
+ catch (e) {
147
+ if (e instanceof source_spec_js_1.SourceSpecError) {
148
+ err.write(`canary overlay add: ${e.message}\n`);
149
+ return 1;
150
+ }
151
+ throw e;
152
+ }
153
+ let reg;
154
+ try {
155
+ reg = registry.read(homeDir);
156
+ }
157
+ catch (e) {
158
+ err.write(`canary overlay add: ${e.message}\n`);
159
+ return 1;
160
+ }
161
+ // Idempotent: re-adding a registered overlay is a no-op with an update hint.
162
+ if (registry.get(reg, parsed.name)) {
163
+ out.write(`overlay "${parsed.name}" is already added — run 'canary overlay update ${parsed.name}' to refresh it.\n`);
164
+ return 0;
165
+ }
166
+ const dest = registry.clonePath(parsed.name, homeDir);
167
+ if (fs.existsSync(dest)) {
168
+ err.write(`canary overlay add: ${dest} already exists but is not registered — remove it and retry.\n`);
169
+ return 1;
170
+ }
171
+ fs.mkdirSync(registry.overlaysDir(homeDir), { recursive: true });
172
+ const args = ["clone", "--quiet"];
173
+ if (ref) {
174
+ args.push("--branch", ref);
175
+ }
176
+ args.push(parsed.cloneUrl, dest);
177
+ const res = git(args);
178
+ if (res.status !== 0) {
179
+ fs.rmSync(dest, { recursive: true, force: true }); // never leave a partial clone
180
+ const reason = classifyCloneFailure(res);
181
+ err.write(`canary overlay add: clone failed (${reason}).\n` +
182
+ (res.stderr.trim() ? `${res.stderr.trim()}\n` : "") +
183
+ `Nothing was registered. Check the overlay's access docs and your git credentials.\n`);
184
+ return 1;
185
+ }
186
+ const confirm = deps.confirm ?? defaultConfirm;
187
+ const { consent, consentCommandsHash } = collectConsent(dest, parsed.name, confirm, out);
188
+ const entry = {
189
+ name: parsed.name,
190
+ source,
191
+ ref,
192
+ path: dest,
193
+ addedDate: stamp(),
194
+ consent,
195
+ consentCommandsHash,
196
+ };
197
+ try {
198
+ registry.write(registry.add(reg, entry), homeDir);
199
+ }
200
+ catch (e) {
201
+ fs.rmSync(dest, { recursive: true, force: true });
202
+ err.write(`canary overlay add: ${e.message}\n`);
203
+ return 1;
204
+ }
205
+ out.write(`Added overlay "${parsed.name}"${ref ? ` @ ${ref}` : ""} → ${dest}\n`);
206
+ return 0;
207
+ }
208
+ /** Count `.canary/skills/<name>/SKILL.md` entries in a clone. */
209
+ function skillCount(dest) {
210
+ const skillsDir = path.join(dest, ".canary", "skills");
211
+ let entries;
212
+ try {
213
+ entries = fs.readdirSync(skillsDir, { withFileTypes: true });
214
+ }
215
+ catch {
216
+ return 0;
217
+ }
218
+ return entries.filter((d) => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, "SKILL.md"))).length;
219
+ }
220
+ /**
221
+ * Whether a clone's working tree is clean, dirty (local modifications), or its
222
+ * git status is unreadable. Shared by `overlay update` (refuses on dirty) and
223
+ * the `doctor` engine check ("no local overlay modifications").
224
+ */
225
+ function workingTreeStatus(dest, git) {
226
+ const status = git(["status", "--porcelain"], { cwd: dest });
227
+ if (status.status !== 0) {
228
+ return "unreadable";
229
+ }
230
+ return status.stdout.trim() === "" ? "clean" : "dirty";
231
+ }
232
+ /**
233
+ * Freshness of a clone against its LOCAL knowledge of the upstream — no fetch
234
+ * is performed (that is `overlay update`'s job). Returns a human-readable
235
+ * status string.
236
+ */
237
+ function freshness(dest, entry, git) {
238
+ if (!fs.existsSync(dest)) {
239
+ return "missing — clone not found (run 'canary overlay update' or re-add)";
240
+ }
241
+ const behind = git(["rev-list", "--count", "HEAD..@{u}"], { cwd: dest });
242
+ if (behind.status !== 0) {
243
+ // No upstream tracking ref — typically a pinned tag/detached HEAD.
244
+ return entry.ref ? `pinned @ ${entry.ref}` : "unknown (no upstream tracking ref)";
245
+ }
246
+ const n = Number.parseInt(behind.stdout.trim(), 10);
247
+ if (!Number.isFinite(n) || n === 0) {
248
+ return "up to date";
249
+ }
250
+ return `${n} commit${n === 1 ? "" : "s"} behind`;
251
+ }
252
+ /**
253
+ * `canary overlay list` — one block per registered overlay: name, source, ref,
254
+ * freshness, and skill count.
255
+ */
256
+ function list(deps = {}) {
257
+ const git = deps.git ?? defaultGit;
258
+ const homeDir = deps.homeDir ?? os.homedir();
259
+ const out = deps.out ?? process.stdout;
260
+ const err = deps.err ?? process.stderr;
261
+ let reg;
262
+ try {
263
+ reg = registry.read(homeDir);
264
+ }
265
+ catch (e) {
266
+ err.write(`canary overlay list: ${e.message}\n`);
267
+ return 1;
268
+ }
269
+ if (reg.overlays.length === 0) {
270
+ out.write("No overlays added. Add one with 'canary overlay add <source>'.\n");
271
+ return 0;
272
+ }
273
+ for (const o of reg.overlays) {
274
+ out.write(`${o.name}\n`);
275
+ out.write(` source: ${o.source}\n`);
276
+ out.write(` ref: ${o.ref ?? "(default branch)"}\n`);
277
+ out.write(` status: ${freshness(o.path, o, git)}\n`);
278
+ out.write(` skills: ${skillCount(o.path)}\n`);
279
+ }
280
+ return 0;
281
+ }
282
+ /** Update one overlay clone. Returns 0 on success, 1 on refusal/failure. */
283
+ function updateOne(o, git, out, err) {
284
+ if (!fs.existsSync(o.path)) {
285
+ err.write(`overlay "${o.name}": clone missing at ${o.path} — remove and re-add it.\n`);
286
+ return 1;
287
+ }
288
+ const clean = workingTreeStatus(o.path, git);
289
+ if (clean === "unreadable") {
290
+ err.write(`overlay "${o.name}": cannot read git status at ${o.path}.\n`);
291
+ return 1;
292
+ }
293
+ if (clean === "dirty") {
294
+ err.write(`overlay "${o.name}": local modifications in ${o.path} — refusing to update. ` +
295
+ `Commit/stash them, or 'canary overlay remove ${o.name}' and re-add.\n`);
296
+ return 1;
297
+ }
298
+ if (o.ref) {
299
+ // Pinned to a tag/branch: fetch (incl. tags), then re-checkout the ref.
300
+ const fetch = git(["fetch", "--quiet", "--tags", "origin"], { cwd: o.path });
301
+ if (fetch.status !== 0) {
302
+ err.write(`overlay "${o.name}": fetch failed.\n${fetch.stderr.trim()}\n`);
303
+ return 1;
304
+ }
305
+ const co = git(["checkout", "--quiet", o.ref], { cwd: o.path });
306
+ if (co.status !== 0) {
307
+ err.write(`overlay "${o.name}": checkout ${o.ref} failed.\n${co.stderr.trim()}\n`);
308
+ return 1;
309
+ }
310
+ out.write(`overlay "${o.name}": fetched, pinned @ ${o.ref}.\n`);
311
+ return 0;
312
+ }
313
+ const pull = git(["pull", "--ff-only", "--quiet"], { cwd: o.path });
314
+ if (pull.status !== 0) {
315
+ err.write(`overlay "${o.name}": cannot fast-forward ${o.path} ` +
316
+ `(diverged or rewritten history) — 'canary overlay remove ${o.name}' and re-add.\n` +
317
+ (pull.stderr.trim() ? `${pull.stderr.trim()}\n` : ""));
318
+ return 1;
319
+ }
320
+ out.write(`overlay "${o.name}": updated.\n`);
321
+ return 0;
322
+ }
323
+ /**
324
+ * `canary overlay update [name]` — fast-forward tracked overlays. With no name,
325
+ * updates all; refuses on local modifications or a non-fast-forward.
326
+ */
327
+ function update(name, deps = {}) {
328
+ const git = deps.git ?? defaultGit;
329
+ const homeDir = deps.homeDir ?? os.homedir();
330
+ const out = deps.out ?? process.stdout;
331
+ const err = deps.err ?? process.stderr;
332
+ let reg;
333
+ try {
334
+ reg = registry.read(homeDir);
335
+ }
336
+ catch (e) {
337
+ err.write(`canary overlay update: ${e.message}\n`);
338
+ return 1;
339
+ }
340
+ let targets;
341
+ if (name) {
342
+ const entry = registry.get(reg, name);
343
+ if (!entry) {
344
+ err.write(`canary overlay update: no overlay named "${name}".\n`);
345
+ return 1;
346
+ }
347
+ targets = [entry];
348
+ }
349
+ else {
350
+ if (reg.overlays.length === 0) {
351
+ out.write("No overlays to update.\n");
352
+ return 0;
353
+ }
354
+ targets = reg.overlays;
355
+ }
356
+ let failures = 0;
357
+ for (const o of targets) {
358
+ if (updateOne(o, git, out, err) !== 0) {
359
+ failures += 1;
360
+ }
361
+ }
362
+ return failures === 0 ? 0 : 1;
363
+ }
364
+ /**
365
+ * `canary overlay remove <name>` — deregister an overlay and delete its clone.
366
+ * Unknown name is an error; the registry is left unchanged in that case.
367
+ */
368
+ function remove(name, deps = {}) {
369
+ const homeDir = deps.homeDir ?? os.homedir();
370
+ const out = deps.out ?? process.stdout;
371
+ const err = deps.err ?? process.stderr;
372
+ let reg;
373
+ try {
374
+ reg = registry.read(homeDir);
375
+ }
376
+ catch (e) {
377
+ err.write(`canary overlay remove: ${e.message}\n`);
378
+ return 1;
379
+ }
380
+ const entry = registry.get(reg, name);
381
+ if (!entry) {
382
+ err.write(`canary overlay remove: no overlay named "${name}".\n`);
383
+ return 1;
384
+ }
385
+ fs.rmSync(entry.path, { recursive: true, force: true });
386
+ const { registry: next } = registry.remove(reg, name);
387
+ registry.write(next, homeDir);
388
+ out.write(`Removed overlay "${name}".\n`);
389
+ return 0;
390
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RegistryError = exports.SCHEMA_VERSION = void 0;
37
+ exports.canaryHome = canaryHome;
38
+ exports.registryPath = registryPath;
39
+ exports.overlaysDir = overlaysDir;
40
+ exports.clonePath = clonePath;
41
+ exports.emptyRegistry = emptyRegistry;
42
+ exports.read = read;
43
+ exports.consentGranted = consentGranted;
44
+ exports.write = write;
45
+ exports.get = get;
46
+ exports.list = list;
47
+ exports.add = add;
48
+ exports.remove = remove;
49
+ const fs = __importStar(require("node:fs"));
50
+ const os = __importStar(require("node:os"));
51
+ const path = __importStar(require("node:path"));
52
+ /**
53
+ * Reader/writer for `~/.canary/overlays.json` — the tracked-overlay registry.
54
+ *
55
+ * Cross-runtime contract (spec Assumption): this file is written ONLY by the
56
+ * TS side. The Python skill loader never parses it; it directory-scans the
57
+ * `~/.canary/overlays/<name>/` clone dirs instead. So the schema here serves
58
+ * the `overlay` commands alone.
59
+ */
60
+ exports.SCHEMA_VERSION = 1;
61
+ /** Raised when the registry file exists but cannot be read or parsed. */
62
+ class RegistryError extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = "RegistryError";
66
+ }
67
+ }
68
+ exports.RegistryError = RegistryError;
69
+ function canaryHome(homeDir = os.homedir()) {
70
+ return path.join(homeDir, ".canary");
71
+ }
72
+ function registryPath(homeDir = os.homedir()) {
73
+ return path.join(canaryHome(homeDir), "overlays.json");
74
+ }
75
+ function overlaysDir(homeDir = os.homedir()) {
76
+ return path.join(canaryHome(homeDir), "overlays");
77
+ }
78
+ function clonePath(name, homeDir = os.homedir()) {
79
+ return path.join(overlaysDir(homeDir), name);
80
+ }
81
+ function emptyRegistry() {
82
+ return { schemaVersion: exports.SCHEMA_VERSION, overlays: [] };
83
+ }
84
+ /**
85
+ * Read the registry. A missing file is an empty registry (not an error); a
86
+ * present-but-unparseable file throws {@link RegistryError} so callers can
87
+ * report it rather than silently discarding overlays.
88
+ */
89
+ /** Parse registry JSON, throwing a RegistryError on unreadable/malformed input. */
90
+ function parseRegistryFile(file) {
91
+ let raw;
92
+ try {
93
+ raw = fs.readFileSync(file, "utf8");
94
+ }
95
+ catch (err) {
96
+ if (err.code === "ENOENT") {
97
+ return emptyRegistry();
98
+ }
99
+ throw new RegistryError(`cannot read ${file}: ${err.message}`);
100
+ }
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(raw);
104
+ }
105
+ catch (err) {
106
+ throw new RegistryError(`malformed ${file}: ${err.message}`);
107
+ }
108
+ const overlays = parsed?.overlays;
109
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(overlays)) {
110
+ throw new RegistryError(`malformed ${file}: expected { schemaVersion, overlays: [] }`);
111
+ }
112
+ return parsed;
113
+ }
114
+ /** Normalize forward-added optional fields so callers never see `undefined`. */
115
+ function normalizeEntry(o) {
116
+ return { ...o, consent: o.consent ?? null, consentCommandsHash: o.consentCommandsHash ?? null };
117
+ }
118
+ function read(homeDir = os.homedir()) {
119
+ const reg = parseRegistryFile(registryPath(homeDir));
120
+ return {
121
+ schemaVersion: typeof reg.schemaVersion === "number" ? reg.schemaVersion : exports.SCHEMA_VERSION,
122
+ overlays: reg.overlays.map(normalizeEntry),
123
+ };
124
+ }
125
+ /**
126
+ * Whether `command-succeeds` doctor checks may run for an overlay: consent must
127
+ * have been granted (`true`) AND still cover the live manifest's command set
128
+ * (`liveHash` matching the recorded fingerprint). A changed manifest (hash
129
+ * mismatch) revokes consent until it is re-confirmed at `overlay add`.
130
+ */
131
+ function consentGranted(entry, liveHash) {
132
+ return entry.consent === true && liveHash !== null && entry.consentCommandsHash === liveHash;
133
+ }
134
+ /** Write the registry atomically (temp file + rename), creating `~/.canary`. */
135
+ function write(registry, homeDir = os.homedir()) {
136
+ fs.mkdirSync(canaryHome(homeDir), { recursive: true });
137
+ const file = registryPath(homeDir);
138
+ const tmp = `${file}.tmp`;
139
+ fs.writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
140
+ fs.renameSync(tmp, file);
141
+ }
142
+ function get(registry, name) {
143
+ return registry.overlays.find((o) => o.name === name) ?? null;
144
+ }
145
+ function list(registry) {
146
+ return [...registry.overlays];
147
+ }
148
+ /** Add an entry. Throws if one with the same name is already registered. */
149
+ function add(registry, entry) {
150
+ if (get(registry, entry.name)) {
151
+ throw new RegistryError(`overlay "${entry.name}" is already registered`);
152
+ }
153
+ return { ...registry, overlays: [...registry.overlays, entry] };
154
+ }
155
+ /** Remove an entry by name. Returns the new registry and whether it existed. */
156
+ function remove(registry, name) {
157
+ const kept = registry.overlays.filter((o) => o.name !== name);
158
+ return {
159
+ registry: { ...registry, overlays: kept },
160
+ removed: kept.length !== registry.overlays.length,
161
+ };
162
+ }
package/dist/router.js ADDED
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TS_COMMANDS = void 0;
37
+ exports.isTsCommand = isTsCommand;
38
+ exports.route = route;
39
+ /**
40
+ * Router for TS-handled `canary` subcommands — the strangler seam (Decision 10).
41
+ *
42
+ * The npm shim (`bin/canary.js`) forwards every command to the bundled Python
43
+ * binary except the ones handled here in TypeScript. Phase 1 wires `overlay`;
44
+ * `doctor` slots into the same table in Phase 2.
45
+ *
46
+ * `route()` returns a process exit code, or `null` when the command is not
47
+ * TS-handled and the shim should fall through to the Python binary.
48
+ */
49
+ const overlay = __importStar(require("./overlay-commands.js"));
50
+ const doctor_js_1 = require("./doctor.js");
51
+ /** Subcommands handled in TypeScript rather than forwarded to the binary. */
52
+ exports.TS_COMMANDS = ["overlay", "doctor"];
53
+ /** True when `argv` (process.argv.slice(2)) targets a TS-handled command. */
54
+ function isTsCommand(argv) {
55
+ return argv.length > 0 && exports.TS_COMMANDS.includes(argv[0]);
56
+ }
57
+ /** Minimal flag parser: `--k v`, `--k=v`, and boolean `--k`. */
58
+ function parseArgs(args) {
59
+ const positionals = [];
60
+ const flags = {};
61
+ for (let i = 0; i < args.length; i += 1) {
62
+ const a = args[i];
63
+ if (a.startsWith("--")) {
64
+ const eq = a.indexOf("=");
65
+ if (eq !== -1) {
66
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
67
+ }
68
+ else {
69
+ const key = a.slice(2);
70
+ const next = args[i + 1];
71
+ if (next !== undefined && !next.startsWith("--")) {
72
+ flags[key] = next;
73
+ i += 1;
74
+ }
75
+ else {
76
+ flags[key] = true;
77
+ }
78
+ }
79
+ }
80
+ else {
81
+ positionals.push(a);
82
+ }
83
+ }
84
+ return { positionals, flags };
85
+ }
86
+ function refFrom(flags) {
87
+ const ref = flags.ref;
88
+ return typeof ref === "string" ? ref : null;
89
+ }
90
+ const OVERLAY_USAGE = "usage: canary overlay <add|list|update|remove> [args]\n" +
91
+ " add <source> [--ref <tag>] list update [name] remove <name>\n";
92
+ /** Overlay subcommand handlers, keyed by name. Each returns a process exit code. */
93
+ const OVERLAY_SUBCOMMANDS = {
94
+ add: ({ positionals, flags, deps, err }) => {
95
+ if (positionals.length < 1) {
96
+ err.write("usage: canary overlay add <source> [--ref <tag>] [--yes]\n");
97
+ return 1;
98
+ }
99
+ // `--yes` grants command-check consent non-interactively (CI); default prompts.
100
+ const addDeps = flags.yes === true ? { ...deps, confirm: () => true } : deps;
101
+ return overlay.add(positionals[0], { ref: refFrom(flags) }, addDeps);
102
+ },
103
+ list: ({ deps }) => overlay.list(deps),
104
+ update: ({ positionals, deps }) => overlay.update(positionals[0] ?? null, deps),
105
+ remove: ({ positionals, deps, err }) => {
106
+ if (positionals.length < 1) {
107
+ err.write("usage: canary overlay remove <name>\n");
108
+ return 1;
109
+ }
110
+ return overlay.remove(positionals[0], deps);
111
+ },
112
+ };
113
+ function runOverlay(args, deps) {
114
+ const err = deps.err ?? process.stderr;
115
+ const handler = OVERLAY_SUBCOMMANDS[args[0]];
116
+ if (!handler) {
117
+ err.write(`canary overlay: unknown subcommand ${args[0] ? `'${args[0]}'` : "(none)"}\n${OVERLAY_USAGE}`);
118
+ return 1;
119
+ }
120
+ const { positionals, flags } = parseArgs(args.slice(1));
121
+ return handler({ positionals, flags, deps, err });
122
+ }
123
+ /**
124
+ * Dispatch a TS-handled command. Returns the process exit code (or a Promise of
125
+ * one for async commands like `doctor`), or `null` when the command should fall
126
+ * through to the Python binary. `deps` is threaded to the command handlers for
127
+ * testing (real dependencies by default).
128
+ */
129
+ function route(argv, deps = {}) {
130
+ if (!isTsCommand(argv)) {
131
+ return null;
132
+ }
133
+ if (argv[0] === "overlay") {
134
+ return runOverlay(argv.slice(1), deps);
135
+ }
136
+ if (argv[0] === "doctor") {
137
+ return (0, doctor_js_1.runDoctor)(argv.slice(1), deps);
138
+ }
139
+ return null;
140
+ }