@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/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 Command5 } from "commander";
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 as resolve2, dirname as dirname2 } from "path";
57
- import { existsSync as existsSync2 } from "fs";
58
- import { fileURLToPath as fileURLToPath2 } from "url";
59
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
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 = resolve2(__dirname2, "..", "..");
62
- const localBin = resolve2(pkgRoot, "node_modules", ".bin", name);
63
- if (existsSync2(localBin)) return localBin;
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 resolve3, dirname as dirname3, basename } from "path";
307
- import { existsSync as existsSync3, readFileSync } from "fs";
308
- import { fileURLToPath as fileURLToPath3 } from "url";
309
- var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
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 = resolve3(cwd, "package.json");
331
- if (existsSync3(pkgPath)) {
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 (existsSync3(resolve3(cwd, file)) && !stacks.includes(stack)) {
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 runTool2(bin, args) {
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(findBin3("stale"), ["scan", "--format", "json", "--path", cwd]);
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(findBin3("envalid"), ["validate", "--format", "json"]);
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(findBin3("berth"), ["check", cwd, "--json"]);
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(findBin3("vow"), ["scan", "--format", "json", "--path", cwd]);
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 = existsSync3(resolve3(cwd, ".aware.json"));
499
+ const hasConfig = existsSync2(resolve2(cwd, ".aware.json"));
427
500
  if (!hasConfig) {
428
- const { exitCode: exitCode2 } = await runTool2(findBin3("aware"), ["init"]);
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(findBin3("aware"), ["doctor"]);
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, run all checks, suggest next steps");
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
- console.log(colorize2(` ${issueCount} tool(s) found issues, ${errorCount} could not run`, c2.yellow, c2.bold));
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 = existsSync3(resolve3(process.env.HOME ?? "~", ".claude", "settings.json"));
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
- writeFileSync(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
684
+ writeFileSync2(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
546
685
  }
547
686
  function sleep(ms) {
548
- return new Promise((resolve4) => setTimeout(resolve4, ms));
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 WhenLabs tools on a schedule and write results to ~/.whenlabs/status.json");
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
- var program = new Command5();
597
- program.name("when").version("0.1.0").description("The WhenLabs developer toolkit \u2014 6 tools, one install");
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-F46OPKIA.js");
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"));