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.
- package/bin/canary +0 -0
- package/bin/canary.js +41 -9
- package/dist/doctor-manifest.js +307 -0
- package/dist/doctor.js +165 -0
- package/dist/engine-checks.js +320 -0
- package/dist/overlay-commands.js +390 -0
- package/dist/overlays-registry.js +162 -0
- package/dist/router.js +140 -0
- package/dist/source-spec.js +107 -0
- package/package.json +10 -3
|
@@ -0,0 +1,320 @@
|
|
|
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.parseRegistryVersion = parseRegistryVersion;
|
|
37
|
+
exports.isOlder = isOlder;
|
|
38
|
+
exports.checkVersion = checkVersion;
|
|
39
|
+
exports.checkGit = checkGit;
|
|
40
|
+
exports.checkOverlays = checkOverlays;
|
|
41
|
+
exports.checkProjectConfig = checkProjectConfig;
|
|
42
|
+
exports.checkMcpConfig = checkMcpConfig;
|
|
43
|
+
exports.runEngineChecks = runEngineChecks;
|
|
44
|
+
/**
|
|
45
|
+
* Built-in `canary doctor` engine checks (Phase 2, tier 1). Each returns a
|
|
46
|
+
* {@link CheckResult}; none throws. Network is injectable and offline-tolerant.
|
|
47
|
+
*/
|
|
48
|
+
const fs = __importStar(require("node:fs"));
|
|
49
|
+
const https = __importStar(require("node:https"));
|
|
50
|
+
const os = __importStar(require("node:os"));
|
|
51
|
+
const path = __importStar(require("node:path"));
|
|
52
|
+
const overlay_commands_js_1 = require("./overlay-commands.js");
|
|
53
|
+
const registry = __importStar(require("./overlays-registry.js"));
|
|
54
|
+
/** The published npm package name (mirrors the install remedy in the shim). */
|
|
55
|
+
const PKG = "canary-test-cli";
|
|
56
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
57
|
+
const realGit = (args, opts = {}) => {
|
|
58
|
+
const { spawnSync } = require("node:child_process");
|
|
59
|
+
const r = spawnSync("git", args, { cwd: opts.cwd, encoding: "utf8" });
|
|
60
|
+
if (r.error) {
|
|
61
|
+
const code = r.error.code;
|
|
62
|
+
return { status: code === "ENOENT" ? 127 : 1, stdout: "", stderr: String(r.error.message) };
|
|
63
|
+
}
|
|
64
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
65
|
+
};
|
|
66
|
+
/** Read this package's own version from its package.json (best effort). */
|
|
67
|
+
function ownVersion() {
|
|
68
|
+
try {
|
|
69
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
70
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version ?? null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** String `version` from a registry body; null on parse error or non-string shape (SEC-DES-001: registry JSON is untrusted). */
|
|
77
|
+
function parseRegistryVersion(rawBody) {
|
|
78
|
+
try {
|
|
79
|
+
const v = JSON.parse(rawBody)?.version;
|
|
80
|
+
return typeof v === "string" ? v : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** GET the latest published version from the npm registry. null on any error. */
|
|
87
|
+
function fetchLatestVersion(timeoutMs) {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const req = https.get(`https://registry.npmjs.org/${PKG}/latest`, (res) => {
|
|
90
|
+
if (res.statusCode !== 200) {
|
|
91
|
+
res.resume();
|
|
92
|
+
resolve(null);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let body = "";
|
|
96
|
+
res.on("data", (c) => (body += c));
|
|
97
|
+
res.on("end", () => resolve(parseRegistryVersion(body)));
|
|
98
|
+
});
|
|
99
|
+
req.on("error", () => resolve(null));
|
|
100
|
+
req.setTimeout(timeoutMs, () => req.destroy());
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** True when semver `a` is strictly older than `b` (numeric compare, no prerelease). */
|
|
104
|
+
function isOlder(a, b) {
|
|
105
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10));
|
|
106
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10));
|
|
107
|
+
for (let i = 0; i < 3; i += 1) {
|
|
108
|
+
const x = pa[i] ?? 0;
|
|
109
|
+
const y = pb[i] ?? 0;
|
|
110
|
+
if (x !== y) {
|
|
111
|
+
return x < y;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
/** CLI version vs latest release. Offline degrades to info, never a failure. */
|
|
117
|
+
async function checkVersion(deps = {}) {
|
|
118
|
+
const current = deps.currentVersion ?? ownVersion();
|
|
119
|
+
if (!current) {
|
|
120
|
+
return { id: "engine:version", status: "info", label: "CLI version: unknown" };
|
|
121
|
+
}
|
|
122
|
+
const getLatest = deps.getLatestVersion ?? (() => fetchLatestVersion(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
123
|
+
const latest = await getLatest();
|
|
124
|
+
if (!latest) {
|
|
125
|
+
return { id: "engine:version", status: "info", label: `CLI ${current} (could not check latest — offline?)` };
|
|
126
|
+
}
|
|
127
|
+
if (isOlder(current, latest)) {
|
|
128
|
+
return {
|
|
129
|
+
id: "engine:version",
|
|
130
|
+
status: "fail",
|
|
131
|
+
label: `CLI ${current} is behind latest ${latest}`,
|
|
132
|
+
remedy: `Upgrade: npm install -g ${PKG}@latest`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { id: "engine:version", status: "pass", label: `CLI ${current} (latest)` };
|
|
136
|
+
}
|
|
137
|
+
/** git present on PATH. */
|
|
138
|
+
function checkGit(deps = {}) {
|
|
139
|
+
const git = deps.git ?? realGit;
|
|
140
|
+
const res = git(["--version"]);
|
|
141
|
+
if (res.status === 0) {
|
|
142
|
+
return { id: "engine:git", status: "pass", label: `git present (${res.stdout.trim() || "ok"})` };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
id: "engine:git",
|
|
146
|
+
status: "fail",
|
|
147
|
+
label: "git not found on PATH",
|
|
148
|
+
remedy: "Install git and ensure it is on your PATH — overlay add/update need it.",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Registered overlays present, fresh, and free of local modifications. */
|
|
152
|
+
function checkOverlays(deps = {}) {
|
|
153
|
+
const git = deps.git ?? realGit;
|
|
154
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
155
|
+
let reg;
|
|
156
|
+
try {
|
|
157
|
+
reg = registry.read(homeDir);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
id: "engine:overlays",
|
|
163
|
+
status: "fail",
|
|
164
|
+
label: "overlays registry unreadable",
|
|
165
|
+
remedy: e.message,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
if (reg.overlays.length === 0) {
|
|
170
|
+
return [{ id: "engine:overlays", status: "info", label: "no overlays registered" }];
|
|
171
|
+
}
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const o of reg.overlays) {
|
|
174
|
+
const fresh = (0, overlay_commands_js_1.freshness)(o.path, o, git);
|
|
175
|
+
if (fresh.startsWith("missing")) {
|
|
176
|
+
results.push({
|
|
177
|
+
id: `overlay:${o.name}:present`,
|
|
178
|
+
status: "fail",
|
|
179
|
+
label: `overlay "${o.name}": clone missing`,
|
|
180
|
+
remedy: `Re-add it: canary overlay remove ${o.name} (if needed) then canary overlay add ${o.source}`,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (fresh.includes("behind")) {
|
|
185
|
+
results.push({
|
|
186
|
+
id: `overlay:${o.name}:fresh`,
|
|
187
|
+
status: "fail",
|
|
188
|
+
label: `overlay "${o.name}": ${fresh}`,
|
|
189
|
+
remedy: `Update it: canary overlay update ${o.name}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
results.push({ id: `overlay:${o.name}:fresh`, status: "pass", label: `overlay "${o.name}": ${fresh}` });
|
|
194
|
+
}
|
|
195
|
+
const clean = (0, overlay_commands_js_1.workingTreeStatus)(o.path, git);
|
|
196
|
+
if (clean === "clean") {
|
|
197
|
+
results.push({ id: `overlay:${o.name}:clean`, status: "pass", label: `overlay "${o.name}": no local changes` });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
results.push({
|
|
201
|
+
id: `overlay:${o.name}:clean`,
|
|
202
|
+
status: "fail",
|
|
203
|
+
label: `overlay "${o.name}": ${clean === "dirty" ? "local modifications" : "git status unreadable"}`,
|
|
204
|
+
remedy: `Commit/stash changes in ${o.path}, or canary overlay remove ${o.name} and re-add.`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
/** Parse a JSON file; returns an error message or null when it parses (or is absent). */
|
|
211
|
+
function parseErrorOrNull(file) {
|
|
212
|
+
let raw;
|
|
213
|
+
try {
|
|
214
|
+
raw = fs.readFileSync(file, "utf8");
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
return e.code === "ENOENT" ? null : e.message;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
JSON.parse(raw);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
return e.message;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Project `.canary/` config files parse as JSON. */
|
|
228
|
+
function checkProjectConfig(deps = {}) {
|
|
229
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
230
|
+
const dir = path.join(cwd, ".canary");
|
|
231
|
+
let names;
|
|
232
|
+
try {
|
|
233
|
+
names = fs.readdirSync(dir).filter((n) => /^company(\.[\w-]+)?\.json$/.test(n));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return { id: "engine:project-config", status: "skip", label: "no project .canary/ config" };
|
|
237
|
+
}
|
|
238
|
+
if (names.length === 0) {
|
|
239
|
+
return { id: "engine:project-config", status: "skip", label: "no project .canary/ config" };
|
|
240
|
+
}
|
|
241
|
+
for (const name of names) {
|
|
242
|
+
const err = parseErrorOrNull(path.join(dir, name));
|
|
243
|
+
if (err) {
|
|
244
|
+
return {
|
|
245
|
+
id: "engine:project-config",
|
|
246
|
+
status: "fail",
|
|
247
|
+
label: `project .canary/${name} does not parse`,
|
|
248
|
+
remedy: `Fix the JSON in .canary/${name}: ${err}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { id: "engine:project-config", status: "pass", label: `project .canary/ config parses (${names.length} file(s))` };
|
|
253
|
+
}
|
|
254
|
+
/** Assert a single `.mcp.json` parses and each server entry is well-formed. */
|
|
255
|
+
function inspectMcpFile(file) {
|
|
256
|
+
let raw;
|
|
257
|
+
try {
|
|
258
|
+
raw = fs.readFileSync(file, "utf8");
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
if (e.code === "ENOENT") {
|
|
262
|
+
return { present: false };
|
|
263
|
+
}
|
|
264
|
+
return { present: true, error: e.message };
|
|
265
|
+
}
|
|
266
|
+
let data;
|
|
267
|
+
try {
|
|
268
|
+
data = JSON.parse(raw);
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
return { present: true, error: `does not parse: ${e.message}` };
|
|
272
|
+
}
|
|
273
|
+
const servers = data.mcpServers;
|
|
274
|
+
if (servers === undefined) {
|
|
275
|
+
return { present: true };
|
|
276
|
+
}
|
|
277
|
+
if (typeof servers !== "object" || servers === null) {
|
|
278
|
+
return { present: true, error: "mcpServers is not an object" };
|
|
279
|
+
}
|
|
280
|
+
for (const [key, entry] of Object.entries(servers)) {
|
|
281
|
+
const e = entry;
|
|
282
|
+
if (typeof e.command !== "string" && typeof e.url !== "string") {
|
|
283
|
+
return { present: true, error: `server "${key}" has neither a command nor a url` };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { present: true };
|
|
287
|
+
}
|
|
288
|
+
/** MCP config references resolvable — read project and home `.mcp.json` directly. */
|
|
289
|
+
function checkMcpConfig(deps = {}) {
|
|
290
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
291
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
292
|
+
const files = [path.join(cwd, ".mcp.json"), path.join(homeDir, ".mcp.json")];
|
|
293
|
+
let anyPresent = false;
|
|
294
|
+
for (const file of files) {
|
|
295
|
+
const r = inspectMcpFile(file);
|
|
296
|
+
anyPresent = anyPresent || r.present;
|
|
297
|
+
if (r.error) {
|
|
298
|
+
return {
|
|
299
|
+
id: "engine:mcp",
|
|
300
|
+
status: "fail",
|
|
301
|
+
label: `MCP config ${file} is invalid`,
|
|
302
|
+
remedy: `Fix ${file}: ${r.error}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!anyPresent) {
|
|
307
|
+
return { id: "engine:mcp", status: "skip", label: "no .mcp.json found" };
|
|
308
|
+
}
|
|
309
|
+
return { id: "engine:mcp", status: "pass", label: "MCP config resolves" };
|
|
310
|
+
}
|
|
311
|
+
/** Run every engine check, in display order. */
|
|
312
|
+
async function runEngineChecks(deps = {}) {
|
|
313
|
+
return [
|
|
314
|
+
await checkVersion(deps),
|
|
315
|
+
checkGit(deps),
|
|
316
|
+
...checkOverlays(deps),
|
|
317
|
+
checkProjectConfig(deps),
|
|
318
|
+
checkMcpConfig(deps),
|
|
319
|
+
];
|
|
320
|
+
}
|