@whenlabs/when 0.9.3 → 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 +21 -2
- package/dist/chunk-JOMP6AU5.js +40 -0
- package/dist/index.js +372 -28
- package/dist/{install-V24JHOA2.js → install-33GE3HKA.js} +7 -0
- package/dist/mcp.js +129 -2
- package/package.json +2 -1
- package/dist/chunk-2A2EZZF4.js +0 -19
package/README.md
CHANGED
|
@@ -51,20 +51,28 @@ These tools are available to Claude in every session after install:
|
|
|
51
51
|
| `velocity_history` | Show task history |
|
|
52
52
|
| `stale_scan` | Detect documentation drift |
|
|
53
53
|
| `stale_fix` | Auto-fix documentation drift (wrong paths, dead links, phantom env vars) |
|
|
54
|
+
| `stale_auto_fix` | Scan + auto-fix drift in one call |
|
|
54
55
|
| `envalid_validate` | Validate .env files against schemas |
|
|
55
56
|
| `envalid_detect` | Find undocumented env vars in codebase |
|
|
56
57
|
| `envalid_generate_schema` | Generate .env.schema from code analysis |
|
|
58
|
+
| `envalid_auto_fix` | Detect undocumented vars + auto-generate schema entries |
|
|
57
59
|
| `berth_status` | Show active ports and conflicts |
|
|
58
60
|
| `berth_check` | Scan project for port conflicts |
|
|
59
61
|
| `berth_resolve` | Auto-resolve port conflicts (kill or reassign) |
|
|
62
|
+
| `berth_auto_resolve` | Check + auto-resolve conflicts in one call |
|
|
60
63
|
| `aware_init` | Auto-detect stack, generate AI context files |
|
|
61
64
|
| `aware_doctor` | Diagnose project health and config issues |
|
|
65
|
+
| `aware_auto_sync` | Diagnose + auto-sync stale AI context files |
|
|
62
66
|
| `vow_scan` | Scan and summarize dependency licenses |
|
|
63
67
|
| `vow_check` | Validate licenses against policy |
|
|
64
68
|
| `vow_hook_install` | Install pre-commit license check hook |
|
|
65
69
|
|
|
66
70
|
> This table shows a highlights subset. Run `when <tool> --help` for all available commands per tool.
|
|
67
71
|
|
|
72
|
+
### Cross-tool Intelligence
|
|
73
|
+
|
|
74
|
+
Tools automatically suggest follow-up actions when they detect issues relevant to other tools. For example, `aware_init` triggers a `stale_scan` when it generates new files, and `envalid_detect` suggests `berth_register` when it finds service URL env vars. These cascading suggestions surface as "Tip:" lines in tool output.
|
|
75
|
+
|
|
68
76
|
## Multi-Editor Support
|
|
69
77
|
|
|
70
78
|
Install MCP servers into other editors alongside Claude Code:
|
|
@@ -83,7 +91,10 @@ Without flags, `install` targets Claude Code only.
|
|
|
83
91
|
You can also run tools directly from the command line:
|
|
84
92
|
|
|
85
93
|
```bash
|
|
86
|
-
when init # Onboard a project —
|
|
94
|
+
when init # Onboard a project — bootstrap configs, run all tools, auto-fix
|
|
95
|
+
when config # Show unified .whenlabs.yml config
|
|
96
|
+
when config init # Generate .whenlabs.yml from existing tool configs
|
|
97
|
+
when config validate # Validate config structure
|
|
87
98
|
when stale scan
|
|
88
99
|
when stale fix # Auto-fix documentation drift
|
|
89
100
|
when envalid validate
|
|
@@ -102,7 +113,15 @@ when ci # Run checks for CI (exits 1 on issues)
|
|
|
102
113
|
|
|
103
114
|
### `when init`
|
|
104
115
|
|
|
105
|
-
One command to onboard any project
|
|
116
|
+
One command to fully onboard any project:
|
|
117
|
+
1. **Bootstrap** — creates `.env.schema`, `.vow.json`, `.stale.yml`, and registers berth ports based on your project
|
|
118
|
+
2. **Scan** — runs all 5 CLI tools in parallel
|
|
119
|
+
3. **Auto-fix** — automatically fixes stale drift if detected
|
|
120
|
+
4. **Config** — generates a unified `.whenlabs.yml` from the bootstrapped configs
|
|
121
|
+
|
|
122
|
+
### `when config`
|
|
123
|
+
|
|
124
|
+
Manage the unified `.whenlabs.yml` project config. All six tools read their settings from this single file instead of separate config files. Subcommands: `init` (generate from existing configs), `validate` (check structure).
|
|
106
125
|
|
|
107
126
|
### `when doctor`
|
|
108
127
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/find-bin.ts
|
|
4
|
+
import { resolve, dirname } from "path";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function findBin(name) {
|
|
9
|
+
const pkgRoot = resolve(__dirname, "../..");
|
|
10
|
+
const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
|
|
11
|
+
if (existsSync(localBin)) return localBin;
|
|
12
|
+
const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
|
|
13
|
+
if (existsSync(directCli)) return directCli;
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/config/whenlabs-config.ts
|
|
18
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
19
|
+
import { resolve as resolve2 } from "path";
|
|
20
|
+
import { parse } from "yaml";
|
|
21
|
+
var CONFIG_FILENAME = ".whenlabs.yml";
|
|
22
|
+
function loadConfig(projectPath) {
|
|
23
|
+
const dir = projectPath ?? process.cwd();
|
|
24
|
+
const configPath = resolve2(dir, CONFIG_FILENAME);
|
|
25
|
+
if (!existsSync2(configPath)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
28
|
+
const parsed = parse(raw);
|
|
29
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
findBin,
|
|
38
|
+
CONFIG_FILENAME,
|
|
39
|
+
loadConfig
|
|
40
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
CONFIG_FILENAME,
|
|
4
|
+
findBin,
|
|
5
|
+
loadConfig
|
|
6
|
+
} from "./chunk-JOMP6AU5.js";
|
|
5
7
|
import {
|
|
6
8
|
getStatusPath
|
|
7
9
|
} from "./chunk-4ZVSCJCJ.js";
|
|
8
10
|
|
|
9
11
|
// src/index.ts
|
|
10
|
-
import { Command as
|
|
12
|
+
import { Command as Command6 } from "commander";
|
|
11
13
|
|
|
12
14
|
// src/commands/delegate.ts
|
|
13
15
|
import { Command } from "commander";
|
|
@@ -297,8 +299,9 @@ function createDoctorCommand() {
|
|
|
297
299
|
import { Command as Command3 } from "commander";
|
|
298
300
|
import { spawn as spawn3 } from "child_process";
|
|
299
301
|
import { resolve as resolve2, dirname as dirname2, basename } from "path";
|
|
300
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
302
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
301
303
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
304
|
+
import { stringify } from "yaml";
|
|
302
305
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
303
306
|
var c2 = {
|
|
304
307
|
reset: "\x1B[0m",
|
|
@@ -312,12 +315,6 @@ var c2 = {
|
|
|
312
315
|
function colorize2(text, ...codes) {
|
|
313
316
|
return codes.join("") + text + c2.reset;
|
|
314
317
|
}
|
|
315
|
-
function findBin3(name) {
|
|
316
|
-
const pkgRoot = resolve2(__dirname2, "..");
|
|
317
|
-
const localBin = resolve2(pkgRoot, "node_modules", ".bin", name);
|
|
318
|
-
if (existsSync2(localBin)) return localBin;
|
|
319
|
-
return name;
|
|
320
|
-
}
|
|
321
318
|
function detectProject(cwd) {
|
|
322
319
|
let name = basename(cwd);
|
|
323
320
|
const pkgPath = resolve2(cwd, "package.json");
|
|
@@ -348,9 +345,25 @@ function detectProject(cwd) {
|
|
|
348
345
|
}
|
|
349
346
|
return { name, stack: stacks.length > 0 ? stacks.join(", ") : "unknown" };
|
|
350
347
|
}
|
|
351
|
-
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) {
|
|
352
364
|
return new Promise((resolveP) => {
|
|
353
365
|
const child = spawn3(bin, args, {
|
|
366
|
+
cwd,
|
|
354
367
|
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
|
|
355
368
|
});
|
|
356
369
|
let stdout = "";
|
|
@@ -365,8 +378,75 @@ function runTool2(bin, args) {
|
|
|
365
378
|
child.on("close", (code) => resolveP({ stdout, stderr, exitCode: code ?? 1 }));
|
|
366
379
|
});
|
|
367
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
|
+
}
|
|
368
448
|
async function scanStale(cwd) {
|
|
369
|
-
const { stdout, exitCode } = await runTool2(
|
|
449
|
+
const { stdout, exitCode } = await runTool2(findBin("stale"), ["scan", "--format", "json", "--path", cwd]);
|
|
370
450
|
if (exitCode === 127) return { label: "Doc drift (stale)", status: "error", detail: "stale not found" };
|
|
371
451
|
try {
|
|
372
452
|
const json = JSON.parse(stdout);
|
|
@@ -377,7 +457,7 @@ async function scanStale(cwd) {
|
|
|
377
457
|
}
|
|
378
458
|
}
|
|
379
459
|
async function scanEnvalid(cwd) {
|
|
380
|
-
const { stdout, exitCode } = await runTool2(
|
|
460
|
+
const { stdout, exitCode } = await runTool2(findBin("envalid"), ["validate", "--format", "json"]);
|
|
381
461
|
if (exitCode === 127) return { label: "Env validation (envalid)", status: "error", detail: "envalid not found" };
|
|
382
462
|
if (exitCode === 2 || stdout.includes("not found")) return { label: "Env validation (envalid)", status: "skipped", detail: "No .env.schema \u2014 run `envalid init`" };
|
|
383
463
|
try {
|
|
@@ -389,7 +469,7 @@ async function scanEnvalid(cwd) {
|
|
|
389
469
|
}
|
|
390
470
|
}
|
|
391
471
|
async function scanBerth(cwd) {
|
|
392
|
-
const { stdout, exitCode } = await runTool2(
|
|
472
|
+
const { stdout, exitCode } = await runTool2(findBin("berth"), ["check", cwd, "--json"]);
|
|
393
473
|
if (exitCode === 127) return { label: "Port conflicts (berth)", status: "error", detail: "berth not found" };
|
|
394
474
|
try {
|
|
395
475
|
const json = JSON.parse(stdout);
|
|
@@ -400,7 +480,7 @@ async function scanBerth(cwd) {
|
|
|
400
480
|
}
|
|
401
481
|
}
|
|
402
482
|
async function scanVow(cwd) {
|
|
403
|
-
const { stdout, exitCode } = await runTool2(
|
|
483
|
+
const { stdout, exitCode } = await runTool2(findBin("vow"), ["scan", "--format", "json", "--path", cwd]);
|
|
404
484
|
if (exitCode === 127) return { label: "License scan (vow)", status: "error", detail: "vow not found" };
|
|
405
485
|
const jsonStart = stdout.indexOf("{");
|
|
406
486
|
const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : stdout;
|
|
@@ -418,12 +498,12 @@ async function scanVow(cwd) {
|
|
|
418
498
|
async function scanAware(cwd) {
|
|
419
499
|
const hasConfig = existsSync2(resolve2(cwd, ".aware.json"));
|
|
420
500
|
if (!hasConfig) {
|
|
421
|
-
const { exitCode: exitCode2 } = await runTool2(
|
|
501
|
+
const { exitCode: exitCode2 } = await runTool2(findBin("aware"), ["init", "--force"], cwd);
|
|
422
502
|
if (exitCode2 === 0) return { label: "AI context (aware)", status: "ok", detail: "Generated .aware.json and context files" };
|
|
423
503
|
if (exitCode2 === 127) return { label: "AI context (aware)", status: "error", detail: "aware not found" };
|
|
424
504
|
return { label: "AI context (aware)", status: "skipped", detail: "Could not generate \u2014 run `aware init` manually" };
|
|
425
505
|
}
|
|
426
|
-
const { stdout, stderr, exitCode } = await runTool2(
|
|
506
|
+
const { stdout, stderr, exitCode } = await runTool2(findBin("aware"), ["doctor"], cwd);
|
|
427
507
|
if (exitCode === 127) return { label: "AI context (aware)", status: "error", detail: "aware not found" };
|
|
428
508
|
const combined = (stdout + stderr).trim();
|
|
429
509
|
const warnings = combined.split("\n").filter((l) => l.includes("\u26A0") || /warn/i.test(l)).length;
|
|
@@ -443,7 +523,7 @@ function statusIcon2(status) {
|
|
|
443
523
|
}
|
|
444
524
|
function createInitCommand() {
|
|
445
525
|
const cmd = new Command3("init");
|
|
446
|
-
cmd.description("Interactive onboarding \u2014 detect stack,
|
|
526
|
+
cmd.description("Interactive onboarding \u2014 detect stack, bootstrap tool configs, run all checks");
|
|
447
527
|
cmd.action(async () => {
|
|
448
528
|
const cwd = process.cwd();
|
|
449
529
|
console.log("");
|
|
@@ -454,6 +534,53 @@ function createInitCommand() {
|
|
|
454
534
|
console.log(` Stack: ${colorize2(project.stack, c2.cyan)}`);
|
|
455
535
|
console.log(` Path: ${colorize2(cwd, c2.dim)}`);
|
|
456
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("");
|
|
457
584
|
process.stdout.write(colorize2(" Scanning project\u2026", c2.dim) + "\n");
|
|
458
585
|
const results = await Promise.all([
|
|
459
586
|
scanStale(cwd),
|
|
@@ -471,13 +598,32 @@ function createInitCommand() {
|
|
|
471
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);
|
|
472
599
|
console.log(` ${icon} ${label} ${detail}`);
|
|
473
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
|
+
}
|
|
474
614
|
const issueCount = results.filter((r) => r.status === "issues").length;
|
|
475
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;
|
|
476
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));
|
|
477
|
-
if (issueCount + errorCount === 0) {
|
|
619
|
+
if (issueCount + errorCount + bootstrapErrors === 0) {
|
|
478
620
|
console.log(colorize2(" All clear \u2014 project looks healthy!", c2.green, c2.bold));
|
|
479
621
|
} else {
|
|
480
|
-
|
|
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));
|
|
481
627
|
}
|
|
482
628
|
console.log("");
|
|
483
629
|
console.log(colorize2(" Next steps:", c2.bold));
|
|
@@ -498,7 +644,7 @@ function createInitCommand() {
|
|
|
498
644
|
import { Command as Command4 } from "commander";
|
|
499
645
|
import { join } from "path";
|
|
500
646
|
import { homedir } from "os";
|
|
501
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
647
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
502
648
|
var STATUS_DIR = join(homedir(), ".whenlabs");
|
|
503
649
|
function toolResultToStatus(r) {
|
|
504
650
|
const count = r.issues + r.warnings;
|
|
@@ -535,10 +681,10 @@ function writeStatus(results) {
|
|
|
535
681
|
},
|
|
536
682
|
summary: buildSummary(results)
|
|
537
683
|
};
|
|
538
|
-
|
|
684
|
+
writeFileSync2(getStatusPath(), JSON.stringify(status, null, 2) + "\n");
|
|
539
685
|
}
|
|
540
686
|
function sleep(ms) {
|
|
541
|
-
return new Promise((
|
|
687
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
542
688
|
}
|
|
543
689
|
function createWatchCommand() {
|
|
544
690
|
const cmd = new Command4("watch");
|
|
@@ -585,16 +731,213 @@ function createWatchCommand() {
|
|
|
585
731
|
return cmd;
|
|
586
732
|
}
|
|
587
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
|
+
|
|
588
931
|
// src/index.ts
|
|
589
|
-
import { readFileSync as
|
|
590
|
-
import { resolve as
|
|
932
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
933
|
+
import { resolve as resolve4, dirname as dirname3 } from "path";
|
|
591
934
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
592
935
|
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
593
|
-
var { version } = JSON.parse(
|
|
594
|
-
var program = new
|
|
936
|
+
var { version } = JSON.parse(readFileSync3(resolve4(__dirname3, "..", "package.json"), "utf8"));
|
|
937
|
+
var program = new Command6();
|
|
595
938
|
program.name("when").version(version).description("The WhenLabs developer toolkit \u2014 6 tools, one install");
|
|
596
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) => {
|
|
597
|
-
const { install } = await import("./install-
|
|
940
|
+
const { install } = await import("./install-33GE3HKA.js");
|
|
598
941
|
await install(options);
|
|
599
942
|
});
|
|
600
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) => {
|
|
@@ -612,6 +955,7 @@ program.command("ci").description("Run stale, envalid, and vow checks \u2014 exi
|
|
|
612
955
|
program.addCommand(createInitCommand());
|
|
613
956
|
program.addCommand(createDoctorCommand());
|
|
614
957
|
program.addCommand(createWatchCommand());
|
|
958
|
+
program.addCommand(createConfigCommand());
|
|
615
959
|
program.addCommand(createDelegateCommand("stale", "Detect documentation drift in your codebase"));
|
|
616
960
|
program.addCommand(createDelegateCommand("envalid", "Validate .env files against a type-safe schema"));
|
|
617
961
|
program.addCommand(createDelegateCommand("berth", "Detect and resolve port conflicts"));
|
|
@@ -60,14 +60,21 @@ All six tools (including velocity) are available through the unified \`whenlabs\
|
|
|
60
60
|
### Tool Reference
|
|
61
61
|
- \`berth_status\` \u2014 Show all active ports, Docker ports, and configured ports
|
|
62
62
|
- \`berth_check\` \u2014 Scan a project directory for port conflicts
|
|
63
|
+
- \`berth_auto_resolve\` \u2014 Check for conflicts and auto-resolve them in one step
|
|
63
64
|
- \`stale_scan\` \u2014 Detect documentation drift in the codebase
|
|
65
|
+
- \`stale_auto_fix\` \u2014 Scan for drift and auto-fix high-confidence issues in one step
|
|
64
66
|
- \`envalid_validate\` \u2014 Validate .env files against their schema
|
|
65
67
|
- \`envalid_detect\` \u2014 Find undocumented env vars in codebase
|
|
68
|
+
- \`envalid_auto_fix\` \u2014 Detect undocumented env vars and auto-generate schema entries
|
|
66
69
|
- \`aware_init\` \u2014 Auto-detect stack and generate AI context files
|
|
67
70
|
- \`aware_doctor\` \u2014 Diagnose project health and config issues
|
|
71
|
+
- \`aware_auto_sync\` \u2014 Diagnose and auto-sync stale AI context files in one step
|
|
68
72
|
- \`vow_scan\` \u2014 Scan and summarize all dependency licenses
|
|
69
73
|
- \`vow_check\` \u2014 Validate licenses against a policy file
|
|
70
74
|
|
|
75
|
+
### Cross-tool Intelligence
|
|
76
|
+
Tools provide follow-up suggestions when they detect issues relevant to other tools. When you see a "Tip:" in tool output, act on the suggestion \u2014 it means another tool can help.
|
|
77
|
+
|
|
71
78
|
### Proactive Background Scans
|
|
72
79
|
WhenLabs tools run automatically in the background on a schedule. The status line shows findings:
|
|
73
80
|
- \`stale:N\` \u2014 N docs have drifted from code. Run \`stale_scan\` and fix the drift.
|
package/dist/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
findBin
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JOMP6AU5.js";
|
|
5
5
|
|
|
6
6
|
// src/mcp/index.ts
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -25,7 +25,7 @@ import { z } from "zod";
|
|
|
25
25
|
// src/mcp/run-cli.ts
|
|
26
26
|
import { spawn } from "child_process";
|
|
27
27
|
import { join } from "path";
|
|
28
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
28
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
|
|
29
29
|
import { homedir } from "os";
|
|
30
30
|
function runCli(bin, args, cwd) {
|
|
31
31
|
return new Promise((res) => {
|
|
@@ -100,6 +100,39 @@ ${staleOutput}`);
|
|
|
100
100
|
extras.push(`
|
|
101
101
|
Note: Conflicts found in project "${projectName}".`);
|
|
102
102
|
}
|
|
103
|
+
try {
|
|
104
|
+
const cacheFiles = readdirSync(CACHE_DIR).filter((f) => f.startsWith("stale_"));
|
|
105
|
+
for (const cacheFile of cacheFiles) {
|
|
106
|
+
const cached = JSON.parse(readFileSync(join(CACHE_DIR, cacheFile), "utf8"));
|
|
107
|
+
if (/\b\d{4,5}\b/.test(cached.output || "")) {
|
|
108
|
+
extras.push("\nTip: Port references found in documentation \u2014 stale_scan may need re-run after resolving conflicts.");
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (toolName === "envalid_detect") {
|
|
117
|
+
const serviceUrlMatches = output.match(/\b[A-Z_]*(?:HOST|PORT|URL|URI)[A-Z_]*\b/g);
|
|
118
|
+
if (serviceUrlMatches && serviceUrlMatches.length > 0) {
|
|
119
|
+
const examples = [...new Set(serviceUrlMatches)].slice(0, 3).join(", ");
|
|
120
|
+
extras.push(`
|
|
121
|
+
Tip: Service URLs detected (${examples}, etc.) \u2014 run berth_register to track their ports for conflict detection.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (toolName === "velocity_end_task") {
|
|
125
|
+
const largeChange = /actual_files["\s:]+([1-9]\d)/i.test(output) || /\b([6-9]|\d{2,})\s+files?\b/i.test(output);
|
|
126
|
+
if (largeChange) {
|
|
127
|
+
extras.push("\nTip: Large change detected \u2014 consider running stale_scan to check for documentation drift.");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (toolName === "vow_scan") {
|
|
131
|
+
const cacheFile = join(CACHE_DIR, `vow_scan_${deriveProject(path)}.json`);
|
|
132
|
+
const isFirstScan = !existsSync(cacheFile);
|
|
133
|
+
const hasNewPackages = /new package|added|installed/i.test(output);
|
|
134
|
+
if (isFirstScan || hasNewPackages) {
|
|
135
|
+
extras.push("\nTip: Dependency changes detected \u2014 run aware_sync to update AI context files with new library info.");
|
|
103
136
|
}
|
|
104
137
|
}
|
|
105
138
|
return extras;
|
|
@@ -157,6 +190,32 @@ function registerStaleTools(server2) {
|
|
|
157
190
|
return { content: [{ type: "text", text: output }] };
|
|
158
191
|
}
|
|
159
192
|
);
|
|
193
|
+
server2.tool(
|
|
194
|
+
"stale_auto_fix",
|
|
195
|
+
"Scan for documentation drift and auto-fix high-confidence issues in one step",
|
|
196
|
+
{
|
|
197
|
+
path: z.string().optional().describe("Project directory to scan (defaults to cwd)"),
|
|
198
|
+
deep: z.coerce.boolean().optional().describe("Enable AI-powered deep analysis")
|
|
199
|
+
},
|
|
200
|
+
async ({ path, deep }) => {
|
|
201
|
+
const scanArgs = ["scan"];
|
|
202
|
+
if (deep) scanArgs.push("--deep");
|
|
203
|
+
const scanResult = await runCli("stale", scanArgs, path);
|
|
204
|
+
const scanOutput = formatOutput(scanResult);
|
|
205
|
+
writeCache("stale_scan", deriveProject(path), scanOutput, scanResult.code);
|
|
206
|
+
writeCache("stale_auto_fix", deriveProject(path), scanOutput, scanResult.code);
|
|
207
|
+
if (scanResult.code !== 0) {
|
|
208
|
+
const fixResult = await runCli("stale", ["fix", "--apply"], path);
|
|
209
|
+
const fixOutput = formatOutput(fixResult);
|
|
210
|
+
const combined = `${scanOutput}
|
|
211
|
+
--- Auto-fix applied ---
|
|
212
|
+
${fixOutput}`;
|
|
213
|
+
writeCache("stale_auto_fix", deriveProject(path), combined, fixResult.code);
|
|
214
|
+
return { content: [{ type: "text", text: combined }] };
|
|
215
|
+
}
|
|
216
|
+
return { content: [{ type: "text", text: scanOutput }] };
|
|
217
|
+
}
|
|
218
|
+
);
|
|
160
219
|
}
|
|
161
220
|
|
|
162
221
|
// src/mcp/envalid.ts
|
|
@@ -303,6 +362,28 @@ function registerEnvalidTools(server2) {
|
|
|
303
362
|
return { content: [{ type: "text", text: output }] };
|
|
304
363
|
}
|
|
305
364
|
);
|
|
365
|
+
server2.tool(
|
|
366
|
+
"envalid_auto_fix",
|
|
367
|
+
"Detect undocumented env vars and auto-generate schema entries",
|
|
368
|
+
{
|
|
369
|
+
path: z2.string().optional().describe("Project directory (defaults to cwd)")
|
|
370
|
+
},
|
|
371
|
+
async ({ path }) => {
|
|
372
|
+
const detectResult = await runCli("envalid", ["detect"], path);
|
|
373
|
+
const detectOutput = formatOutput(detectResult);
|
|
374
|
+
writeCache("envalid_detect", deriveProject(path), detectOutput, detectResult.code);
|
|
375
|
+
const hasUndocumented = /undocumented|missing from schema/i.test(detectOutput);
|
|
376
|
+
if (hasUndocumented) {
|
|
377
|
+
const generateResult = await runCli("envalid", ["detect", "--generate"], path);
|
|
378
|
+
const generateOutput = formatOutput(generateResult);
|
|
379
|
+
const combined = `${detectOutput}
|
|
380
|
+
--- Auto-generated schema entries ---
|
|
381
|
+
${generateOutput}`;
|
|
382
|
+
return { content: [{ type: "text", text: combined }] };
|
|
383
|
+
}
|
|
384
|
+
return { content: [{ type: "text", text: detectOutput }] };
|
|
385
|
+
}
|
|
386
|
+
);
|
|
306
387
|
}
|
|
307
388
|
|
|
308
389
|
// src/mcp/berth.ts
|
|
@@ -443,6 +524,30 @@ function registerBerthTools(server2) {
|
|
|
443
524
|
return { content: [{ type: "text", text: output }] };
|
|
444
525
|
}
|
|
445
526
|
);
|
|
527
|
+
server2.tool(
|
|
528
|
+
"berth_auto_resolve",
|
|
529
|
+
"Check for port conflicts and auto-resolve them",
|
|
530
|
+
{
|
|
531
|
+
path: z3.string().optional().describe("Project directory (defaults to cwd)"),
|
|
532
|
+
strategy: z3.enum(["kill", "reassign", "auto"]).optional().describe("Resolution strategy (default: auto)")
|
|
533
|
+
},
|
|
534
|
+
async ({ path, strategy }) => {
|
|
535
|
+
const checkResult = await runCli("berth", ["check", path || "."]);
|
|
536
|
+
const checkOutput = formatOutput(checkResult);
|
|
537
|
+
writeCache("berth_check", deriveProject(path), checkOutput, checkResult.code);
|
|
538
|
+
const hasConflicts = /conflict/i.test(checkOutput);
|
|
539
|
+
if (hasConflicts) {
|
|
540
|
+
const resolveArgs = ["resolve", "--strategy", strategy || "auto", "--kill"];
|
|
541
|
+
const resolveResult = await runCli("berth", resolveArgs, path);
|
|
542
|
+
const resolveOutput = formatOutput(resolveResult);
|
|
543
|
+
const combined = `${checkOutput}
|
|
544
|
+
--- Auto-resolve applied ---
|
|
545
|
+
${resolveOutput}`;
|
|
546
|
+
return { content: [{ type: "text", text: combined }] };
|
|
547
|
+
}
|
|
548
|
+
return { content: [{ type: "text", text: checkOutput }] };
|
|
549
|
+
}
|
|
550
|
+
);
|
|
446
551
|
}
|
|
447
552
|
|
|
448
553
|
// src/mcp/aware.ts
|
|
@@ -534,6 +639,28 @@ function registerAwareTools(server2) {
|
|
|
534
639
|
return { content: [{ type: "text", text: output }] };
|
|
535
640
|
}
|
|
536
641
|
);
|
|
642
|
+
server2.tool(
|
|
643
|
+
"aware_auto_sync",
|
|
644
|
+
"Diagnose project health and auto-sync stale AI context files",
|
|
645
|
+
{
|
|
646
|
+
path: z4.string().optional().describe("Project directory (defaults to cwd)")
|
|
647
|
+
},
|
|
648
|
+
async ({ path }) => {
|
|
649
|
+
const doctorResult = await runCli("aware", ["doctor"], path);
|
|
650
|
+
const doctorOutput = formatOutput(doctorResult);
|
|
651
|
+
writeCache("aware_doctor", deriveProject(path), doctorOutput, doctorResult.code);
|
|
652
|
+
const needsSync = /stale|outdated|drift/i.test(doctorOutput);
|
|
653
|
+
if (needsSync) {
|
|
654
|
+
const syncResult = await runCli("aware", ["sync"], path);
|
|
655
|
+
const syncOutput = formatOutput(syncResult);
|
|
656
|
+
const combined = `${doctorOutput}
|
|
657
|
+
--- Auto-sync applied ---
|
|
658
|
+
${syncOutput}`;
|
|
659
|
+
return { content: [{ type: "text", text: combined }] };
|
|
660
|
+
}
|
|
661
|
+
return { content: [{ type: "text", text: doctorOutput }] };
|
|
662
|
+
}
|
|
663
|
+
);
|
|
537
664
|
}
|
|
538
665
|
|
|
539
666
|
// src/mcp/vow.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whenlabs/when",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The WhenLabs developer toolkit — 6 tools, one install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"@whenlabs/velocity-mcp": "^0.1.3",
|
|
37
37
|
"@whenlabs/vow": "^0.1.4",
|
|
38
38
|
"commander": "^12.0.0",
|
|
39
|
+
"yaml": "^2.8.3",
|
|
39
40
|
"zod": "^4.3.6"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
package/dist/chunk-2A2EZZF4.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/utils/find-bin.ts
|
|
4
|
-
import { resolve, dirname } from "path";
|
|
5
|
-
import { existsSync } from "fs";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
function findBin(name) {
|
|
9
|
-
const pkgRoot = resolve(__dirname, "../..");
|
|
10
|
-
const localBin = resolve(pkgRoot, "node_modules", ".bin", name);
|
|
11
|
-
if (existsSync(localBin)) return localBin;
|
|
12
|
-
const directCli = resolve(pkgRoot, "node_modules", "@whenlabs", name, "dist", "cli.js");
|
|
13
|
-
if (existsSync(directCli)) return directCli;
|
|
14
|
-
return name;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
findBin
|
|
19
|
-
};
|