canary-test-cli 5.7.0 → 5.10.1

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/bin/canary ADDED
Binary file
package/bin/canary.js CHANGED
@@ -4,27 +4,59 @@
4
4
  const { execFileSync } = require("node:child_process");
5
5
  const path = require("node:path");
6
6
  const fs = require("node:fs");
7
+ const { isTsCommand, route } = require("../dist/router.js");
7
8
 
8
9
  function getBinaryPath(platform) {
9
10
  const name = platform === "win32" ? "canary.exe" : "canary";
10
11
  return path.join(__dirname, name);
11
12
  }
12
13
 
13
- function main() {
14
- const binaryPath = getBinaryPath(process.platform);
15
- if (!fs.existsSync(binaryPath)) {
16
- process.stderr.write(
14
+ /**
15
+ * Forward a command to the bundled Python binary. Returns the exit code
16
+ * (0 on success, the binary's status on failure, 1 when the binary is missing).
17
+ * Dependencies are injectable for testing.
18
+ */
19
+ function forwardToBinary(
20
+ argv,
21
+ {
22
+ execFile = execFileSync,
23
+ existsSync = fs.existsSync,
24
+ platform = process.platform,
25
+ stderr = process.stderr,
26
+ } = {}
27
+ ) {
28
+ const binaryPath = getBinaryPath(platform);
29
+ if (!existsSync(binaryPath)) {
30
+ stderr.write(
17
31
  `canary binary not found at ${binaryPath}.\n` +
18
- `Try reinstalling: volta install canary-test-cli\n`
32
+ `Try reinstalling: npm install -g canary-test-cli\n`
19
33
  );
20
- process.exit(1);
34
+ return 1;
21
35
  }
22
36
  try {
23
- execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
37
+ execFile(binaryPath, argv, { stdio: "inherit" });
38
+ return 0;
24
39
  } catch (err) {
25
- process.exit(err.status ?? 1);
40
+ return err.status ?? 1;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Dispatch one invocation: TS-handled commands (e.g. `overlay`, `doctor`) go to
46
+ * the router; everything else forwards verbatim to the Python binary. Returns
47
+ * the process exit code, or a Promise of one for async commands (`doctor`).
48
+ */
49
+ function run(argv, deps = {}) {
50
+ if (isTsCommand(argv)) {
51
+ return route(argv, deps) ?? 0;
26
52
  }
53
+ return forwardToBinary(argv, deps);
54
+ }
55
+
56
+ function main() {
57
+ // `run` may return a number (sync commands) or a Promise<number> (doctor).
58
+ Promise.resolve(run(process.argv.slice(2))).then((code) => process.exit(code));
27
59
  }
28
60
 
29
- module.exports = { getBinaryPath };
61
+ module.exports = { getBinaryPath, forwardToBinary, run };
30
62
  if (require.main === module) main();
@@ -0,0 +1,285 @@
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.DEFAULT_CHECK_TIMEOUT_MS = exports.CHECK_TYPES = void 0;
37
+ exports.manifestPath = manifestPath;
38
+ exports.commandSucceedsHash = commandSucceedsHash;
39
+ exports.loadManifest = loadManifest;
40
+ exports.filterByPersona = filterByPersona;
41
+ exports.runCheck = runCheck;
42
+ /**
43
+ * Overlay check manifest — `<clone>/.canary/doctor.json` (Phase 2, tier 2).
44
+ *
45
+ * This module loads and validates the manifest and filters checks by persona.
46
+ * Execution of the individual check types lives in the runner (added in a
47
+ * later task). A malformed manifest never throws: {@link loadManifest} returns
48
+ * a single failing {@link CheckResult} so `doctor` can report it and carry on.
49
+ */
50
+ const node_crypto_1 = require("node:crypto");
51
+ const fs = __importStar(require("node:fs"));
52
+ const http = __importStar(require("node:http"));
53
+ const https = __importStar(require("node:https"));
54
+ const path = __importStar(require("node:path"));
55
+ const node_child_process_1 = require("node:child_process");
56
+ exports.CHECK_TYPES = ["file-exists", "url-reachable", "command-succeeds"];
57
+ /** Path to an overlay's manifest. */
58
+ function manifestPath(cloneDir) {
59
+ return path.join(cloneDir, ".canary", "doctor.json");
60
+ }
61
+ /**
62
+ * Stable fingerprint of a manifest's `command-succeeds` checks — the (id,
63
+ * command) pairs, sorted by id. Returns null when there are no such checks
64
+ * (nothing to gate). Consent is re-requested when this value changes.
65
+ */
66
+ function commandSucceedsHash(checks) {
67
+ const cmds = checks
68
+ .filter((c) => c.type === "command-succeeds")
69
+ .map((c) => ({ id: c.id, command: c.command ?? [] }))
70
+ .sort((a, b) => a.id.localeCompare(b.id));
71
+ if (cmds.length === 0) {
72
+ return null;
73
+ }
74
+ return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(cmds)).digest("hex");
75
+ }
76
+ /** True when `v` is a non-empty array of strings. */
77
+ function isStringArray(v) {
78
+ return Array.isArray(v) && v.every((a) => typeof a === "string");
79
+ }
80
+ /**
81
+ * Validate the type-specific field of a check. Returns an error string, or
82
+ * null when the field is well-formed for the given type.
83
+ */
84
+ function validateTypeField(type, c) {
85
+ if (type === "file-exists" && typeof c.path !== "string") {
86
+ return `check "${c.id}" (file-exists) is missing a string "path"`;
87
+ }
88
+ if (type === "url-reachable" && typeof c.url !== "string") {
89
+ return `check "${c.id}" (url-reachable) is missing a string "url"`;
90
+ }
91
+ if (type === "command-succeeds" && !(isStringArray(c.command) && c.command.length > 0)) {
92
+ return `check "${c.id}" (command-succeeds) needs a non-empty string[] "command"`;
93
+ }
94
+ return null;
95
+ }
96
+ /** Validate the id/type/remedy/persona fields common to every check type. */
97
+ function validateCommonFields(c, index) {
98
+ if (typeof c.id !== "string" || c.id === "") {
99
+ return `check[${index}] is missing a string "id"`;
100
+ }
101
+ if (typeof c.type !== "string" || !exports.CHECK_TYPES.includes(c.type)) {
102
+ return `check "${c.id}" has an unknown type (expected one of: ${exports.CHECK_TYPES.join(", ")})`;
103
+ }
104
+ if (typeof c.remedy !== "string" || c.remedy === "") {
105
+ return `check "${c.id}" is missing a string "remedy"`;
106
+ }
107
+ if (c.persona !== undefined && !isStringArray(c.persona)) {
108
+ return `check "${c.id}" has a non-string persona list`;
109
+ }
110
+ return null;
111
+ }
112
+ /** Validate one raw check entry. Returns the typed check or an error string. */
113
+ function validateCheck(raw, index) {
114
+ if (typeof raw !== "object" || raw === null) {
115
+ return `check[${index}] is not an object`;
116
+ }
117
+ const c = raw;
118
+ const commonError = validateCommonFields(c, index);
119
+ if (commonError) {
120
+ return commonError;
121
+ }
122
+ const type = c.type;
123
+ const typeError = validateTypeField(type, c);
124
+ if (typeError) {
125
+ return typeError;
126
+ }
127
+ return {
128
+ id: c.id,
129
+ type,
130
+ remedy: c.remedy,
131
+ persona: c.persona,
132
+ path: c.path,
133
+ url: c.url,
134
+ command: c.command,
135
+ };
136
+ }
137
+ /** Build the single failing check used when a manifest cannot be loaded. */
138
+ function manifestFailure(cloneDir, detail) {
139
+ const file = manifestPath(cloneDir);
140
+ return {
141
+ ok: false,
142
+ failure: {
143
+ id: `manifest:${file}`,
144
+ status: "fail",
145
+ label: `doctor.json is invalid (${file})`,
146
+ remedy: detail,
147
+ },
148
+ };
149
+ }
150
+ /**
151
+ * Load and validate `<clone>/.canary/doctor.json`. A missing file is not an
152
+ * error (no checks). A malformed file or an invalid check degrades to a single
153
+ * failing check ({@link ManifestLoad} `ok: false`) — never a throw.
154
+ */
155
+ function loadManifest(cloneDir) {
156
+ const file = manifestPath(cloneDir);
157
+ let raw;
158
+ try {
159
+ raw = fs.readFileSync(file, "utf8");
160
+ }
161
+ catch (e) {
162
+ if (e.code === "ENOENT") {
163
+ return { ok: true, checks: [] };
164
+ }
165
+ return manifestFailure(cloneDir, e.message);
166
+ }
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(raw);
170
+ }
171
+ catch (e) {
172
+ return manifestFailure(cloneDir, `does not parse: ${e.message}`);
173
+ }
174
+ const checksRaw = parsed.checks;
175
+ if (!Array.isArray(checksRaw)) {
176
+ return manifestFailure(cloneDir, 'expected an object with a "checks" array');
177
+ }
178
+ const checks = [];
179
+ for (let i = 0; i < checksRaw.length; i += 1) {
180
+ const result = validateCheck(checksRaw[i], i);
181
+ if (typeof result === "string") {
182
+ return manifestFailure(cloneDir, result);
183
+ }
184
+ checks.push(result);
185
+ }
186
+ return { ok: true, checks };
187
+ }
188
+ /**
189
+ * Keep checks that should run for `persona`: a null persona runs everything;
190
+ * otherwise keep checks with no persona plus those whose persona list contains
191
+ * the tag (case-insensitive).
192
+ */
193
+ function filterByPersona(checks, persona) {
194
+ if (persona === null) {
195
+ return [...checks];
196
+ }
197
+ const want = persona.toLowerCase();
198
+ return checks.filter((c) => !c.persona || c.persona.length === 0 || c.persona.some((p) => p.toLowerCase() === want));
199
+ }
200
+ /** Default per-check timeout for url and command checks. */
201
+ exports.DEFAULT_CHECK_TIMEOUT_MS = 10000;
202
+ function defaultProbeUrl(url, timeoutMs) {
203
+ return new Promise((resolve) => {
204
+ let mod;
205
+ try {
206
+ mod = new URL(url).protocol === "http:" ? http : https;
207
+ }
208
+ catch {
209
+ resolve(false);
210
+ return;
211
+ }
212
+ const req = mod.get(url, (res) => {
213
+ const s = res.statusCode ?? 0;
214
+ res.resume();
215
+ resolve(s >= 200 && s < 400);
216
+ });
217
+ req.on("error", () => resolve(false));
218
+ req.setTimeout(timeoutMs, () => {
219
+ req.destroy();
220
+ resolve(false);
221
+ });
222
+ });
223
+ }
224
+ const defaultRunCommand = (command, cwd, timeoutMs) => {
225
+ const r = (0, node_child_process_1.spawnSync)(command[0], command.slice(1), { cwd, timeout: timeoutMs, encoding: "utf8" });
226
+ if (r.error) {
227
+ const code = r.error.code;
228
+ return { ok: false, timedOut: code === "ETIMEDOUT" || r.signal === "SIGTERM", detail: String(r.error.message) };
229
+ }
230
+ if (r.signal === "SIGTERM") {
231
+ return { ok: false, timedOut: true };
232
+ }
233
+ return { ok: r.status === 0, timedOut: false, detail: r.status === 0 ? undefined : `exit ${r.status}` };
234
+ };
235
+ function pass(check, label) {
236
+ return { id: check.id, status: "pass", label };
237
+ }
238
+ function fail(check, label) {
239
+ return { id: check.id, status: "fail", label, remedy: check.remedy };
240
+ }
241
+ function runFileExists(check, cloneDir) {
242
+ const target = path.join(cloneDir, check.path ?? "");
243
+ return fs.existsSync(target)
244
+ ? pass(check, `${check.id}: ${check.path} exists`)
245
+ : fail(check, `${check.id}: ${check.path} is missing`);
246
+ }
247
+ async function runUrlReachable(check, ctx, timeoutMs) {
248
+ const probe = ctx.probeUrl ?? defaultProbeUrl;
249
+ const reachable = await probe(check.url ?? "", timeoutMs);
250
+ return reachable
251
+ ? pass(check, `${check.id}: ${check.url} reachable`)
252
+ : fail(check, `${check.id}: ${check.url} unreachable`);
253
+ }
254
+ function skipped(check, reason) {
255
+ return { id: check.id, status: "skip", label: `${check.id}: skipped (${reason})` };
256
+ }
257
+ function runCommandSucceeds(check, ctx, timeoutMs) {
258
+ if (!ctx.consentGranted) {
259
+ return skipped(check, "command checks need consent — re-run 'canary overlay add'");
260
+ }
261
+ const command = check.command ?? [];
262
+ const cmd = `\`${command.join(" ")}\``;
263
+ const runner = ctx.runCommand ?? defaultRunCommand;
264
+ const r = runner(command, ctx.cloneDir, timeoutMs);
265
+ if (r.ok) {
266
+ return pass(check, `${check.id}: ${cmd} succeeded`);
267
+ }
268
+ const why = r.timedOut ? `timed out after ${timeoutMs}ms` : (r.detail ?? "failed");
269
+ return fail(check, `${check.id}: ${cmd} ${why}`);
270
+ }
271
+ /**
272
+ * Execute one validated check against its overlay clone, under a bounded
273
+ * timeout. `command-succeeds` is skipped (not failed) unless consent is
274
+ * granted. Never throws.
275
+ */
276
+ async function runCheck(check, ctx) {
277
+ const timeoutMs = ctx.timeoutMs ?? exports.DEFAULT_CHECK_TIMEOUT_MS;
278
+ if (check.type === "file-exists") {
279
+ return runFileExists(check, ctx.cloneDir);
280
+ }
281
+ if (check.type === "url-reachable") {
282
+ return runUrlReachable(check, ctx, timeoutMs);
283
+ }
284
+ return runCommandSucceeds(check, ctx, timeoutMs);
285
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,130 @@
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.runDoctor = runDoctor;
37
+ /**
38
+ * `canary doctor` — environment self-check (Phase 2).
39
+ *
40
+ * Runs two tiers of checks and prints each as pass/fail with a remedy line
41
+ * under every failure, adopting the `harness doctor` output shape; the process
42
+ * exits non-zero when any check fails. Tier 1 is built-in engine checks; tier 2
43
+ * is data-driven checks read from each tracked overlay's `.canary/doctor.json`.
44
+ */
45
+ const os = __importStar(require("node:os"));
46
+ const engine_checks_js_1 = require("./engine-checks.js");
47
+ const doctor_manifest_js_1 = require("./doctor-manifest.js");
48
+ const registry = __importStar(require("./overlays-registry.js"));
49
+ const SYMBOL = { pass: "✓", fail: "✗", skip: "-", info: "ℹ" };
50
+ /** `--persona <tag>` / `--persona=<tag>`, or null when unset. */
51
+ function parsePersona(args) {
52
+ for (let i = 0; i < args.length; i += 1) {
53
+ const a = args[i];
54
+ if (a === "--persona") {
55
+ return args[i + 1] ?? null;
56
+ }
57
+ if (a.startsWith("--persona=")) {
58
+ return a.slice("--persona=".length);
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ function renderCheck(r) {
64
+ const line = ` ${SYMBOL[r.status]} ${r.label}\n`;
65
+ return r.status === "fail" && r.remedy ? `${line} → ${r.remedy}\n` : line;
66
+ }
67
+ /** Load, persona-filter, and run one overlay's manifest checks. */
68
+ async function overlayResults(entry, deps, persona) {
69
+ const header = `Overlay: ${entry.name}`;
70
+ const load = (0, doctor_manifest_js_1.loadManifest)(entry.path);
71
+ if (!load.ok) {
72
+ return { header, results: [load.failure] };
73
+ }
74
+ const granted = registry.consentGranted(entry, (0, doctor_manifest_js_1.commandSucceedsHash)(load.checks));
75
+ const checks = (0, doctor_manifest_js_1.filterByPersona)(load.checks, persona);
76
+ const ctx = {
77
+ cloneDir: entry.path,
78
+ consentGranted: granted,
79
+ timeoutMs: deps.timeoutMs,
80
+ probeUrl: deps.probeUrl,
81
+ runCommand: deps.runCommand,
82
+ };
83
+ const results = [];
84
+ for (const check of checks) {
85
+ results.push(await (0, doctor_manifest_js_1.runCheck)(check, ctx));
86
+ }
87
+ return { header, results };
88
+ }
89
+ /**
90
+ * Run `canary doctor`. Returns a process exit code: 0 when every check passed
91
+ * or was skipped/info, non-zero when any check failed. A malformed manifest for
92
+ * one overlay never blocks engine checks or other overlays.
93
+ */
94
+ async function runDoctor(args, deps = {}) {
95
+ const out = deps.out ?? process.stdout;
96
+ const persona = parsePersona(args);
97
+ const homeDir = deps.homeDir ?? os.homedir();
98
+ const engineDeps = {
99
+ git: deps.git,
100
+ homeDir,
101
+ cwd: deps.cwd,
102
+ currentVersion: deps.currentVersion,
103
+ getLatestVersion: deps.getLatestVersion,
104
+ timeoutMs: deps.timeoutMs,
105
+ };
106
+ const groups = [{ header: "Engine", results: await (0, engine_checks_js_1.runEngineChecks)(engineDeps) }];
107
+ let reg;
108
+ try {
109
+ reg = registry.read(homeDir);
110
+ }
111
+ catch {
112
+ reg = registry.emptyRegistry();
113
+ }
114
+ for (const entry of reg.overlays) {
115
+ groups.push(await overlayResults(entry, deps, persona));
116
+ }
117
+ out.write("canary doctor\n");
118
+ let failures = 0;
119
+ for (const group of groups) {
120
+ out.write(`\n${group.header}\n`);
121
+ for (const result of group.results) {
122
+ if (result.status === "fail") {
123
+ failures += 1;
124
+ }
125
+ out.write(renderCheck(result));
126
+ }
127
+ }
128
+ out.write(`\n${failures === 0 ? "All checks passed." : `${failures} check(s) failed.`}\n`);
129
+ return failures === 0 ? 0 : 1;
130
+ }