@whenlabs/when 0.9.2 → 0.10.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/README.md +26 -3
- package/dist/chunk-JOMP6AU5.js +40 -0
- package/dist/index.js +392 -50
- package/dist/install-33GE3HKA.js +190 -0
- package/dist/mcp.js +757 -597
- package/package.json +3 -1
- package/templates/statusline.py +311 -0
- package/dist/install-F46OPKIA.js +0 -484
package/dist/index.js
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_FILENAME,
|
|
4
|
+
findBin,
|
|
5
|
+
loadConfig
|
|
6
|
+
} from "./chunk-JOMP6AU5.js";
|
|
2
7
|
import {
|
|
3
8
|
getStatusPath
|
|
4
9
|
} from "./chunk-4ZVSCJCJ.js";
|
|
5
10
|
|
|
6
11
|
// src/index.ts
|
|
7
|
-
import { Command as
|
|
12
|
+
import { Command as Command6 } from "commander";
|
|
8
13
|
|
|
9
14
|
// src/commands/delegate.ts
|
|
10
15
|
import { Command } from "commander";
|
|
11
16
|
import { spawn } from "child_process";
|
|
12
|
-
import { resolve, dirname } from "path";
|
|
13
|
-
import { existsSync } from "fs";
|
|
14
|
-
import { fileURLToPath } from "url";
|
|
15
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
function findBin(name) {
|
|
17
|
-
const pkgRoot = resolve(__dirname, "..");
|
|
18
|
-
const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
|
|
19
|
-
if (existsSync(localBin)) return localBin;
|
|
20
|
-
return name;
|
|
21
|
-
}
|
|
22
17
|
function createDelegateCommand(name, description, binName) {
|
|
23
18
|
const cmd = new Command(name);
|
|
24
19
|
cmd.description(description);
|
|
@@ -53,14 +48,14 @@ import { Command as Command2 } from "commander";
|
|
|
53
48
|
|
|
54
49
|
// src/utils/tool-runner.ts
|
|
55
50
|
import { spawn as spawn2 } from "child_process";
|
|
56
|
-
import { resolve
|
|
57
|
-
import { existsSync
|
|
58
|
-
import { fileURLToPath
|
|
59
|
-
var
|
|
51
|
+
import { resolve, dirname } from "path";
|
|
52
|
+
import { existsSync } from "fs";
|
|
53
|
+
import { fileURLToPath } from "url";
|
|
54
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
60
55
|
function findBin2(name) {
|
|
61
|
-
const pkgRoot =
|
|
62
|
-
const localBin =
|
|
63
|
-
if (
|
|
56
|
+
const pkgRoot = resolve(__dirname, "..", "..");
|
|
57
|
+
const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
|
|
58
|
+
if (existsSync(localBin)) return localBin;
|
|
64
59
|
return name;
|
|
65
60
|
}
|
|
66
61
|
function runTool(bin, args) {
|
|
@@ -303,10 +298,11 @@ function createDoctorCommand() {
|
|
|
303
298
|
// src/commands/init.ts
|
|
304
299
|
import { Command as Command3 } from "commander";
|
|
305
300
|
import { spawn as spawn3 } from "child_process";
|
|
306
|
-
import { resolve as
|
|
307
|
-
import { existsSync as
|
|
308
|
-
import { fileURLToPath as
|
|
309
|
-
|
|
301
|
+
import { resolve as resolve2, dirname as dirname2, basename } from "path";
|
|
302
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
303
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
304
|
+
import { stringify } from "yaml";
|
|
305
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
310
306
|
var c2 = {
|
|
311
307
|
reset: "\x1B[0m",
|
|
312
308
|
bold: "\x1B[1m",
|
|
@@ -319,16 +315,10 @@ var c2 = {
|
|
|
319
315
|
function colorize2(text, ...codes) {
|
|
320
316
|
return codes.join("") + text + c2.reset;
|
|
321
317
|
}
|
|
322
|
-
function findBin3(name) {
|
|
323
|
-
const pkgRoot = resolve3(__dirname3, "..");
|
|
324
|
-
const localBin = resolve3(pkgRoot, "node_modules", ".bin", name);
|
|
325
|
-
if (existsSync3(localBin)) return localBin;
|
|
326
|
-
return name;
|
|
327
|
-
}
|
|
328
318
|
function detectProject(cwd) {
|
|
329
319
|
let name = basename(cwd);
|
|
330
|
-
const pkgPath =
|
|
331
|
-
if (
|
|
320
|
+
const pkgPath = resolve2(cwd, "package.json");
|
|
321
|
+
if (existsSync2(pkgPath)) {
|
|
332
322
|
try {
|
|
333
323
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
334
324
|
if (pkg.name) name = pkg.name;
|
|
@@ -349,15 +339,31 @@ function detectProject(cwd) {
|
|
|
349
339
|
];
|
|
350
340
|
const stacks = [];
|
|
351
341
|
for (const [file, stack] of stackFiles) {
|
|
352
|
-
if (
|
|
342
|
+
if (existsSync2(resolve2(cwd, file)) && !stacks.includes(stack)) {
|
|
353
343
|
stacks.push(stack);
|
|
354
344
|
}
|
|
355
345
|
}
|
|
356
346
|
return { name, stack: stacks.length > 0 ? stacks.join(", ") : "unknown" };
|
|
357
347
|
}
|
|
358
|
-
function
|
|
348
|
+
function detectLicenseTemplate(cwd) {
|
|
349
|
+
const pkgPath = resolve2(cwd, "package.json");
|
|
350
|
+
if (existsSync2(pkgPath)) {
|
|
351
|
+
try {
|
|
352
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
353
|
+
const license = (pkg.license ?? "").toLowerCase();
|
|
354
|
+
if (["mit", "isc", "apache-2.0", "apache2", "bsd-2-clause", "bsd-3-clause"].some((l) => license.includes(l))) {
|
|
355
|
+
return "opensource";
|
|
356
|
+
}
|
|
357
|
+
if (license) return "commercial";
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return "opensource";
|
|
362
|
+
}
|
|
363
|
+
function runTool2(bin, args, cwd) {
|
|
359
364
|
return new Promise((resolveP) => {
|
|
360
365
|
const child = spawn3(bin, args, {
|
|
366
|
+
cwd,
|
|
361
367
|
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
|
|
362
368
|
});
|
|
363
369
|
let stdout = "";
|
|
@@ -372,8 +378,75 @@ function runTool2(bin, args) {
|
|
|
372
378
|
child.on("close", (code) => resolveP({ stdout, stderr, exitCode: code ?? 1 }));
|
|
373
379
|
});
|
|
374
380
|
}
|
|
381
|
+
async function bootstrapConfigs(cwd) {
|
|
382
|
+
const results = [];
|
|
383
|
+
const hasEnv = existsSync2(resolve2(cwd, ".env"));
|
|
384
|
+
const hasEnvSchema = existsSync2(resolve2(cwd, ".env.schema"));
|
|
385
|
+
if (hasEnv && !hasEnvSchema) {
|
|
386
|
+
const { exitCode } = await runTool2(findBin("envalid"), ["init"], cwd);
|
|
387
|
+
if (exitCode === 0) {
|
|
388
|
+
results.push({ label: ".env.schema", action: "created", detail: "Created .env.schema from .env" });
|
|
389
|
+
} else if (exitCode === 127) {
|
|
390
|
+
results.push({ label: ".env.schema", action: "error", detail: "envalid not found" });
|
|
391
|
+
} else {
|
|
392
|
+
results.push({ label: ".env.schema", action: "error", detail: "envalid init failed" });
|
|
393
|
+
}
|
|
394
|
+
} else if (hasEnvSchema) {
|
|
395
|
+
results.push({ label: ".env.schema", action: "skipped", detail: "Skipped (already exists)" });
|
|
396
|
+
} else {
|
|
397
|
+
results.push({ label: ".env.schema", action: "skipped", detail: "Skipped (no .env found)" });
|
|
398
|
+
}
|
|
399
|
+
const hasVowConfig = existsSync2(resolve2(cwd, ".vow.json"));
|
|
400
|
+
if (!hasVowConfig) {
|
|
401
|
+
const template = detectLicenseTemplate(cwd);
|
|
402
|
+
const { exitCode } = await runTool2(findBin("vow"), ["init", "--template", template], cwd);
|
|
403
|
+
if (exitCode === 0) {
|
|
404
|
+
results.push({ label: ".vow.json", action: "created", detail: `Created .vow.json (template: ${template})` });
|
|
405
|
+
} else if (exitCode === 127) {
|
|
406
|
+
results.push({ label: ".vow.json", action: "error", detail: "vow not found" });
|
|
407
|
+
} else {
|
|
408
|
+
results.push({ label: ".vow.json", action: "error", detail: "vow init failed" });
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
results.push({ label: ".vow.json", action: "skipped", detail: "Skipped (already exists)" });
|
|
412
|
+
}
|
|
413
|
+
const hasStaleConfig = existsSync2(resolve2(cwd, ".stale.yml"));
|
|
414
|
+
let staleScanNeeded = false;
|
|
415
|
+
if (!hasStaleConfig) {
|
|
416
|
+
const { exitCode } = await runTool2(findBin("stale"), ["init"], cwd);
|
|
417
|
+
if (exitCode === 0) {
|
|
418
|
+
results.push({ label: ".stale.yml", action: "created", detail: "Created .stale.yml" });
|
|
419
|
+
staleScanNeeded = true;
|
|
420
|
+
} else if (exitCode === 127) {
|
|
421
|
+
results.push({ label: ".stale.yml", action: "error", detail: "stale not found" });
|
|
422
|
+
} else {
|
|
423
|
+
results.push({ label: ".stale.yml", action: "error", detail: "stale init failed" });
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
results.push({ label: ".stale.yml", action: "skipped", detail: "Skipped (already exists)" });
|
|
427
|
+
}
|
|
428
|
+
const { exitCode: berthCode } = await runTool2(findBin("berth"), ["register", "--yes", "--dir", cwd], cwd);
|
|
429
|
+
if (berthCode === 0) {
|
|
430
|
+
results.push({ label: "berth ports", action: "created", detail: "Registered project ports" });
|
|
431
|
+
} else if (berthCode === 127) {
|
|
432
|
+
results.push({ label: "berth ports", action: "error", detail: "berth not found" });
|
|
433
|
+
} else {
|
|
434
|
+
results.push({ label: "berth ports", action: "error", detail: "berth register failed" });
|
|
435
|
+
}
|
|
436
|
+
return { results, staleScanNeeded };
|
|
437
|
+
}
|
|
438
|
+
function bootstrapIcon(action) {
|
|
439
|
+
switch (action) {
|
|
440
|
+
case "created":
|
|
441
|
+
return colorize2("+", c2.green);
|
|
442
|
+
case "skipped":
|
|
443
|
+
return colorize2("-", c2.dim);
|
|
444
|
+
case "error":
|
|
445
|
+
return colorize2("!", c2.yellow);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
375
448
|
async function scanStale(cwd) {
|
|
376
|
-
const { stdout, exitCode } = await runTool2(
|
|
449
|
+
const { stdout, exitCode } = await runTool2(findBin("stale"), ["scan", "--format", "json", "--path", cwd]);
|
|
377
450
|
if (exitCode === 127) return { label: "Doc drift (stale)", status: "error", detail: "stale not found" };
|
|
378
451
|
try {
|
|
379
452
|
const json = JSON.parse(stdout);
|
|
@@ -384,7 +457,7 @@ async function scanStale(cwd) {
|
|
|
384
457
|
}
|
|
385
458
|
}
|
|
386
459
|
async function scanEnvalid(cwd) {
|
|
387
|
-
const { stdout, exitCode } = await runTool2(
|
|
460
|
+
const { stdout, exitCode } = await runTool2(findBin("envalid"), ["validate", "--format", "json"]);
|
|
388
461
|
if (exitCode === 127) return { label: "Env validation (envalid)", status: "error", detail: "envalid not found" };
|
|
389
462
|
if (exitCode === 2 || stdout.includes("not found")) return { label: "Env validation (envalid)", status: "skipped", detail: "No .env.schema \u2014 run `envalid init`" };
|
|
390
463
|
try {
|
|
@@ -396,7 +469,7 @@ async function scanEnvalid(cwd) {
|
|
|
396
469
|
}
|
|
397
470
|
}
|
|
398
471
|
async function scanBerth(cwd) {
|
|
399
|
-
const { stdout, exitCode } = await runTool2(
|
|
472
|
+
const { stdout, exitCode } = await runTool2(findBin("berth"), ["check", cwd, "--json"]);
|
|
400
473
|
if (exitCode === 127) return { label: "Port conflicts (berth)", status: "error", detail: "berth not found" };
|
|
401
474
|
try {
|
|
402
475
|
const json = JSON.parse(stdout);
|
|
@@ -407,7 +480,7 @@ async function scanBerth(cwd) {
|
|
|
407
480
|
}
|
|
408
481
|
}
|
|
409
482
|
async function scanVow(cwd) {
|
|
410
|
-
const { stdout, exitCode } = await runTool2(
|
|
483
|
+
const { stdout, exitCode } = await runTool2(findBin("vow"), ["scan", "--format", "json", "--path", cwd]);
|
|
411
484
|
if (exitCode === 127) return { label: "License scan (vow)", status: "error", detail: "vow not found" };
|
|
412
485
|
const jsonStart = stdout.indexOf("{");
|
|
413
486
|
const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : stdout;
|
|
@@ -423,14 +496,14 @@ async function scanVow(cwd) {
|
|
|
423
496
|
}
|
|
424
497
|
}
|
|
425
498
|
async function scanAware(cwd) {
|
|
426
|
-
const hasConfig =
|
|
499
|
+
const hasConfig = existsSync2(resolve2(cwd, ".aware.json"));
|
|
427
500
|
if (!hasConfig) {
|
|
428
|
-
const { exitCode: exitCode2 } = await runTool2(
|
|
501
|
+
const { exitCode: exitCode2 } = await runTool2(findBin("aware"), ["init", "--force"], cwd);
|
|
429
502
|
if (exitCode2 === 0) return { label: "AI context (aware)", status: "ok", detail: "Generated .aware.json and context files" };
|
|
430
503
|
if (exitCode2 === 127) return { label: "AI context (aware)", status: "error", detail: "aware not found" };
|
|
431
504
|
return { label: "AI context (aware)", status: "skipped", detail: "Could not generate \u2014 run `aware init` manually" };
|
|
432
505
|
}
|
|
433
|
-
const { stdout, stderr, exitCode } = await runTool2(
|
|
506
|
+
const { stdout, stderr, exitCode } = await runTool2(findBin("aware"), ["doctor"], cwd);
|
|
434
507
|
if (exitCode === 127) return { label: "AI context (aware)", status: "error", detail: "aware not found" };
|
|
435
508
|
const combined = (stdout + stderr).trim();
|
|
436
509
|
const warnings = combined.split("\n").filter((l) => l.includes("\u26A0") || /warn/i.test(l)).length;
|
|
@@ -450,7 +523,7 @@ function statusIcon2(status) {
|
|
|
450
523
|
}
|
|
451
524
|
function createInitCommand() {
|
|
452
525
|
const cmd = new Command3("init");
|
|
453
|
-
cmd.description("Interactive onboarding \u2014 detect stack,
|
|
526
|
+
cmd.description("Interactive onboarding \u2014 detect stack, bootstrap tool configs, run all checks");
|
|
454
527
|
cmd.action(async () => {
|
|
455
528
|
const cwd = process.cwd();
|
|
456
529
|
console.log("");
|
|
@@ -461,6 +534,53 @@ function createInitCommand() {
|
|
|
461
534
|
console.log(` Stack: ${colorize2(project.stack, c2.cyan)}`);
|
|
462
535
|
console.log(` Path: ${colorize2(cwd, c2.dim)}`);
|
|
463
536
|
console.log("");
|
|
537
|
+
process.stdout.write(colorize2(" Bootstrapping tool configs\u2026", c2.dim) + "\n");
|
|
538
|
+
const { results: bootstrapResults, staleScanNeeded } = await bootstrapConfigs(cwd);
|
|
539
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
540
|
+
console.log(colorize2(" Bootstrap", c2.bold));
|
|
541
|
+
console.log(colorize2(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c2.dim));
|
|
542
|
+
for (const r of bootstrapResults) {
|
|
543
|
+
const icon = bootstrapIcon(r.action);
|
|
544
|
+
const label = r.label.padEnd(20);
|
|
545
|
+
const detail = r.action === "created" ? colorize2(r.detail, c2.green) : r.action === "error" ? colorize2(r.detail, c2.yellow) : colorize2(r.detail, c2.dim);
|
|
546
|
+
console.log(` ${icon} ${label} ${detail}`);
|
|
547
|
+
}
|
|
548
|
+
const whenlabsConfigPath = resolve2(cwd, CONFIG_FILENAME);
|
|
549
|
+
if (!existsSync2(whenlabsConfigPath)) {
|
|
550
|
+
try {
|
|
551
|
+
const mergedConfig = {};
|
|
552
|
+
const staleConfigPath = resolve2(cwd, ".stale.yml");
|
|
553
|
+
if (existsSync2(staleConfigPath)) {
|
|
554
|
+
mergedConfig["stale"] = {};
|
|
555
|
+
}
|
|
556
|
+
const vowConfigPath = resolve2(cwd, ".vow.json");
|
|
557
|
+
if (existsSync2(vowConfigPath)) {
|
|
558
|
+
try {
|
|
559
|
+
const vowData = JSON.parse(readFileSync(vowConfigPath, "utf-8"));
|
|
560
|
+
mergedConfig["vow"] = {
|
|
561
|
+
...typeof vowData.policy === "string" ? { policy: vowData.policy } : {},
|
|
562
|
+
...typeof vowData.production_only === "boolean" ? { production_only: vowData.production_only } : {}
|
|
563
|
+
};
|
|
564
|
+
} catch {
|
|
565
|
+
mergedConfig["vow"] = {};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const envSchemaPath = resolve2(cwd, ".env.schema");
|
|
569
|
+
if (existsSync2(envSchemaPath)) {
|
|
570
|
+
mergedConfig["envalid"] = { schema: ".env.schema" };
|
|
571
|
+
}
|
|
572
|
+
mergedConfig["berth"] = {};
|
|
573
|
+
mergedConfig["aware"] = {};
|
|
574
|
+
mergedConfig["velocity"] = {};
|
|
575
|
+
writeFileSync(whenlabsConfigPath, stringify(mergedConfig, { lineWidth: 0 }), "utf-8");
|
|
576
|
+
console.log(` ${colorize2("+", c2.green)} ${colorize2(CONFIG_FILENAME, c2.bold)} ${colorize2("created", c2.green)}`);
|
|
577
|
+
} catch {
|
|
578
|
+
console.log(` ${colorize2("!", c2.yellow)} Could not generate ${colorize2(CONFIG_FILENAME, c2.bold)}`);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
console.log(` ${colorize2("-", c2.dim)} ${colorize2(CONFIG_FILENAME, c2.bold)} ${colorize2("already exists", c2.dim)}`);
|
|
582
|
+
}
|
|
583
|
+
console.log("");
|
|
464
584
|
process.stdout.write(colorize2(" Scanning project\u2026", c2.dim) + "\n");
|
|
465
585
|
const results = await Promise.all([
|
|
466
586
|
scanStale(cwd),
|
|
@@ -478,17 +598,36 @@ function createInitCommand() {
|
|
|
478
598
|
const detail = r.status === "ok" ? colorize2(r.detail, c2.green) : r.status === "skipped" ? colorize2(r.detail, c2.dim) : r.status === "error" ? colorize2(r.detail, c2.yellow) : colorize2(r.detail, c2.red);
|
|
479
599
|
console.log(` ${icon} ${label} ${detail}`);
|
|
480
600
|
}
|
|
601
|
+
const staleResult = results.find((r) => r.label === "Doc drift (stale)");
|
|
602
|
+
if (staleResult?.status === "issues" || staleScanNeeded) {
|
|
603
|
+
process.stdout.write(colorize2(" Auto-fixing doc drift\u2026", c2.dim) + "\n");
|
|
604
|
+
const { exitCode: fixCode } = await runTool2(findBin("stale"), ["fix", "--apply"], cwd);
|
|
605
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
606
|
+
if (fixCode === 0) {
|
|
607
|
+
console.log(` ${colorize2("\u2713", c2.green)} ${colorize2("Doc drift auto-fixed", c2.green)}`);
|
|
608
|
+
} else if (fixCode === 127) {
|
|
609
|
+
console.log(` ${colorize2("!", c2.yellow)} ${colorize2("stale not found for auto-fix", c2.yellow)}`);
|
|
610
|
+
} else {
|
|
611
|
+
console.log(` ${colorize2("-", c2.dim)} ${colorize2("No high-confidence fixes available", c2.dim)}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
481
614
|
const issueCount = results.filter((r) => r.status === "issues").length;
|
|
482
615
|
const errorCount = results.filter((r) => r.status === "error").length;
|
|
616
|
+
const bootstrapErrors = bootstrapResults.filter((r) => r.action === "error").length;
|
|
617
|
+
const bootstrapCreated = bootstrapResults.filter((r) => r.action === "created").length;
|
|
483
618
|
console.log(colorize2(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c2.dim));
|
|
484
|
-
if (issueCount + errorCount === 0) {
|
|
619
|
+
if (issueCount + errorCount + bootstrapErrors === 0) {
|
|
485
620
|
console.log(colorize2(" All clear \u2014 project looks healthy!", c2.green, c2.bold));
|
|
486
621
|
} else {
|
|
487
|
-
|
|
622
|
+
const parts = [];
|
|
623
|
+
if (bootstrapCreated > 0) parts.push(`${bootstrapCreated} config(s) created`);
|
|
624
|
+
if (issueCount > 0) parts.push(`${issueCount} scan(s) found issues`);
|
|
625
|
+
if (errorCount + bootstrapErrors > 0) parts.push(`${errorCount + bootstrapErrors} tool(s) could not run`);
|
|
626
|
+
console.log(colorize2(` ${parts.join(", ")}`, c2.yellow, c2.bold));
|
|
488
627
|
}
|
|
489
628
|
console.log("");
|
|
490
629
|
console.log(colorize2(" Next steps:", c2.bold));
|
|
491
|
-
const mcpInstalled =
|
|
630
|
+
const mcpInstalled = existsSync2(resolve2(process.env.HOME ?? "~", ".claude", "settings.json"));
|
|
492
631
|
if (!mcpInstalled) {
|
|
493
632
|
console.log(` ${colorize2("\u2022", c2.cyan)} Run ${colorize2("when install", c2.bold)} to connect MCP tools to Claude Code`);
|
|
494
633
|
}
|
|
@@ -505,7 +644,7 @@ function createInitCommand() {
|
|
|
505
644
|
import { Command as Command4 } from "commander";
|
|
506
645
|
import { join } from "path";
|
|
507
646
|
import { homedir } from "os";
|
|
508
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
647
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
509
648
|
var STATUS_DIR = join(homedir(), ".whenlabs");
|
|
510
649
|
function toolResultToStatus(r) {
|
|
511
650
|
const count = r.issues + r.warnings;
|
|
@@ -542,14 +681,14 @@ function writeStatus(results) {
|
|
|
542
681
|
},
|
|
543
682
|
summary: buildSummary(results)
|
|
544
683
|
};
|
|
545
|
-
|
|
684
|
+
writeFileSync2(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
|
|
546
685
|
}
|
|
547
686
|
function sleep(ms) {
|
|
548
|
-
return new Promise((
|
|
687
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
549
688
|
}
|
|
550
689
|
function createWatchCommand() {
|
|
551
690
|
const cmd = new Command4("watch");
|
|
552
|
-
cmd.description("Run all 5
|
|
691
|
+
cmd.description("Run all 5 CLI tools on a schedule and write results to ~/.whenlabs/status.json (velocity is embedded and always-on \u2014 it does not participate in scheduled scans)");
|
|
553
692
|
cmd.option("--once", "Run a single scan and exit");
|
|
554
693
|
cmd.option("--interval <seconds>", "Override the default scan interval (seconds)", "60");
|
|
555
694
|
cmd.action(async (options) => {
|
|
@@ -592,11 +731,213 @@ function createWatchCommand() {
|
|
|
592
731
|
return cmd;
|
|
593
732
|
}
|
|
594
733
|
|
|
734
|
+
// src/commands/config.ts
|
|
735
|
+
import { Command as Command5 } from "commander";
|
|
736
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
737
|
+
import { resolve as resolve3 } from "path";
|
|
738
|
+
import { parse, stringify as stringify2 } from "yaml";
|
|
739
|
+
var c3 = {
|
|
740
|
+
reset: "\x1B[0m",
|
|
741
|
+
bold: "\x1B[1m",
|
|
742
|
+
green: "\x1B[32m",
|
|
743
|
+
yellow: "\x1B[33m",
|
|
744
|
+
red: "\x1B[31m",
|
|
745
|
+
cyan: "\x1B[36m",
|
|
746
|
+
dim: "\x1B[2m"
|
|
747
|
+
};
|
|
748
|
+
function colorize3(text, ...codes) {
|
|
749
|
+
return codes.join("") + text + c3.reset;
|
|
750
|
+
}
|
|
751
|
+
function readExistingToolConfigs(cwd) {
|
|
752
|
+
const config = {};
|
|
753
|
+
const stalePath = resolve3(cwd, ".stale.yml");
|
|
754
|
+
if (existsSync3(stalePath)) {
|
|
755
|
+
try {
|
|
756
|
+
const raw = readFileSync2(stalePath, "utf-8");
|
|
757
|
+
const parsed = parse(raw);
|
|
758
|
+
config.stale = {
|
|
759
|
+
ignore: Array.isArray(parsed?.ignore) ? parsed.ignore : void 0,
|
|
760
|
+
deep: typeof parsed?.deep === "boolean" ? parsed.deep : void 0
|
|
761
|
+
};
|
|
762
|
+
} catch {
|
|
763
|
+
config.stale = {};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const vowPath = resolve3(cwd, ".vow.json");
|
|
767
|
+
if (existsSync3(vowPath)) {
|
|
768
|
+
try {
|
|
769
|
+
const vow = JSON.parse(readFileSync2(vowPath, "utf-8"));
|
|
770
|
+
config.vow = {
|
|
771
|
+
policy: typeof vow.policy === "string" ? vow.policy : void 0,
|
|
772
|
+
production_only: typeof vow.production_only === "boolean" ? vow.production_only : void 0
|
|
773
|
+
};
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const schemaPath = resolve3(cwd, ".env.schema");
|
|
778
|
+
if (existsSync3(schemaPath)) {
|
|
779
|
+
config.envalid = { schema: ".env.schema" };
|
|
780
|
+
}
|
|
781
|
+
return config;
|
|
782
|
+
}
|
|
783
|
+
function generateDefaultConfig(cwd) {
|
|
784
|
+
const base = readExistingToolConfigs(cwd);
|
|
785
|
+
return {
|
|
786
|
+
stale: base.stale ?? {},
|
|
787
|
+
envalid: base.envalid ?? {},
|
|
788
|
+
vow: base.vow ?? {},
|
|
789
|
+
berth: {},
|
|
790
|
+
aware: {},
|
|
791
|
+
velocity: {}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function validateConfig(config) {
|
|
795
|
+
const errors = [];
|
|
796
|
+
if (config.stale !== void 0 && typeof config.stale !== "object") {
|
|
797
|
+
errors.push("stale: must be an object");
|
|
798
|
+
}
|
|
799
|
+
if (config.stale?.ignore !== void 0 && !Array.isArray(config.stale.ignore)) {
|
|
800
|
+
errors.push("stale.ignore: must be an array of strings");
|
|
801
|
+
}
|
|
802
|
+
if (config.stale?.deep !== void 0 && typeof config.stale.deep !== "boolean") {
|
|
803
|
+
errors.push("stale.deep: must be a boolean");
|
|
804
|
+
}
|
|
805
|
+
if (config.envalid !== void 0 && typeof config.envalid !== "object") {
|
|
806
|
+
errors.push("envalid: must be an object");
|
|
807
|
+
}
|
|
808
|
+
if (config.envalid?.schema !== void 0 && typeof config.envalid.schema !== "string") {
|
|
809
|
+
errors.push("envalid.schema: must be a string");
|
|
810
|
+
}
|
|
811
|
+
if (config.envalid?.environments !== void 0 && !Array.isArray(config.envalid.environments)) {
|
|
812
|
+
errors.push("envalid.environments: must be an array of strings");
|
|
813
|
+
}
|
|
814
|
+
if (config.vow !== void 0 && typeof config.vow !== "object") {
|
|
815
|
+
errors.push("vow: must be an object");
|
|
816
|
+
}
|
|
817
|
+
if (config.vow?.policy !== void 0 && typeof config.vow.policy !== "string") {
|
|
818
|
+
errors.push("vow.policy: must be a string");
|
|
819
|
+
}
|
|
820
|
+
if (config.vow?.production_only !== void 0 && typeof config.vow.production_only !== "boolean") {
|
|
821
|
+
errors.push("vow.production_only: must be a boolean");
|
|
822
|
+
}
|
|
823
|
+
if (config.berth !== void 0 && typeof config.berth !== "object") {
|
|
824
|
+
errors.push("berth: must be an object");
|
|
825
|
+
}
|
|
826
|
+
if (config.berth?.ports !== void 0) {
|
|
827
|
+
if (typeof config.berth.ports !== "object" || Array.isArray(config.berth.ports)) {
|
|
828
|
+
errors.push("berth.ports: must be a key/value map of port names to numbers");
|
|
829
|
+
} else {
|
|
830
|
+
for (const [k, v] of Object.entries(config.berth.ports)) {
|
|
831
|
+
if (typeof v !== "number") errors.push(`berth.ports.${k}: must be a number`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (config.aware !== void 0 && typeof config.aware !== "object") {
|
|
836
|
+
errors.push("aware: must be an object");
|
|
837
|
+
}
|
|
838
|
+
if (config.aware?.targets !== void 0 && !Array.isArray(config.aware.targets)) {
|
|
839
|
+
errors.push("aware.targets: must be an array of strings");
|
|
840
|
+
}
|
|
841
|
+
if (config.velocity !== void 0 && typeof config.velocity !== "object") {
|
|
842
|
+
errors.push("velocity: must be an object");
|
|
843
|
+
}
|
|
844
|
+
if (config.velocity?.project !== void 0 && typeof config.velocity.project !== "string") {
|
|
845
|
+
errors.push("velocity.project: must be a string");
|
|
846
|
+
}
|
|
847
|
+
return errors;
|
|
848
|
+
}
|
|
849
|
+
function createConfigCommand() {
|
|
850
|
+
const cmd = new Command5("config");
|
|
851
|
+
cmd.description("Manage unified .whenlabs.yml project config");
|
|
852
|
+
cmd.action(() => {
|
|
853
|
+
const cwd = process.cwd();
|
|
854
|
+
const configPath = resolve3(cwd, CONFIG_FILENAME);
|
|
855
|
+
console.log("");
|
|
856
|
+
console.log(colorize3(" when config", c3.bold, c3.cyan));
|
|
857
|
+
console.log(colorize3(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c3.dim));
|
|
858
|
+
if (!existsSync3(configPath)) {
|
|
859
|
+
console.log(` ${colorize3("-", c3.dim)} No ${colorize3(CONFIG_FILENAME, c3.bold)} found`);
|
|
860
|
+
console.log(` ${colorize3("\u2022", c3.dim)} Run ${colorize3("when config init", c3.bold)} to generate one`);
|
|
861
|
+
console.log("");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
865
|
+
console.log(` ${colorize3(configPath, c3.dim)}`);
|
|
866
|
+
console.log("");
|
|
867
|
+
for (const line of raw.split("\n")) {
|
|
868
|
+
console.log(` ${line}`);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
const initCmd = new Command5("init");
|
|
872
|
+
initCmd.description(`Generate ${CONFIG_FILENAME} from existing tool configs`);
|
|
873
|
+
initCmd.option("--force", "Overwrite existing config");
|
|
874
|
+
initCmd.action((options) => {
|
|
875
|
+
const cwd = process.cwd();
|
|
876
|
+
const configPath = resolve3(cwd, CONFIG_FILENAME);
|
|
877
|
+
console.log("");
|
|
878
|
+
console.log(colorize3(" when config init", c3.bold, c3.cyan));
|
|
879
|
+
console.log(colorize3(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c3.dim));
|
|
880
|
+
if (existsSync3(configPath) && !options.force) {
|
|
881
|
+
console.log(` ${colorize3("-", c3.dim)} ${colorize3(CONFIG_FILENAME, c3.bold)} already exists \u2014 use ${colorize3("--force", c3.bold)} to overwrite`);
|
|
882
|
+
console.log("");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const config = generateDefaultConfig(cwd);
|
|
886
|
+
const yaml = stringify2(config, { lineWidth: 0 });
|
|
887
|
+
writeFileSync3(configPath, yaml, "utf-8");
|
|
888
|
+
console.log(` ${colorize3("+", c3.green)} Created ${colorize3(CONFIG_FILENAME, c3.bold)}`);
|
|
889
|
+
console.log("");
|
|
890
|
+
for (const line of yaml.split("\n")) {
|
|
891
|
+
if (line.trim()) console.log(` ${colorize3(line, c3.dim)}`);
|
|
892
|
+
}
|
|
893
|
+
console.log("");
|
|
894
|
+
});
|
|
895
|
+
const validateCmd = new Command5("validate");
|
|
896
|
+
validateCmd.description(`Validate ${CONFIG_FILENAME} structure`);
|
|
897
|
+
validateCmd.action(() => {
|
|
898
|
+
const cwd = process.cwd();
|
|
899
|
+
const configPath = resolve3(cwd, CONFIG_FILENAME);
|
|
900
|
+
console.log("");
|
|
901
|
+
console.log(colorize3(" when config validate", c3.bold, c3.cyan));
|
|
902
|
+
console.log(colorize3(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", c3.dim));
|
|
903
|
+
if (!existsSync3(configPath)) {
|
|
904
|
+
console.log(` ${colorize3("-", c3.dim)} No ${colorize3(CONFIG_FILENAME, c3.bold)} found \u2014 nothing to validate`);
|
|
905
|
+
console.log("");
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const config = loadConfig(cwd);
|
|
909
|
+
if (!config) {
|
|
910
|
+
console.log(` ${colorize3("!", c3.yellow)} Could not parse ${colorize3(CONFIG_FILENAME, c3.bold)} \u2014 invalid YAML`);
|
|
911
|
+
console.log("");
|
|
912
|
+
process.exitCode = 1;
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const errors = validateConfig(config);
|
|
916
|
+
if (errors.length === 0) {
|
|
917
|
+
console.log(` ${colorize3("\u2713", c3.green)} ${colorize3(CONFIG_FILENAME, c3.bold)} is valid`);
|
|
918
|
+
} else {
|
|
919
|
+
for (const err of errors) {
|
|
920
|
+
console.log(` ${colorize3("\u2717", c3.red)} ${err}`);
|
|
921
|
+
}
|
|
922
|
+
process.exitCode = 1;
|
|
923
|
+
}
|
|
924
|
+
console.log("");
|
|
925
|
+
});
|
|
926
|
+
cmd.addCommand(initCmd);
|
|
927
|
+
cmd.addCommand(validateCmd);
|
|
928
|
+
return cmd;
|
|
929
|
+
}
|
|
930
|
+
|
|
595
931
|
// src/index.ts
|
|
596
|
-
|
|
597
|
-
|
|
932
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
933
|
+
import { resolve as resolve4, dirname as dirname3 } from "path";
|
|
934
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
935
|
+
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
936
|
+
var { version } = JSON.parse(readFileSync3(resolve4(__dirname3, "..", "package.json"), "utf8"));
|
|
937
|
+
var program = new Command6();
|
|
938
|
+
program.name("when").version(version).description("The WhenLabs developer toolkit \u2014 6 tools, one install");
|
|
598
939
|
program.command("install").description("Install all WhenLabs tools globally (MCP server + CLAUDE.md instructions)").option("--cursor", "Install MCP servers into Cursor (~/.cursor/mcp.json)").option("--vscode", "Install MCP servers into VS Code (settings.json)").option("--windsurf", "Install MCP servers into Windsurf (~/.codeium/windsurf/mcp_config.json)").option("--all", "Install MCP servers into all supported editors").action(async (options) => {
|
|
599
|
-
const { install } = await import("./install-
|
|
940
|
+
const { install } = await import("./install-33GE3HKA.js");
|
|
600
941
|
await install(options);
|
|
601
942
|
});
|
|
602
943
|
program.command("uninstall").description("Remove all WhenLabs tools").option("--cursor", "Remove MCP servers from Cursor").option("--vscode", "Remove MCP servers from VS Code").option("--windsurf", "Remove MCP servers from Windsurf").option("--all", "Remove MCP servers from all supported editors").action(async (options) => {
|
|
@@ -614,6 +955,7 @@ program.command("ci").description("Run stale, envalid, and vow checks \u2014 exi
|
|
|
614
955
|
program.addCommand(createInitCommand());
|
|
615
956
|
program.addCommand(createDoctorCommand());
|
|
616
957
|
program.addCommand(createWatchCommand());
|
|
958
|
+
program.addCommand(createConfigCommand());
|
|
617
959
|
program.addCommand(createDelegateCommand("stale", "Detect documentation drift in your codebase"));
|
|
618
960
|
program.addCommand(createDelegateCommand("envalid", "Validate .env files against a type-safe schema"));
|
|
619
961
|
program.addCommand(createDelegateCommand("berth", "Detect and resolve port conflicts"));
|