context-mode 0.6.1 → 0.7.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/build/cli.js CHANGED
@@ -5,11 +5,17 @@
5
5
  * Usage:
6
6
  * context-mode → Start MCP server (stdio)
7
7
  * context-mode setup → Interactive setup (detect runtimes, install Bun)
8
- * context-mode doctor → Diagnose runtime issues
8
+ * context-mode doctor → Diagnose runtime issues, hooks, FTS5, version
9
+ * context-mode upgrade → Fix hooks, permissions, and settings
10
+ * context-mode stats → (skill only — /context-mode:stats)
9
11
  */
10
12
  import * as p from "@clack/prompts";
11
13
  import color from "picocolors";
12
14
  import { execSync } from "node:child_process";
15
+ import { readFileSync, writeFileSync, copyFileSync, chmodSync, accessSync, readdirSync, constants } from "node:fs";
16
+ import { resolve, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { homedir } from "node:os";
13
19
  import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
14
20
  const args = process.argv.slice(2);
15
21
  if (args[0] === "setup") {
@@ -18,10 +24,479 @@ if (args[0] === "setup") {
18
24
  else if (args[0] === "doctor") {
19
25
  doctor();
20
26
  }
27
+ else if (args[0] === "upgrade") {
28
+ upgrade();
29
+ }
21
30
  else {
22
31
  // Default: start MCP server
23
32
  import("./server.js");
24
33
  }
34
+ /* -------------------------------------------------------
35
+ * Shared helpers
36
+ * ------------------------------------------------------- */
37
+ function getPluginRoot() {
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = dirname(__filename);
40
+ return resolve(__dirname, "..");
41
+ }
42
+ function getSettingsPath() {
43
+ return resolve(homedir(), ".claude", "settings.json");
44
+ }
45
+ function readSettings() {
46
+ try {
47
+ const raw = readFileSync(getSettingsPath(), "utf-8");
48
+ return JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function getHookScriptPath() {
55
+ return resolve(getPluginRoot(), "hooks", "pretooluse.sh");
56
+ }
57
+ function getLocalVersion() {
58
+ try {
59
+ const pkg = JSON.parse(readFileSync(resolve(getPluginRoot(), "package.json"), "utf-8"));
60
+ return pkg.version ?? "unknown";
61
+ }
62
+ catch {
63
+ return "unknown";
64
+ }
65
+ }
66
+ async function fetchLatestVersion() {
67
+ try {
68
+ const resp = await fetch("https://registry.npmjs.org/context-mode/latest");
69
+ if (!resp.ok)
70
+ return "unknown";
71
+ const data = (await resp.json());
72
+ return data.version ?? "unknown";
73
+ }
74
+ catch {
75
+ return "unknown";
76
+ }
77
+ }
78
+ function getMarketplaceVersion() {
79
+ // Detect from our own path: .../plugins/cache/<marketplace>/<plugin>/<version>/
80
+ const root = getPluginRoot();
81
+ const match = root.match(/plugins\/cache\/[^/]+\/[^/]+\/(\d+\.\d+\.\d+[^/]*)/);
82
+ if (match)
83
+ return match[1];
84
+ // Fallback: scan common plugin cache locations
85
+ const bases = [
86
+ resolve(homedir(), ".claude"),
87
+ resolve(homedir(), ".config", "claude"),
88
+ ];
89
+ for (const base of bases) {
90
+ const cacheDir = resolve(base, "plugins", "cache", "claude-context-mode", "context-mode");
91
+ try {
92
+ const entries = readdirSync(cacheDir);
93
+ const versions = entries
94
+ .filter((e) => /^\d+\.\d+\.\d+/.test(e))
95
+ .sort((a, b) => {
96
+ const pa = a.split(".").map(Number);
97
+ const pb = b.split(".").map(Number);
98
+ for (let i = 0; i < 3; i++) {
99
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0))
100
+ return (pa[i] ?? 0) - (pb[i] ?? 0);
101
+ }
102
+ return 0;
103
+ });
104
+ if (versions.length > 0)
105
+ return versions[versions.length - 1];
106
+ }
107
+ catch { /* continue */ }
108
+ }
109
+ return "not installed";
110
+ }
111
+ function semverGt(a, b) {
112
+ const pa = a.split(".").map(Number);
113
+ const pb = b.split(".").map(Number);
114
+ for (let i = 0; i < 3; i++) {
115
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
116
+ return true;
117
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
118
+ return false;
119
+ }
120
+ return false;
121
+ }
122
+ /* -------------------------------------------------------
123
+ * Doctor
124
+ * ------------------------------------------------------- */
125
+ async function doctor() {
126
+ console.clear();
127
+ p.intro(color.bgMagenta(color.white(" context-mode doctor ")));
128
+ const s = p.spinner();
129
+ s.start("Running diagnostics");
130
+ const runtimes = detectRuntimes();
131
+ const available = getAvailableLanguages(runtimes);
132
+ s.stop("Diagnostics complete");
133
+ // Runtime check
134
+ p.note(getRuntimeSummary(runtimes), "Runtimes");
135
+ // Speed tier
136
+ if (hasBunRuntime()) {
137
+ p.log.success(color.green("Performance: FAST") +
138
+ " — Bun detected for JS/TS execution");
139
+ }
140
+ else {
141
+ p.log.warn(color.yellow("Performance: NORMAL") +
142
+ " — Using Node.js (install Bun for 3-5x speed boost)");
143
+ }
144
+ // Language coverage
145
+ const total = 10;
146
+ const pct = ((available.length / total) * 100).toFixed(0);
147
+ p.log.info(`Language coverage: ${available.length}/${total} (${pct}%)` +
148
+ color.dim(` — ${available.join(", ")}`));
149
+ // Server test
150
+ p.log.step("Testing server initialization...");
151
+ try {
152
+ const { PolyglotExecutor } = await import("./executor.js");
153
+ const executor = new PolyglotExecutor({ runtimes });
154
+ const result = await executor.execute({
155
+ language: "javascript",
156
+ code: 'console.log("ok");',
157
+ timeout: 5000,
158
+ });
159
+ if (result.exitCode === 0 && result.stdout.trim() === "ok") {
160
+ p.log.success(color.green("Server test: PASS"));
161
+ }
162
+ else {
163
+ p.log.error(color.red("Server test: FAIL") + ` — exit ${result.exitCode}`);
164
+ }
165
+ }
166
+ catch (err) {
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
169
+ }
170
+ // Hooks installed
171
+ p.log.step("Checking hooks configuration...");
172
+ const settings = readSettings();
173
+ const hookScriptPath = getHookScriptPath();
174
+ if (settings) {
175
+ const hooks = settings.hooks;
176
+ const preToolUse = hooks?.PreToolUse;
177
+ if (preToolUse && preToolUse.length > 0) {
178
+ const hasCorrectHook = preToolUse.some((entry) => entry.hooks?.some((h) => h.command?.includes("pretooluse.sh")));
179
+ if (hasCorrectHook) {
180
+ p.log.success(color.green("Hooks installed: PASS") + " — PreToolUse hook configured");
181
+ }
182
+ else {
183
+ p.log.error(color.red("Hooks installed: FAIL") +
184
+ " — PreToolUse exists but does not point to pretooluse.sh" +
185
+ color.dim("\n Run: npx context-mode upgrade"));
186
+ }
187
+ }
188
+ else {
189
+ p.log.error(color.red("Hooks installed: FAIL") +
190
+ " — No PreToolUse hooks found" +
191
+ color.dim("\n Run: npx context-mode upgrade"));
192
+ }
193
+ }
194
+ else {
195
+ p.log.error(color.red("Hooks installed: FAIL") +
196
+ " — Could not read ~/.claude/settings.json" +
197
+ color.dim("\n Run: npx context-mode upgrade"));
198
+ }
199
+ // Hook script exists
200
+ p.log.step("Checking hook script...");
201
+ try {
202
+ accessSync(hookScriptPath, constants.R_OK);
203
+ p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${hookScriptPath}`));
204
+ }
205
+ catch {
206
+ p.log.error(color.red("Hook script exists: FAIL") +
207
+ color.dim(` — not found at ${hookScriptPath}`));
208
+ }
209
+ // Plugin enabled
210
+ p.log.step("Checking plugin registration...");
211
+ if (settings) {
212
+ const enabledPlugins = settings.enabledPlugins;
213
+ if (enabledPlugins) {
214
+ const pluginKey = Object.keys(enabledPlugins).find((k) => k.startsWith("context-mode"));
215
+ if (pluginKey && enabledPlugins[pluginKey]) {
216
+ p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginKey}`));
217
+ }
218
+ else {
219
+ p.log.warn(color.yellow("Plugin enabled: WARN") +
220
+ " — context-mode not in enabledPlugins" +
221
+ color.dim(" (might be using standalone MCP mode)"));
222
+ }
223
+ }
224
+ else {
225
+ p.log.warn(color.yellow("Plugin enabled: WARN") +
226
+ " — no enabledPlugins section found" +
227
+ color.dim(" (might be using standalone MCP mode)"));
228
+ }
229
+ }
230
+ else {
231
+ p.log.warn(color.yellow("Plugin enabled: WARN") +
232
+ " — could not read settings.json");
233
+ }
234
+ // FTS5 / better-sqlite3
235
+ p.log.step("Checking FTS5 / better-sqlite3...");
236
+ try {
237
+ const Database = (await import("better-sqlite3")).default;
238
+ const db = new Database(":memory:");
239
+ db.exec("CREATE VIRTUAL TABLE fts_test USING fts5(content)");
240
+ db.exec("INSERT INTO fts_test(content) VALUES ('hello world')");
241
+ const row = db.prepare("SELECT * FROM fts_test WHERE fts_test MATCH 'hello'").get();
242
+ db.close();
243
+ if (row && row.content === "hello world") {
244
+ p.log.success(color.green("FTS5 / better-sqlite3: PASS") + " — native module works");
245
+ }
246
+ else {
247
+ p.log.error(color.red("FTS5 / better-sqlite3: FAIL") + " — query returned unexpected result");
248
+ }
249
+ }
250
+ catch (err) {
251
+ const message = err instanceof Error ? err.message : String(err);
252
+ p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
253
+ ` — ${message}` +
254
+ color.dim("\n Try: npm rebuild better-sqlite3"));
255
+ }
256
+ // Version check
257
+ p.log.step("Checking versions...");
258
+ const localVersion = getLocalVersion();
259
+ const latestVersion = await fetchLatestVersion();
260
+ const marketplaceVersion = getMarketplaceVersion();
261
+ // npm / MCP version
262
+ if (latestVersion === "unknown") {
263
+ p.log.warn(color.yellow("npm (MCP): WARN") +
264
+ ` — local v${localVersion}, could not reach npm registry`);
265
+ }
266
+ else if (localVersion === latestVersion) {
267
+ p.log.success(color.green("npm (MCP): PASS") +
268
+ ` — v${localVersion}`);
269
+ }
270
+ else {
271
+ p.log.warn(color.yellow("npm (MCP): WARN") +
272
+ ` — local v${localVersion}, latest v${latestVersion}` +
273
+ color.dim("\n Run: npm install -g context-mode@latest"));
274
+ }
275
+ // Marketplace version
276
+ if (marketplaceVersion === "not installed") {
277
+ p.log.info(color.dim("Marketplace: not installed") +
278
+ " — using standalone MCP mode");
279
+ }
280
+ else if (latestVersion !== "unknown" && marketplaceVersion === latestVersion) {
281
+ p.log.success(color.green("Marketplace: PASS") +
282
+ ` — v${marketplaceVersion}`);
283
+ }
284
+ else if (latestVersion !== "unknown") {
285
+ p.log.warn(color.yellow("Marketplace: WARN") +
286
+ ` — v${marketplaceVersion}, latest v${latestVersion}` +
287
+ color.dim("\n Update via Claude Code marketplace or reinstall plugin"));
288
+ }
289
+ else {
290
+ p.log.info(`Marketplace: v${marketplaceVersion}` +
291
+ color.dim(" — could not verify against npm registry"));
292
+ }
293
+ // Summary
294
+ p.outro(available.length >= 4
295
+ ? color.green("Diagnostics complete!")
296
+ : color.yellow("Some checks need attention — see above for details"));
297
+ }
298
+ /* -------------------------------------------------------
299
+ * Upgrade
300
+ * ------------------------------------------------------- */
301
+ async function upgrade() {
302
+ console.clear();
303
+ p.intro(color.bgCyan(color.black(" context-mode upgrade ")));
304
+ let pluginRoot = getPluginRoot();
305
+ const settingsPath = getSettingsPath();
306
+ const changes = [];
307
+ const s = p.spinner();
308
+ // Step 1: Pull latest from GitHub (same source as marketplace)
309
+ p.log.step("Pulling latest from GitHub...");
310
+ const localVersion = getLocalVersion();
311
+ const tmpDir = `/tmp/context-mode-upgrade-${Date.now()}`;
312
+ s.start("Cloning mksglu/claude-context-mode");
313
+ try {
314
+ execSync(`git clone --depth 1 https://github.com/mksglu/claude-context-mode.git "${tmpDir}"`, { stdio: "pipe", timeout: 30000 });
315
+ s.stop("Downloaded");
316
+ const srcDir = tmpDir;
317
+ // Read new version
318
+ const newPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
319
+ const newVersion = newPkg.version ?? "unknown";
320
+ if (newVersion === localVersion) {
321
+ p.log.success(color.green("Already on latest") + ` — v${localVersion}`);
322
+ }
323
+ else {
324
+ p.log.info(`Update available: ${color.yellow("v" + localVersion)} → ${color.green("v" + newVersion)}`);
325
+ }
326
+ // Step 2: Install dependencies + build
327
+ s.start("Installing dependencies & building");
328
+ execSync("npm install --no-audit --no-fund 2>/dev/null", {
329
+ cwd: srcDir,
330
+ stdio: "pipe",
331
+ timeout: 60000,
332
+ });
333
+ execSync("npm run build 2>/dev/null", {
334
+ cwd: srcDir,
335
+ stdio: "pipe",
336
+ timeout: 30000,
337
+ });
338
+ s.stop("Built successfully");
339
+ // Step 3: Copy to plugin root
340
+ s.start("Installing files");
341
+ const items = [
342
+ "build", "hooks", "skills", ".claude-plugin",
343
+ "start.sh", "server.bundle.mjs", "package.json", ".mcp.json",
344
+ ];
345
+ for (const item of items) {
346
+ try {
347
+ execSync(`rm -rf "${pluginRoot}/${item}"`, { stdio: "pipe" });
348
+ execSync(`cp -r "${srcDir}/${item}" "${pluginRoot}/"`, { stdio: "pipe" });
349
+ }
350
+ catch { /* some files may not exist */ }
351
+ }
352
+ s.stop(color.green("Files installed"));
353
+ // Install production deps in plugin root
354
+ s.start("Installing production dependencies");
355
+ execSync("npm install --production --no-audit --no-fund 2>/dev/null", {
356
+ cwd: pluginRoot,
357
+ stdio: "pipe",
358
+ timeout: 60000,
359
+ });
360
+ s.stop("Dependencies ready");
361
+ // Step 2.5: Migrate versioned cache directory if version changed
362
+ const cacheMatch = pluginRoot.match(/^(.*\/plugins\/cache\/[^/]+\/[^/]+\/)(\d+\.\d+\.\d+[^/]*)$/);
363
+ if (cacheMatch && newVersion !== cacheMatch[2] && newVersion !== "unknown") {
364
+ const oldDirVersion = cacheMatch[2];
365
+ const newCacheDir = cacheMatch[1] + newVersion;
366
+ s.start(`Migrating cache: ${oldDirVersion} → ${newVersion}`);
367
+ try {
368
+ execSync(`rm -rf "${newCacheDir}"`, { stdio: "pipe" });
369
+ execSync(`mv "${pluginRoot}" "${newCacheDir}"`, { stdio: "pipe" });
370
+ pluginRoot = newCacheDir;
371
+ s.stop(color.green(`Cache directory: ${newVersion}`));
372
+ changes.push(`Migrated cache: ${oldDirVersion} → ${newVersion}`);
373
+ }
374
+ catch {
375
+ s.stop(color.yellow("Cache migration skipped — using existing directory"));
376
+ }
377
+ }
378
+ // Update global npm package from same GitHub source
379
+ s.start("Updating npm global package");
380
+ try {
381
+ execSync(`npm install -g "${pluginRoot}" --no-audit --no-fund 2>/dev/null`, {
382
+ stdio: "pipe",
383
+ timeout: 30000,
384
+ });
385
+ s.stop(color.green("npm global updated"));
386
+ changes.push("Updated npm global package");
387
+ }
388
+ catch {
389
+ s.stop(color.yellow("npm global update skipped"));
390
+ p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
391
+ }
392
+ // Cleanup
393
+ execSync(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
394
+ changes.push(newVersion !== localVersion
395
+ ? `Updated v${localVersion} → v${newVersion}`
396
+ : `Reinstalled v${localVersion} from GitHub`);
397
+ p.log.success(color.green("Plugin reinstalled from GitHub!") +
398
+ color.dim(` — v${newVersion}`));
399
+ }
400
+ catch (err) {
401
+ const message = err instanceof Error ? err.message : String(err);
402
+ s.stop(color.red("Update failed"));
403
+ p.log.error(color.red("GitHub pull failed") + ` — ${message}`);
404
+ p.log.info(color.dim("Continuing with hooks/settings fix..."));
405
+ // Cleanup on failure
406
+ try {
407
+ execSync(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
408
+ }
409
+ catch { /* ignore */ }
410
+ }
411
+ // Step 3: Backup settings.json
412
+ p.log.step("Backing up settings.json...");
413
+ try {
414
+ accessSync(settingsPath, constants.R_OK);
415
+ const backupPath = settingsPath + ".bak";
416
+ copyFileSync(settingsPath, backupPath);
417
+ p.log.success(color.green("Backup created") + color.dim(" -> " + backupPath));
418
+ changes.push("Backed up settings.json");
419
+ }
420
+ catch {
421
+ p.log.warn(color.yellow("No existing settings.json to backup") +
422
+ " — a new one will be created");
423
+ }
424
+ // Step 4: Fix hooks
425
+ p.log.step("Configuring PreToolUse hooks...");
426
+ const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.sh");
427
+ const settings = readSettings() ?? {};
428
+ const desiredHookEntry = {
429
+ matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch|Task",
430
+ hooks: [
431
+ {
432
+ type: "command",
433
+ command: "bash " + hookScriptPath,
434
+ },
435
+ ],
436
+ };
437
+ const hooks = (settings.hooks ?? {});
438
+ const existingPreToolUse = hooks.PreToolUse;
439
+ if (existingPreToolUse && Array.isArray(existingPreToolUse)) {
440
+ const existingIdx = existingPreToolUse.findIndex((entry) => {
441
+ const entryHooks = entry.hooks;
442
+ return entryHooks?.some((h) => h.command?.includes("pretooluse.sh"));
443
+ });
444
+ if (existingIdx >= 0) {
445
+ existingPreToolUse[existingIdx] = desiredHookEntry;
446
+ p.log.info(color.dim("Updated existing PreToolUse hook entry"));
447
+ changes.push("Updated existing PreToolUse hook entry");
448
+ }
449
+ else {
450
+ existingPreToolUse.push(desiredHookEntry);
451
+ p.log.info(color.dim("Added PreToolUse hook entry"));
452
+ changes.push("Added PreToolUse hook entry to existing hooks");
453
+ }
454
+ hooks.PreToolUse = existingPreToolUse;
455
+ }
456
+ else {
457
+ hooks.PreToolUse = [desiredHookEntry];
458
+ p.log.info(color.dim("Created PreToolUse hooks section"));
459
+ changes.push("Created PreToolUse hooks section");
460
+ }
461
+ settings.hooks = hooks;
462
+ // Write updated settings
463
+ try {
464
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
465
+ p.log.success(color.green("Hooks configured") + color.dim(" -> " + settingsPath));
466
+ }
467
+ catch (err) {
468
+ const message = err instanceof Error ? err.message : String(err);
469
+ p.log.error(color.red("Failed to write settings.json") + " — " + message);
470
+ p.outro(color.red("Upgrade failed."));
471
+ process.exit(1);
472
+ }
473
+ // Step 5: Set hook script permissions
474
+ p.log.step("Setting hook script permissions...");
475
+ try {
476
+ accessSync(hookScriptPath, constants.R_OK);
477
+ chmodSync(hookScriptPath, 0o755);
478
+ p.log.success(color.green("Permissions set") + color.dim(" — chmod +x " + hookScriptPath));
479
+ changes.push("Set pretooluse.sh as executable");
480
+ }
481
+ catch {
482
+ p.log.error(color.red("Hook script not found") +
483
+ color.dim(" — expected at " + hookScriptPath));
484
+ }
485
+ // Step 6: Report
486
+ if (changes.length > 0) {
487
+ p.note(changes.map((c) => color.green(" + ") + c).join("\n"), "Changes Applied");
488
+ }
489
+ else {
490
+ p.log.info(color.dim("No changes were needed."));
491
+ }
492
+ // Step 7: Run doctor
493
+ p.log.step("Running doctor to verify...");
494
+ console.log();
495
+ await doctor();
496
+ }
497
+ /* -------------------------------------------------------
498
+ * Setup
499
+ * ------------------------------------------------------- */
25
500
  async function setup() {
26
501
  console.clear();
27
502
  p.intro(color.bgCyan(color.black(" context-mode setup ")));
@@ -142,52 +617,3 @@ async function setup() {
142
617
  " " +
143
618
  color.dim(available.length + " languages ready."));
144
619
  }
145
- async function doctor() {
146
- console.clear();
147
- p.intro(color.bgMagenta(color.white(" context-mode doctor ")));
148
- const s = p.spinner();
149
- s.start("Running diagnostics");
150
- const runtimes = detectRuntimes();
151
- const available = getAvailableLanguages(runtimes);
152
- s.stop("Diagnostics complete");
153
- // Runtime check
154
- p.note(getRuntimeSummary(runtimes), "Runtimes");
155
- // Speed tier
156
- if (hasBunRuntime()) {
157
- p.log.success(color.green("Performance: FAST") +
158
- " — Bun detected for JS/TS execution");
159
- }
160
- else {
161
- p.log.warn(color.yellow("Performance: NORMAL") +
162
- " — Using Node.js (install Bun for 3-5x speed boost)");
163
- }
164
- // Language coverage
165
- const total = 10;
166
- const pct = ((available.length / total) * 100).toFixed(0);
167
- p.log.info(`Language coverage: ${available.length}/${total} (${pct}%)` +
168
- color.dim(` — ${available.join(", ")}`));
169
- // Server test
170
- p.log.step("Testing server initialization...");
171
- try {
172
- const { PolyglotExecutor } = await import("./executor.js");
173
- const executor = new PolyglotExecutor({ runtimes });
174
- const result = await executor.execute({
175
- language: "javascript",
176
- code: 'console.log("ok");',
177
- timeout: 5000,
178
- });
179
- if (result.exitCode === 0 && result.stdout.trim() === "ok") {
180
- p.log.success(color.green("Server test: PASS"));
181
- }
182
- else {
183
- p.log.error(color.red("Server test: FAIL") + ` — exit ${result.exitCode}`);
184
- }
185
- }
186
- catch (err) {
187
- const message = err instanceof Error ? err.message : String(err);
188
- p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
189
- }
190
- p.outro(available.length >= 4
191
- ? color.green("Everything looks good!")
192
- : color.yellow("Some runtimes missing — install them for full coverage"));
193
- }