@lumenflow/cli 2.8.0 → 2.9.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 +3 -2
- package/dist/__tests__/commands.test.js +75 -0
- package/dist/__tests__/doctor.test.js +510 -0
- package/dist/__tests__/init.test.js +222 -0
- package/dist/commands.js +171 -0
- package/dist/doctor.js +479 -8
- package/dist/init.js +248 -6
- package/package.json +7 -6
package/dist/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @file doctor.ts
|
|
3
3
|
* LumenFlow health check command (WU-1177)
|
|
4
4
|
* WU-1191: Lane health check integration
|
|
5
|
+
* WU-1386: Agent-friction checks (managed-file dirty, WU validity, worktree sanity)
|
|
5
6
|
* Verifies all safety components are installed and configured correctly
|
|
6
7
|
*/
|
|
7
8
|
import * as fs from 'node:fs';
|
|
@@ -9,8 +10,20 @@ import * as path from 'node:path';
|
|
|
9
10
|
import { execFileSync } from 'node:child_process';
|
|
10
11
|
import { createWUParser } from '@lumenflow/core';
|
|
11
12
|
import { loadLaneDefinitions, detectLaneOverlaps } from './lane-health.js';
|
|
13
|
+
/**
|
|
14
|
+
* WU-1386: Managed files that should not have uncommitted changes
|
|
15
|
+
*/
|
|
16
|
+
const MANAGED_FILE_PATTERNS = [
|
|
17
|
+
'.lumenflow.config.yaml',
|
|
18
|
+
'.lumenflow.lane-inference.yaml',
|
|
19
|
+
'AGENTS.md',
|
|
20
|
+
'CLAUDE.md',
|
|
21
|
+
];
|
|
22
|
+
/** WU-1386: Managed directories with glob patterns */
|
|
23
|
+
const MANAGED_DIR_PATTERNS = ['docs/04-operations/tasks/'];
|
|
12
24
|
/**
|
|
13
25
|
* CLI option definitions for doctor command
|
|
26
|
+
* WU-1386: Added --deep flag
|
|
14
27
|
*/
|
|
15
28
|
const DOCTOR_OPTIONS = {
|
|
16
29
|
verbose: {
|
|
@@ -23,9 +36,15 @@ const DOCTOR_OPTIONS = {
|
|
|
23
36
|
flags: '--json',
|
|
24
37
|
description: 'Output results as JSON',
|
|
25
38
|
},
|
|
39
|
+
deep: {
|
|
40
|
+
name: 'deep',
|
|
41
|
+
flags: '--deep',
|
|
42
|
+
description: 'Run heavier checks including WU validation (slower)',
|
|
43
|
+
},
|
|
26
44
|
};
|
|
27
45
|
/**
|
|
28
46
|
* Parse doctor command options
|
|
47
|
+
* WU-1386: Added deep flag
|
|
29
48
|
*/
|
|
30
49
|
export function parseDoctorOptions() {
|
|
31
50
|
const opts = createWUParser({
|
|
@@ -36,6 +55,7 @@ export function parseDoctorOptions() {
|
|
|
36
55
|
return {
|
|
37
56
|
verbose: opts.verbose ?? false,
|
|
38
57
|
json: opts.json ?? false,
|
|
58
|
+
deep: opts.deep ?? false,
|
|
39
59
|
};
|
|
40
60
|
}
|
|
41
61
|
/**
|
|
@@ -252,10 +272,314 @@ function checkPrerequisites() {
|
|
|
252
272
|
},
|
|
253
273
|
};
|
|
254
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* WU-1387: Get git repository root directory
|
|
277
|
+
* This ensures managed-file detection works from subdirectories
|
|
278
|
+
*/
|
|
279
|
+
function getGitRepoRoot(projectDir) {
|
|
280
|
+
try {
|
|
281
|
+
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
282
|
+
cwd: projectDir,
|
|
283
|
+
encoding: 'utf-8',
|
|
284
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
285
|
+
}).trim();
|
|
286
|
+
return repoRoot;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* WU-1386: Check for uncommitted changes to managed files
|
|
294
|
+
* WU-1387: Uses git repo root for path resolution (works from subdirectories)
|
|
295
|
+
*/
|
|
296
|
+
async function checkManagedFilesDirty(projectDir) {
|
|
297
|
+
try {
|
|
298
|
+
// WU-1387: Get git repo root to handle subdirectory execution
|
|
299
|
+
const repoRoot = getGitRepoRoot(projectDir);
|
|
300
|
+
if (!repoRoot) {
|
|
301
|
+
return {
|
|
302
|
+
passed: true,
|
|
303
|
+
files: [],
|
|
304
|
+
message: 'Git status check skipped (not a git repository)',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Run git status from repo root, not the passed projectDir
|
|
308
|
+
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
|
309
|
+
cwd: repoRoot,
|
|
310
|
+
encoding: 'utf-8',
|
|
311
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
312
|
+
});
|
|
313
|
+
const dirtyFiles = [];
|
|
314
|
+
// Parse status output and check each line
|
|
315
|
+
const lines = statusOutput.split('\n').filter((line) => line.trim());
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
// Status format: XY filename (where XY is 2-char status)
|
|
318
|
+
const filePath = line.slice(3).trim();
|
|
319
|
+
if (!filePath)
|
|
320
|
+
continue;
|
|
321
|
+
// Check if file matches managed patterns
|
|
322
|
+
const isManaged = MANAGED_FILE_PATTERNS.some((pattern) => filePath === pattern) ||
|
|
323
|
+
MANAGED_DIR_PATTERNS.some((dir) => filePath.startsWith(dir));
|
|
324
|
+
if (isManaged) {
|
|
325
|
+
dirtyFiles.push(filePath);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (dirtyFiles.length > 0) {
|
|
329
|
+
return {
|
|
330
|
+
passed: false,
|
|
331
|
+
files: dirtyFiles,
|
|
332
|
+
message: `${dirtyFiles.length} managed file(s) have uncommitted changes`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
passed: true,
|
|
337
|
+
files: [],
|
|
338
|
+
message: 'No uncommitted changes to managed files',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Not a git repo or git not available - graceful degradation
|
|
343
|
+
return {
|
|
344
|
+
passed: true,
|
|
345
|
+
files: [],
|
|
346
|
+
message: 'Git status check skipped (not a git repository)',
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function parseWorktreePruneOutput(output) {
|
|
351
|
+
// Parse summary line for orphan directories: "Orphan directories: N"
|
|
352
|
+
const orphanSummaryMatch = output.match(/Orphan directories:\s*(\d+)/);
|
|
353
|
+
const orphansFromSummary = orphanSummaryMatch ? parseInt(orphanSummaryMatch[1], 10) : 0;
|
|
354
|
+
// Also count "orphan director" mentions (the detailed output)
|
|
355
|
+
const orphanDetailMatches = output.match(/orphan director/gi) || [];
|
|
356
|
+
const orphansFromDetail = orphanDetailMatches.length;
|
|
357
|
+
// Use the higher count (summary is authoritative, but detail catches other mentions)
|
|
358
|
+
const orphans = Math.max(orphansFromSummary, orphansFromDetail > 0 ? 1 : 0);
|
|
359
|
+
// Parse specific worktree warnings from wu:prune output
|
|
360
|
+
// These appear as "Stale worktree:", "Blocked worktree:", "Unclaimed worktree:"
|
|
361
|
+
const staleCount = (output.match(/Stale worktree:/gi) || []).length;
|
|
362
|
+
const blockedCount = (output.match(/Blocked worktree:/gi) || []).length;
|
|
363
|
+
const unclaimedCount = (output.match(/Unclaimed worktree:/gi) || []).length;
|
|
364
|
+
// Parse missing tracked worktrees: "Missing tracked worktree" header
|
|
365
|
+
const missingMatch = output.match(/Missing tracked worktree/gi) || [];
|
|
366
|
+
const missingCount = missingMatch.length > 0 ? 1 : 0; // At least one if header present
|
|
367
|
+
// Also count specific missing path lines
|
|
368
|
+
const missingPathMatches = output.match(/worktrees \(tracked by git but directory missing\)/gi);
|
|
369
|
+
const missingFromDetail = missingPathMatches ? missingPathMatches.length : 0;
|
|
370
|
+
// Parse summary warnings and errors
|
|
371
|
+
const warningSummaryMatch = output.match(/Warnings:\s*(\d+)/);
|
|
372
|
+
const warnings = warningSummaryMatch ? parseInt(warningSummaryMatch[1], 10) : 0;
|
|
373
|
+
const errorSummaryMatch = output.match(/Errors:\s*(\d+)/);
|
|
374
|
+
const errors = errorSummaryMatch ? parseInt(errorSummaryMatch[1], 10) : 0;
|
|
375
|
+
return {
|
|
376
|
+
orphans,
|
|
377
|
+
stale: staleCount,
|
|
378
|
+
blocked: blockedCount,
|
|
379
|
+
unclaimed: unclaimedCount,
|
|
380
|
+
missing: Math.max(missingCount, missingFromDetail),
|
|
381
|
+
warnings,
|
|
382
|
+
errors,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* WU-1386: Check worktree sanity by calling wu:prune in dry-run mode
|
|
387
|
+
* WU-1387: Enhanced parsing for orphan, missing, stale, blocked, and unclaimed worktrees
|
|
388
|
+
*/
|
|
389
|
+
async function checkWorktreeSanity(projectDir) {
|
|
390
|
+
try {
|
|
391
|
+
// First check if this is a git repo
|
|
392
|
+
execFileSync('git', ['rev-parse', '--git-dir'], {
|
|
393
|
+
cwd: projectDir,
|
|
394
|
+
encoding: 'utf-8',
|
|
395
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
396
|
+
});
|
|
397
|
+
// Call wu:prune in dry-run mode (default) via pnpm
|
|
398
|
+
// Capture both stdout and stderr to parse the output
|
|
399
|
+
let pruneOutput;
|
|
400
|
+
let commandFailed = false;
|
|
401
|
+
try {
|
|
402
|
+
pruneOutput = execFileSync('pnpm', ['wu:prune'], {
|
|
403
|
+
cwd: projectDir,
|
|
404
|
+
encoding: 'utf-8',
|
|
405
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
406
|
+
timeout: 30000, // 30 second timeout
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
const execError = e;
|
|
411
|
+
pruneOutput = (execError.stdout || '') + (execError.stderr || '');
|
|
412
|
+
// Check if this is a "command not found" type error vs wu:prune finding issues
|
|
413
|
+
// If pnpm can't run the script (no package.json, script not defined, etc.), gracefully skip
|
|
414
|
+
const errorMsg = execError.message || pruneOutput || '';
|
|
415
|
+
if (errorMsg.includes('ERR_PNPM') ||
|
|
416
|
+
errorMsg.includes('ENOENT') ||
|
|
417
|
+
errorMsg.includes('Missing script') ||
|
|
418
|
+
errorMsg.includes('command not found') ||
|
|
419
|
+
!pruneOutput.includes('[wu-prune]')) {
|
|
420
|
+
// Command couldn't run - gracefully skip
|
|
421
|
+
return {
|
|
422
|
+
passed: true,
|
|
423
|
+
orphans: 0,
|
|
424
|
+
stale: 0,
|
|
425
|
+
message: 'Worktree check skipped (wu:prune not available)',
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// Otherwise, wu:prune ran but found issues (non-zero exit)
|
|
429
|
+
commandFailed = true;
|
|
430
|
+
}
|
|
431
|
+
// WU-1387: Enhanced parsing for all worktree issue types
|
|
432
|
+
const parsed = parseWorktreePruneOutput(pruneOutput);
|
|
433
|
+
// Calculate total issues (orphans count as issues, plus stale, blocked, unclaimed, missing)
|
|
434
|
+
const totalIssues = parsed.orphans + parsed.stale + parsed.blocked + parsed.unclaimed + parsed.missing;
|
|
435
|
+
const hasIssues = totalIssues > 0 || parsed.warnings > 0 || parsed.errors > 0;
|
|
436
|
+
const passed = !hasIssues && !commandFailed;
|
|
437
|
+
// Build descriptive message
|
|
438
|
+
let message = 'All worktrees are valid';
|
|
439
|
+
if (!passed) {
|
|
440
|
+
const parts = [];
|
|
441
|
+
if (parsed.orphans > 0)
|
|
442
|
+
parts.push(`${parsed.orphans} orphan(s)`);
|
|
443
|
+
if (parsed.missing > 0)
|
|
444
|
+
parts.push(`${parsed.missing} missing`);
|
|
445
|
+
if (parsed.stale > 0)
|
|
446
|
+
parts.push(`${parsed.stale} stale`);
|
|
447
|
+
if (parsed.blocked > 0)
|
|
448
|
+
parts.push(`${parsed.blocked} blocked`);
|
|
449
|
+
if (parsed.unclaimed > 0)
|
|
450
|
+
parts.push(`${parsed.unclaimed} unclaimed`);
|
|
451
|
+
if (parts.length === 0 && (parsed.warnings > 0 || parsed.errors > 0)) {
|
|
452
|
+
parts.push(`${parsed.warnings} warning(s), ${parsed.errors} error(s)`);
|
|
453
|
+
}
|
|
454
|
+
message = parts.length > 0 ? `Worktree issues: ${parts.join(', ')}` : 'Worktree issues found';
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
passed,
|
|
458
|
+
orphans: parsed.orphans,
|
|
459
|
+
stale: parsed.stale + parsed.blocked + parsed.unclaimed + parsed.missing, // Combined for API compat
|
|
460
|
+
message,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// Not a git repo or git not available
|
|
465
|
+
return {
|
|
466
|
+
passed: true,
|
|
467
|
+
orphans: 0,
|
|
468
|
+
stale: 0,
|
|
469
|
+
message: 'Worktree check skipped (not a git repository)',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* WU-1386: Run WU validation (--deep mode only)
|
|
475
|
+
* WU-1387: Sets passed=false with clear message when CLI fails to run
|
|
476
|
+
* Calls wu:validate --all --no-strict for full schema/lint validation in warn-only mode
|
|
477
|
+
*/
|
|
478
|
+
async function checkWUValidity(projectDir) {
|
|
479
|
+
const wuDir = path.join(projectDir, 'docs', '04-operations', 'tasks', 'wu');
|
|
480
|
+
if (!fs.existsSync(wuDir)) {
|
|
481
|
+
return {
|
|
482
|
+
passed: true,
|
|
483
|
+
total: 0,
|
|
484
|
+
valid: 0,
|
|
485
|
+
invalid: 0,
|
|
486
|
+
warnings: 0,
|
|
487
|
+
message: 'No WU directory found',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
// Call wu:validate --all --no-strict to get warn-only validation
|
|
492
|
+
// This runs the full schema + lint validation, treating warnings as advisory
|
|
493
|
+
let validateOutput;
|
|
494
|
+
let cliError = false;
|
|
495
|
+
let cliErrorMessage = '';
|
|
496
|
+
try {
|
|
497
|
+
validateOutput = execFileSync('pnpm', ['wu:validate', '--all', '--no-strict'], {
|
|
498
|
+
cwd: projectDir,
|
|
499
|
+
encoding: 'utf-8',
|
|
500
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
501
|
+
timeout: 60000, // 60 second timeout for full validation
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
// wu:validate exits non-zero if validation fails
|
|
506
|
+
const execError = e;
|
|
507
|
+
validateOutput = (execError.stdout || '') + (execError.stderr || '');
|
|
508
|
+
const errorMsg = execError.message || '';
|
|
509
|
+
// WU-1387: Distinguish between CLI execution failure and validation failure
|
|
510
|
+
// CLI failure = script missing, command not found, ENOENT
|
|
511
|
+
// Validation failure = script ran but found invalid WUs
|
|
512
|
+
if (errorMsg.includes('ERR_PNPM') ||
|
|
513
|
+
errorMsg.includes('ENOENT') ||
|
|
514
|
+
errorMsg.includes('Missing script') ||
|
|
515
|
+
errorMsg.includes('command not found') ||
|
|
516
|
+
execError.code === 'ENOENT' ||
|
|
517
|
+
// If no recognizable wu:validate output, assume CLI failed
|
|
518
|
+
(!validateOutput.includes('[wu:validate]') && !validateOutput.includes('Valid:'))) {
|
|
519
|
+
cliError = true;
|
|
520
|
+
cliErrorMessage = errorMsg.includes('Missing script')
|
|
521
|
+
? 'wu:validate script not found'
|
|
522
|
+
: errorMsg.includes('ENOENT')
|
|
523
|
+
? 'pnpm command not available'
|
|
524
|
+
: 'wu:validate could not run';
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// WU-1387: If CLI failed to run, report as failure with clear message
|
|
528
|
+
if (cliError) {
|
|
529
|
+
return {
|
|
530
|
+
passed: false,
|
|
531
|
+
total: 0,
|
|
532
|
+
valid: 0,
|
|
533
|
+
invalid: 0,
|
|
534
|
+
warnings: 0,
|
|
535
|
+
message: `WU validation failed: ${cliErrorMessage}`,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
// Parse output for counts
|
|
539
|
+
// wu:validate outputs summary like:
|
|
540
|
+
// ✓ Valid: N
|
|
541
|
+
// ✗ Invalid: N
|
|
542
|
+
// ⚠ Warnings: N
|
|
543
|
+
const validMatch = validateOutput.match(/Valid:\s*(\d+)/);
|
|
544
|
+
const invalidMatch = validateOutput.match(/Invalid:\s*(\d+)/);
|
|
545
|
+
const warningMatch = validateOutput.match(/Warnings:\s*(\d+)/);
|
|
546
|
+
const validCount = validMatch ? parseInt(validMatch[1], 10) : 0;
|
|
547
|
+
const invalidCount = invalidMatch ? parseInt(invalidMatch[1], 10) : 0;
|
|
548
|
+
const warningCount = warningMatch ? parseInt(warningMatch[1], 10) : 0;
|
|
549
|
+
const total = validCount + invalidCount;
|
|
550
|
+
// In warn-only mode (--no-strict), we only fail on actual invalid WUs
|
|
551
|
+
const passed = invalidCount === 0;
|
|
552
|
+
return {
|
|
553
|
+
passed,
|
|
554
|
+
total,
|
|
555
|
+
valid: validCount,
|
|
556
|
+
invalid: invalidCount,
|
|
557
|
+
warnings: warningCount,
|
|
558
|
+
message: passed
|
|
559
|
+
? total > 0
|
|
560
|
+
? `All ${total} WU(s) valid${warningCount > 0 ? ` (${warningCount} warning(s))` : ''}`
|
|
561
|
+
: 'No WUs to validate'
|
|
562
|
+
: `${invalidCount}/${total} WU(s) have issues`,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
// WU-1387: Unexpected errors should report failure, not silently pass
|
|
567
|
+
const error = e;
|
|
568
|
+
return {
|
|
569
|
+
passed: false,
|
|
570
|
+
total: 0,
|
|
571
|
+
valid: 0,
|
|
572
|
+
invalid: 0,
|
|
573
|
+
warnings: 0,
|
|
574
|
+
message: `WU validation failed: ${error.message || 'unknown error'}`,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
255
578
|
/**
|
|
256
579
|
* Run all doctor checks
|
|
580
|
+
* WU-1386: Added options parameter for --deep flag
|
|
257
581
|
*/
|
|
258
|
-
export async function runDoctor(projectDir) {
|
|
582
|
+
export async function runDoctor(projectDir, options = {}) {
|
|
259
583
|
const checks = {
|
|
260
584
|
husky: checkHusky(projectDir),
|
|
261
585
|
safeGit: checkSafeGit(projectDir),
|
|
@@ -266,19 +590,137 @@ export async function runDoctor(projectDir) {
|
|
|
266
590
|
};
|
|
267
591
|
const vendorConfigs = checkVendorConfigs(projectDir);
|
|
268
592
|
const prerequisites = checkPrerequisites();
|
|
593
|
+
// WU-1386: Workflow health checks
|
|
594
|
+
const managedFilesDirty = await checkManagedFilesDirty(projectDir);
|
|
595
|
+
const worktreeSanity = await checkWorktreeSanity(projectDir);
|
|
596
|
+
const workflowHealth = {
|
|
597
|
+
managedFilesDirty,
|
|
598
|
+
worktreeSanity,
|
|
599
|
+
};
|
|
600
|
+
// WU-1386: WU validity check only in --deep mode
|
|
601
|
+
if (options.deep) {
|
|
602
|
+
workflowHealth.wuValidity = await checkWUValidity(projectDir);
|
|
603
|
+
}
|
|
269
604
|
// Determine overall status
|
|
270
605
|
// Note: laneHealth is advisory (not included in critical checks)
|
|
271
606
|
const criticalChecks = [checks.husky, checks.safeGit, checks.agentsMd];
|
|
272
607
|
const allCriticalPassed = criticalChecks.every((check) => check.passed);
|
|
608
|
+
// WU-1386: Calculate exit code
|
|
609
|
+
// 0 = healthy (all checks pass, no warnings)
|
|
610
|
+
// 1 = warnings (non-critical issues)
|
|
611
|
+
// 2 = errors (critical safety checks failed)
|
|
612
|
+
let exitCode = 0;
|
|
613
|
+
if (!allCriticalPassed) {
|
|
614
|
+
exitCode = 2;
|
|
615
|
+
}
|
|
616
|
+
else if (!managedFilesDirty.passed ||
|
|
617
|
+
!worktreeSanity.passed ||
|
|
618
|
+
(workflowHealth.wuValidity && !workflowHealth.wuValidity.passed)) {
|
|
619
|
+
exitCode = 1;
|
|
620
|
+
}
|
|
273
621
|
return {
|
|
274
622
|
status: allCriticalPassed ? 'ACTIVE' : 'INCOMPLETE',
|
|
623
|
+
exitCode,
|
|
275
624
|
checks,
|
|
276
625
|
vendorConfigs,
|
|
277
626
|
prerequisites,
|
|
627
|
+
workflowHealth,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* WU-1386: Run doctor for init (non-blocking, warnings only)
|
|
632
|
+
* WU-1387: Shows accurate status including lane health and prerequisite failures
|
|
633
|
+
* This is used after lumenflow init to provide feedback without blocking
|
|
634
|
+
*/
|
|
635
|
+
export async function runDoctorForInit(projectDir) {
|
|
636
|
+
const result = await runDoctor(projectDir, { deep: false });
|
|
637
|
+
// Count warnings and errors using check keys, not messages
|
|
638
|
+
let warnings = 0;
|
|
639
|
+
let errors = 0;
|
|
640
|
+
// Critical checks that count as errors (if they fail, safety is compromised)
|
|
641
|
+
const criticalCheckKeys = ['husky', 'safeGit', 'agentsMd'];
|
|
642
|
+
for (const [key, check] of Object.entries(result.checks)) {
|
|
643
|
+
if (!check.passed) {
|
|
644
|
+
if (criticalCheckKeys.includes(key)) {
|
|
645
|
+
errors++;
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
warnings++;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// WU-1387: Count prerequisite failures as warnings
|
|
653
|
+
for (const [, prereq] of Object.entries(result.prerequisites)) {
|
|
654
|
+
if (!prereq.passed) {
|
|
655
|
+
warnings++;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Count workflow health issues as warnings (not errors)
|
|
659
|
+
if (result.workflowHealth) {
|
|
660
|
+
if (!result.workflowHealth.managedFilesDirty.passed)
|
|
661
|
+
warnings++;
|
|
662
|
+
if (!result.workflowHealth.worktreeSanity.passed)
|
|
663
|
+
warnings++;
|
|
664
|
+
}
|
|
665
|
+
// Format concise output
|
|
666
|
+
const lines = [];
|
|
667
|
+
lines.push('[lumenflow doctor] Quick health check...');
|
|
668
|
+
if (result.exitCode === 0 && warnings === 0) {
|
|
669
|
+
lines.push(' All checks passed');
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// Show critical errors first
|
|
673
|
+
if (!result.checks.husky.passed) {
|
|
674
|
+
lines.push(' Error: Husky hooks not installed');
|
|
675
|
+
}
|
|
676
|
+
if (!result.checks.safeGit.passed) {
|
|
677
|
+
lines.push(' Error: safe-git script not found');
|
|
678
|
+
}
|
|
679
|
+
if (!result.checks.agentsMd.passed) {
|
|
680
|
+
lines.push(' Error: AGENTS.md not found');
|
|
681
|
+
}
|
|
682
|
+
// WU-1387: Show lane health issues
|
|
683
|
+
if (!result.checks.laneHealth.passed) {
|
|
684
|
+
lines.push(` Warning: Lane overlap detected - ${result.checks.laneHealth.message}`);
|
|
685
|
+
}
|
|
686
|
+
// WU-1387: Show prerequisite failures
|
|
687
|
+
if (!result.prerequisites.node.passed) {
|
|
688
|
+
lines.push(` Warning: Node.js version ${result.prerequisites.node.version} (required: ${result.prerequisites.node.required})`);
|
|
689
|
+
}
|
|
690
|
+
if (!result.prerequisites.pnpm.passed) {
|
|
691
|
+
lines.push(` Warning: pnpm version ${result.prerequisites.pnpm.version} (required: ${result.prerequisites.pnpm.required})`);
|
|
692
|
+
}
|
|
693
|
+
if (!result.prerequisites.git.passed) {
|
|
694
|
+
lines.push(` Warning: Git version ${result.prerequisites.git.version} (required: ${result.prerequisites.git.required})`);
|
|
695
|
+
}
|
|
696
|
+
// Workflow health warnings
|
|
697
|
+
if (result.workflowHealth?.managedFilesDirty.files.length) {
|
|
698
|
+
lines.push(` Warning: ${result.workflowHealth.managedFilesDirty.files.length} managed file(s) have uncommitted changes`);
|
|
699
|
+
for (const file of result.workflowHealth.managedFilesDirty.files.slice(0, 3)) {
|
|
700
|
+
lines.push(` -> ${file}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (result.workflowHealth?.worktreeSanity.orphans) {
|
|
704
|
+
lines.push(` Warning: ${result.workflowHealth.worktreeSanity.orphans} orphan worktree(s) found`);
|
|
705
|
+
}
|
|
706
|
+
if (result.workflowHealth && !result.workflowHealth.worktreeSanity.passed) {
|
|
707
|
+
// WU-1387: Show worktree sanity message if there are issues beyond orphans
|
|
708
|
+
const wsSanity = result.workflowHealth.worktreeSanity;
|
|
709
|
+
if (wsSanity.stale > 0 && wsSanity.orphans === 0) {
|
|
710
|
+
lines.push(` Warning: ${wsSanity.message}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
blocked: false, // Never blocks init
|
|
716
|
+
warnings,
|
|
717
|
+
errors,
|
|
718
|
+
output: lines.join('\n'),
|
|
278
719
|
};
|
|
279
720
|
}
|
|
280
721
|
/**
|
|
281
722
|
* Format doctor output for terminal display
|
|
723
|
+
* WU-1386: Added workflow health section
|
|
282
724
|
*/
|
|
283
725
|
export function formatDoctorOutput(result) {
|
|
284
726
|
const lines = [];
|
|
@@ -300,6 +742,31 @@ export function formatDoctorOutput(result) {
|
|
|
300
742
|
lines.push('');
|
|
301
743
|
lines.push('Lane Health:');
|
|
302
744
|
lines.push(formatCheck(result.checks.laneHealth));
|
|
745
|
+
// WU-1386: Workflow Health section
|
|
746
|
+
if (result.workflowHealth) {
|
|
747
|
+
lines.push('');
|
|
748
|
+
lines.push('Workflow Health:');
|
|
749
|
+
const mfd = result.workflowHealth.managedFilesDirty;
|
|
750
|
+
const mfdSymbol = mfd.passed ? '✓' : '⚠';
|
|
751
|
+
lines.push(` ${mfdSymbol} ${mfd.message}`);
|
|
752
|
+
if (!mfd.passed && mfd.files.length > 0) {
|
|
753
|
+
for (const file of mfd.files.slice(0, 5)) {
|
|
754
|
+
lines.push(` → ${file}`);
|
|
755
|
+
}
|
|
756
|
+
if (mfd.files.length > 5) {
|
|
757
|
+
lines.push(` → ... and ${mfd.files.length - 5} more`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const ws = result.workflowHealth.worktreeSanity;
|
|
761
|
+
const wsSymbol = ws.passed ? '✓' : '⚠';
|
|
762
|
+
lines.push(` ${wsSymbol} ${ws.message}`);
|
|
763
|
+
// WU validity (only in --deep mode)
|
|
764
|
+
if (result.workflowHealth.wuValidity) {
|
|
765
|
+
const wv = result.workflowHealth.wuValidity;
|
|
766
|
+
const wvSymbol = wv.passed ? '✓' : '⚠';
|
|
767
|
+
lines.push(` ${wvSymbol} ${wv.message}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
303
770
|
// Vendor configs
|
|
304
771
|
lines.push('');
|
|
305
772
|
lines.push('Vendor Configs:');
|
|
@@ -324,7 +791,12 @@ export function formatDoctorOutput(result) {
|
|
|
324
791
|
lines.push('');
|
|
325
792
|
lines.push('─'.repeat(45));
|
|
326
793
|
lines.push(`LumenFlow safety: ${result.status}`);
|
|
327
|
-
|
|
794
|
+
// WU-1386: Show exit code meaning
|
|
795
|
+
if (result.exitCode === 1) {
|
|
796
|
+
lines.push('');
|
|
797
|
+
lines.push('Warnings detected (exit code 1). Use --deep for full WU validation.');
|
|
798
|
+
}
|
|
799
|
+
else if (result.exitCode === 2) {
|
|
328
800
|
lines.push('');
|
|
329
801
|
lines.push('To fix missing components:');
|
|
330
802
|
lines.push(' pnpm install && pnpm prepare # Install Husky hooks');
|
|
@@ -341,26 +813,25 @@ export function formatDoctorJson(result) {
|
|
|
341
813
|
}
|
|
342
814
|
/**
|
|
343
815
|
* CLI entry point
|
|
816
|
+
* WU-1386: Updated to use new exit codes
|
|
344
817
|
*/
|
|
345
818
|
export async function main() {
|
|
346
819
|
const opts = parseDoctorOptions();
|
|
347
820
|
const projectDir = process.cwd();
|
|
348
|
-
const result = await runDoctor(projectDir);
|
|
821
|
+
const result = await runDoctor(projectDir, { deep: opts.deep });
|
|
349
822
|
if (opts.json) {
|
|
350
823
|
console.log(formatDoctorJson(result));
|
|
351
824
|
}
|
|
352
825
|
else {
|
|
353
826
|
console.log(formatDoctorOutput(result));
|
|
354
827
|
}
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
828
|
+
// WU-1386: Use new exit code system
|
|
829
|
+
process.exit(result.exitCode);
|
|
359
830
|
}
|
|
360
831
|
// CLI invocation when run directly
|
|
361
832
|
if (import.meta.main) {
|
|
362
833
|
main().catch((error) => {
|
|
363
834
|
console.error('Doctor failed:', error);
|
|
364
|
-
process.exit(
|
|
835
|
+
process.exit(2);
|
|
365
836
|
});
|
|
366
837
|
}
|