dev-diagnosis-cli 0.1.7 → 0.2.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.
@@ -11,18 +11,28 @@
11
11
  *
12
12
  * Exit codes: 0 = every check that ran passed; 1 = at least one check
13
13
  * found something to fix, or a check could not run; 2 = usage error.
14
+ * With --fix the exit code follows a re-scan after the fix, so a fix
15
+ * that leaves the repo clean exits 0.
16
+ *
17
+ * Agent modes: --mcp serves the checks over MCP on stdio (see
18
+ * lib/mcp.mjs); --skill-generate writes a Claude Agent Skills SKILL.md
19
+ * that tells an agent to run the CLI when it starts work in a repo
20
+ * (see lib/skill.mjs).
14
21
  */
15
22
  import fs from 'node:fs';
16
23
  import path from 'node:path';
17
24
  import { fileURLToPath } from 'node:url';
18
- import { scanDirectory, applyAiignore, formatBytes } from '../lib/context.mjs';
25
+ import { formatBytes } from '../lib/context.mjs';
19
26
  import {
20
- buildContexts, findCommandCopies, fixForDirectory, lookupCommand,
21
- searchToolchainBins,
22
- } from '../lib/pathcheck.mjs';
23
- import { detectCacheDir, runCacheCheck } from '../lib/cache.mjs';
24
- import { formatFlakesTable, scanRuns } from '../lib/flakes.mjs';
25
- import { formatMs, section, verdictLine } from '../lib/report.mjs';
27
+ UsageError, allJson, applyContextFixAndRescan, formatSummary,
28
+ rescanContext, runAll, runCacheCheck, runContextCheck, runFlakesCheck,
29
+ runPathCheck, stripCacheStdout,
30
+ } from '../lib/checks.mjs';
31
+ import { runMcpServer } from '../lib/mcp.mjs';
32
+ import { generateSkill } from '../lib/skill.mjs';
33
+ import { applySetup, checkSetup, planSetup } from '../lib/setup.mjs';
34
+ import { formatFlakesTable } from '../lib/flakes.mjs';
35
+ import { formatMs, formatTokens, section, verdictLine } from '../lib/report.mjs';
26
36
 
27
37
  const VERSION = JSON.parse(
28
38
  fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
@@ -34,8 +44,11 @@ Usage:
34
44
  dev-diagnosis [all] [path] [--command <name>] [--flakes <dir>] [--fix] [-- <build-command...>]
35
45
  dev-diagnosis context [path] [--json] [--fix]
36
46
  dev-diagnosis path <command> [--json]
37
- dev-diagnosis cache [--cache-dir <dir>] [--cwd <dir>] -- <build-command...>
47
+ dev-diagnosis cache [--json] [--cache-dir <dir>] [--cwd <dir>] -- <build-command...>
38
48
  dev-diagnosis flakes <runs-dir> [--json] [--top N]
49
+ dev-diagnosis --mcp [--sandbox <dir>]
50
+ dev-diagnosis --skill-generate [--global | --project]
51
+ dev-diagnosis setup [--dry-run] [--check]
39
52
  dev-diagnosis --help
40
53
  dev-diagnosis --version
41
54
 
@@ -59,33 +72,52 @@ Commands:
59
72
  flakes Rank flaky tests from JUnit XML across CI runs, including
60
73
  passed-on-retry. Each subdirectory of <runs-dir> is one run;
61
74
  each *.xml file directly under it is a run of its own.
75
+ --mcp Serve the checks as an MCP server on stdio, so any
76
+ MCP-compatible agent can call them as tools. See the README
77
+ for the client config. --sandbox <dir> (alias --root)
78
+ restricts every path the tools touch to that directory.
79
+ --skill-generate
80
+ Write a Claude Agent Skills SKILL.md for dev-diagnosis:
81
+ ./.claude/skills/dev-diagnosis/SKILL.md by default
82
+ (--project), or ~/.claude/skills/dev-diagnosis/SKILL.md
83
+ with --global. Refuses to overwrite an existing file.
84
+ setup One-command install: detect the agents on this machine
85
+ (Claude Code, Codex CLI, Cursor, VS Code) and write the
86
+ MCP config that wires dev-diagnosis into each of them.
87
+ Prints a preview of exactly what will be written before
88
+ writing anything; a config that already mentions
89
+ dev-diagnosis is left alone. --dry-run shows the preview
90
+ without writing; --check reports which agents are wired.
62
91
 
63
92
  Options:
64
- --json machine-readable output (context, path, flakes, all)
93
+ --json machine-readable output (context, path, cache, flakes, all)
65
94
  --fix write the context check's .aiignore suggestions into
66
- <path>/.aiignore (context and all only). Entries that
67
- are already there are left alone; nothing else in the
68
- file is changed.
95
+ <path>/.aiignore (context and all only), then re-scan:
96
+ the verdict and the exit code follow the post-fix
97
+ score. Entries that are already there are left alone;
98
+ nothing else in the file is changed.
69
99
  --top N show only the N highest-ranked flaky tests
100
+ --sandbox <dir> restrict --mcp to paths under <dir> (--root is an alias)
101
+ --global with --skill-generate: write the skill to ~/.claude/skills
102
+ --project with --skill-generate: write the skill to ./.claude/skills
103
+ --dry-run with setup: show the preview without writing anything
104
+ --check with setup: report which agents are wired, without writing
70
105
  -h, --help show this help
71
106
  -v, --version print the version
72
107
 
73
108
  Exit codes: 0 = every check that ran passed; 1 = a check found something
74
- to fix or could not run; 2 = usage error.
109
+ to fix or could not run; 2 = usage error. With --fix the exit code
110
+ follows a re-scan after the fix, so a fix that leaves the repo clean
111
+ exits 0.
75
112
  `;
76
113
 
77
- class UsageError extends Error {
78
- constructor(message) {
79
- super(message);
80
- this.name = 'UsageError';
81
- }
82
- }
83
-
84
114
  function parseArgs(argv) {
85
115
  const parsed = {
86
116
  command: null, positional: [], json: false, fix: false, help: false, version: false,
87
117
  commandName: null, flakesDir: null, cacheDir: null, cwd: null, top: null,
88
118
  buildCommand: null, error: null,
119
+ mcp: false, sandbox: null, skillGenerate: false, skillGlobal: false, skillProject: false,
120
+ dryRun: false, check: false,
89
121
  };
90
122
  let i = 0;
91
123
  while (i < argv.length) {
@@ -98,6 +130,12 @@ function parseArgs(argv) {
98
130
  else if (arg === '--fix') parsed.fix = true;
99
131
  else if (arg === '--help' || arg === '-h') parsed.help = true;
100
132
  else if (arg === '--version' || arg === '-v') parsed.version = true;
133
+ else if (arg === '--mcp') parsed.mcp = true;
134
+ else if (arg === '--dry-run') parsed.dryRun = true;
135
+ else if (arg === '--check') parsed.check = true;
136
+ else if (arg === '--skill-generate') parsed.skillGenerate = true;
137
+ else if (arg === '--global') parsed.skillGlobal = true;
138
+ else if (arg === '--project') parsed.skillProject = true;
101
139
  else if (arg === '--command') {
102
140
  parsed.commandName = argv[i + 1];
103
141
  if (parsed.commandName === undefined) parsed.error = '--command needs a value';
@@ -118,6 +156,12 @@ function parseArgs(argv) {
118
156
  if (parsed.cwd === undefined) parsed.error = '--cwd needs a value';
119
157
  i += 1;
120
158
  } else if (arg.startsWith('--cwd=')) parsed.cwd = arg.slice('--cwd='.length);
159
+ else if (arg === '--sandbox' || arg === '--root') {
160
+ parsed.sandbox = argv[i + 1];
161
+ if (parsed.sandbox === undefined) parsed.error = `${arg} needs a value`;
162
+ i += 1;
163
+ } else if (arg.startsWith('--sandbox=')) parsed.sandbox = arg.slice('--sandbox='.length);
164
+ else if (arg.startsWith('--root=')) parsed.sandbox = arg.slice('--root='.length);
121
165
  else if (arg === '--top') {
122
166
  const value = Number(argv[i + 1]);
123
167
  if (!Number.isInteger(value) || value < 1) parsed.error = '--top needs a positive integer';
@@ -153,6 +197,12 @@ function printContext(result, json) {
153
197
  console.log(` ${item.label} — ${item.reason} (${formatBytes(item.bytes)})`);
154
198
  }
155
199
  if (result.noise.length > 20) console.log(` ... and ${result.noise.length - 20} more`);
200
+ // What the noise costs in tokens, at ~4 bytes per token — an
201
+ // estimate, labelled as one. Tokens are what the user pays for, so
202
+ // the cost belongs next to the files, not in a footnote.
203
+ if (result.estimatedTokens != null) {
204
+ console.log(`Estimated tokens wasted on this noise: ≈ ${formatTokens(result.estimatedTokens)} tokens (estimate, ~4 bytes per token)`);
205
+ }
156
206
  }
157
207
  if (result.review.length > 0) {
158
208
  console.log('Worth a look before sharing:');
@@ -160,26 +210,16 @@ function printContext(result, json) {
160
210
  console.log(` ${item.label} — ${item.reason}`);
161
211
  }
162
212
  }
213
+ if (result.reviewSuggestions?.length > 0) {
214
+ console.log('Consider for .aiignore (advisory — --fix does not write these):');
215
+ for (const suggestion of result.reviewSuggestions) console.log(` ${suggestion}`);
216
+ }
163
217
  if (result.suggestions.length > 0) {
164
218
  console.log('Fix: add these to .aiignore (or the agent\'s ignore list):');
165
219
  for (const suggestion of result.suggestions) console.log(` ${suggestion}`);
166
220
  }
167
221
  }
168
222
 
169
- function runContext(target) {
170
- const result = scanDirectory(target || process.cwd());
171
- const passed = result.score >= 85;
172
- return { result, passed, verdict: passed ? 'context is clean' : `${result.status.toLowerCase()} (score ${result.score}/100)` };
173
- }
174
-
175
- // Apply the context check's .aiignore suggestions and record the outcome
176
- // on the check result, so --json output carries it too.
177
- function applyContextFix(check) {
178
- const fix = applyAiignore(check.result.root, check.result.suggestions);
179
- check.result.fix = fix;
180
- return fix;
181
- }
182
-
183
223
  function printFixLine(fix, suggestionCount) {
184
224
  if (fix.added.length > 0) {
185
225
  const noun = fix.added.length === 1 ? 'entry' : 'entries';
@@ -215,28 +255,6 @@ function printPath(result, json) {
215
255
  for (const row of unreliable) console.log(`Note: ${row.name} context fell back to the current PATH (${row.note}).`);
216
256
  }
217
257
 
218
- function runPath(commandName) {
219
- if (!commandName) throw new UsageError('path needs a command, for example: dev-diagnosis path node');
220
- const contexts = buildContexts();
221
- const rows = contexts.map((ctx) => {
222
- const found = lookupCommand(commandName, ctx.pathValue);
223
- return { ...ctx, resolved: found.resolved, path: found.path, reason: found.reason || null };
224
- });
225
- const copies = findCommandCopies(commandName, process.env.PATH || '');
226
- const toolchainHits = rows[0].resolved ? [] : searchToolchainBins(commandName);
227
- let fix = null;
228
- if (!rows[0].resolved && toolchainHits.length > 0) fix = fixForDirectory(toolchainHits[0].directory);
229
- else if (!rows[0].resolved && rows.some((row) => row.resolved)) {
230
- fix = 'it resolves in another shell context — check which startup file sets your PATH';
231
- }
232
- const passed = rows[0].resolved;
233
- return {
234
- result: { command: commandName, contexts: rows, copies, toolchainHits, fix },
235
- passed,
236
- verdict: passed ? `${commandName} resolves to ${rows[0].path}` : `${commandName} is missing in the current shell`,
237
- };
238
- }
239
-
240
258
  function printFlakes(result, top, json) {
241
259
  if (json) {
242
260
  // --top limits the listing, not the scan: the tests array holds only
@@ -249,51 +267,9 @@ function printFlakes(result, top, json) {
249
267
  console.log(formatFlakesTable(result, top));
250
268
  }
251
269
 
252
- function runFlakes(dir) {
253
- if (!dir) throw new UsageError('flakes needs a runs directory, for example: dev-diagnosis flakes examples/runs');
254
- const result = scanRuns(dir);
255
- const unparsable = result.warnings.length;
256
- const skippedRuns = result.skippedRuns ? result.skippedRuns.length : 0;
257
- // A file that could not be parsed, or a run directory that produced no
258
- // reports, means the verdict is based on incomplete data: "no flaky
259
- // tests" on a half-read run set is not a pass, so the check fails and
260
- // the verdict says what was unreadable. The warnings themselves are
261
- // printed with the table.
262
- const passed = result.totals.flaky === 0 && result.totals.alwaysFailing === 0 && unparsable === 0 && skippedRuns === 0;
263
- const base = result.totals.flaky === 0 && result.totals.alwaysFailing === 0
264
- ? `no flaky tests across ${result.totals.runs} runs`
265
- : `${result.totals.flaky} flaky, ${result.totals.alwaysFailing} always-failing across ${result.totals.runs} runs`;
266
- const problems = [];
267
- if (unparsable > 0) problems.push(`${unparsable} file${unparsable === 1 ? '' : 's'} could not be parsed`);
268
- if (skippedRuns > 0) problems.push(`${skippedRuns} run director${skippedRuns === 1 ? 'y' : 'ies'} had no JUnit XML files`);
269
- return {
270
- result,
271
- passed,
272
- verdict: problems.length > 0 ? `${base}; ${problems.join('; ')}` : base,
273
- };
274
- }
275
-
276
- function runCache(options) {
277
- if (!options.buildCommand || options.buildCommand.length === 0) {
278
- throw new UsageError('cache needs a build command after --, for example: dev-diagnosis cache -- node build.mjs');
279
- }
280
- const result = runCacheCheck({
281
- root: options.cwd || process.cwd(),
282
- cacheDir: options.cacheDir,
283
- command: options.buildCommand,
284
- });
285
- const passed = result.verdict !== 'cache costs';
286
- return {
287
- result,
288
- passed,
289
- verdict: `${result.verdict}: cold ${formatMs(result.coldMs)}, warm ${formatMs(result.warmMs)} (${result.verdictDetail})`,
290
- };
291
- }
292
-
293
270
  function printCache(result, json) {
294
271
  if (json) {
295
- const { coldStdout, warmStdout, ...rest } = result;
296
- console.log(JSON.stringify(rest, null, 2));
272
+ console.log(JSON.stringify(stripCacheStdout(result), null, 2));
297
273
  return;
298
274
  }
299
275
  console.log(`Cache dir: ${result.cacheDir}`);
@@ -302,7 +278,57 @@ function printCache(result, json) {
302
278
  console.log(`Verdict: ${result.verdict} — ${result.verdictDetail}`);
303
279
  }
304
280
 
305
- function main(argv) {
281
+ function printSetupPreview(plan) {
282
+ for (const row of plan.agents) {
283
+ console.log(`${row.name} (${row.configPath})`);
284
+ if (row.status === 'already-wired') {
285
+ console.log(' already wired — dev-diagnosis is already in this config');
286
+ } else if (row.status === 'unreadable') {
287
+ console.log(` skipped — ${row.note}`);
288
+ } else {
289
+ console.log(` ${row.status} — will write:`);
290
+ for (const line of row.preview.replace(/\n$/, '').split('\n')) console.log(` ${line}`);
291
+ }
292
+ }
293
+ }
294
+
295
+ function printPathWarning(plan) {
296
+ if (!plan.commandOnPath) {
297
+ console.log('Note: `dev-diagnosis` is not on PATH — install it globally (`npm install -g dev-diagnosis-cli`) or your agents will not be able to run it.');
298
+ }
299
+ }
300
+
301
+ function runSetup(args) {
302
+ if (args.check) {
303
+ const result = checkSetup();
304
+ if (result.detected === 0) {
305
+ console.log('No supported agent found (looked for Claude Code, Codex CLI, Cursor, VS Code).');
306
+ return 1;
307
+ }
308
+ for (const row of result.rows) {
309
+ const state = row.wired ? 'wired' : `not wired (${row.status})`;
310
+ console.log(`${row.name}: ${state} — ${row.configPath}`);
311
+ }
312
+ return result.allWired ? 0 : 1;
313
+ }
314
+ const plan = planSetup();
315
+ if (plan.agents.length === 0) {
316
+ console.log('No supported agent found (looked for Claude Code, Codex CLI, Cursor, VS Code).');
317
+ return 1;
318
+ }
319
+ if (args.dryRun) console.log('Dry run — nothing will be written.');
320
+ printSetupPreview(plan);
321
+ if (args.dryRun) {
322
+ printPathWarning(plan);
323
+ return 0;
324
+ }
325
+ const results = applySetup(plan);
326
+ for (const result of results) console.log(`${result.name}: ${result.action} — ${result.detail}`);
327
+ printPathWarning(plan);
328
+ return results.some((result) => result.action === 'unreadable') ? 1 : 0;
329
+ }
330
+
331
+ async function main(argv) {
306
332
  const args = parseArgs(argv);
307
333
  if (args.error) {
308
334
  console.error(`Error: ${args.error}`);
@@ -317,6 +343,64 @@ function main(argv) {
317
343
  console.log(VERSION);
318
344
  return 0;
319
345
  }
346
+ // The three agent/install modes are exclusive: whichever the user
347
+ // asked for, the other two must not run — --mcp would otherwise
348
+ // swallow `setup --mcp` and sit on stdin waiting for messages.
349
+ const modes = [args.mcp, args.skillGenerate, args.command === 'setup'].filter(Boolean).length;
350
+ if (modes > 1) {
351
+ console.error('Error: --mcp, --skill-generate and setup cannot be combined.');
352
+ console.error('Run with --help to see the available options.');
353
+ return 2;
354
+ }
355
+ if (args.mcp) {
356
+ await runMcpServer({ sandbox: args.sandbox, version: VERSION });
357
+ return 0;
358
+ }
359
+ if (args.skillGenerate) {
360
+ if (args.skillGlobal && args.skillProject) {
361
+ console.error('Error: choose one of --global or --project for --skill-generate.');
362
+ console.error('Run with --help to see the available options.');
363
+ return 2;
364
+ }
365
+ const scope = args.skillGlobal ? 'global' : 'project';
366
+ const out = generateSkill(scope);
367
+ if (!out.created) {
368
+ console.error(`Error: ${out.reason} — refusing to overwrite it.`);
369
+ return 1;
370
+ }
371
+ console.log(`Wrote ${out.path}`);
372
+ return 0;
373
+ }
374
+ if (args.skillGlobal || args.skillProject) {
375
+ console.error('Error: --global and --project only apply to --skill-generate.');
376
+ console.error('Run with --help to see the available options.');
377
+ return 2;
378
+ }
379
+ if (args.sandbox) {
380
+ console.error('Error: --sandbox/--root only applies to --mcp.');
381
+ console.error('Run with --help to see the available options.');
382
+ return 2;
383
+ }
384
+ if (args.dryRun || args.check) {
385
+ if (args.command !== 'setup') {
386
+ console.error('Error: --dry-run and --check only apply to setup.');
387
+ console.error('Run with --help to see the available options.');
388
+ return 2;
389
+ }
390
+ if (args.dryRun && args.check) {
391
+ console.error('Error: choose one of --dry-run or --check for setup.');
392
+ console.error('Run with --help to see the available options.');
393
+ return 2;
394
+ }
395
+ }
396
+ if (args.command === 'setup') {
397
+ if (args.mcp || args.skillGenerate || args.json || args.fix) {
398
+ console.error('Error: setup cannot be combined with --mcp, --skill-generate, --json or --fix.');
399
+ console.error('Run with --help to see the available options.');
400
+ return 2;
401
+ }
402
+ return runSetup(args);
403
+ }
320
404
 
321
405
  const command = args.command || 'all';
322
406
  // `dev-diagnosis <path>` means `dev-diagnosis all <path>`: the usage line
@@ -340,34 +424,47 @@ function main(argv) {
340
424
  try {
341
425
  if (resolvedCommand === 'context') {
342
426
  const target = args.positional[0];
343
- const check = runContext(target);
344
- // In --json mode the fix must be part of the printed object, so it
345
- // runs before printing; in text mode the report prints first, so a
346
- // failed write still leaves the diagnosis on screen.
347
- if (args.fix && args.json) applyContextFix(check);
348
- printContext(check.result, args.json);
349
- if (args.fix && !args.json) {
350
- printFixLine(applyContextFix(check), check.result.suggestions.length);
427
+ const check = runContextCheck(target);
428
+ if (args.fix) {
429
+ // A fix command reports the repo as it stands after the fix:
430
+ // re-scan and let the verdict and the exit code follow the
431
+ // post-fix score, or a successful fix still exits 1. In --json
432
+ // mode the fix runs before printing so the one printed object
433
+ // is the post-fix result; in text mode the pre-fix report
434
+ // prints first, so a failed write still leaves the diagnosis
435
+ // on screen.
436
+ if (args.json) {
437
+ const fixed = applyContextFixAndRescan(check);
438
+ console.log(JSON.stringify(fixed.result, null, 2));
439
+ return fixed.passed ? 0 : 1;
440
+ }
441
+ printContext(check.result, false);
442
+ const fixed = applyContextFixAndRescan(check);
443
+ printFixLine(fixed.result.fix, check.result.suggestions.length);
444
+ console.log(`Re-scanned after fix: Score: ${fixed.result.score}/100 — ${fixed.result.status}`);
445
+ console.log(verdictLine(fixed.passed, fixed.verdict));
446
+ return fixed.passed ? 0 : 1;
351
447
  }
448
+ printContext(check.result, args.json);
352
449
  if (!args.json) console.log(verdictLine(check.passed, check.verdict));
353
450
  return check.passed ? 0 : 1;
354
451
  }
355
452
  if (resolvedCommand === 'path') {
356
453
  const name = args.positional[0];
357
- const check = runPath(name);
454
+ const check = runPathCheck(name);
358
455
  printPath(check.result, args.json);
359
456
  if (!args.json) console.log(verdictLine(check.passed, check.verdict));
360
457
  return check.passed ? 0 : 1;
361
458
  }
362
459
  if (resolvedCommand === 'flakes') {
363
460
  const dir = args.positional[0];
364
- const check = runFlakes(dir);
461
+ const check = runFlakesCheck(dir);
365
462
  printFlakes(check.result, args.top, args.json);
366
463
  if (!args.json) console.log(verdictLine(check.passed, check.verdict));
367
464
  return check.passed ? 0 : 1;
368
465
  }
369
466
  if (resolvedCommand === 'cache') {
370
- const check = runCache({ buildCommand: args.buildCommand, cacheDir: args.cacheDir, cwd: args.cwd });
467
+ const check = runCacheCheck({ buildCommand: args.buildCommand, cacheDir: args.cacheDir, cwd: args.cwd });
371
468
  printCache(check.result, args.json);
372
469
  if (!args.json) console.log(verdictLine(check.passed, check.verdict));
373
470
  return check.passed ? 0 : 1;
@@ -375,79 +472,38 @@ function main(argv) {
375
472
  if (resolvedCommand === 'all') {
376
473
  const target = args.positional[0];
377
474
  const root = target ? path.resolve(target) : process.cwd();
378
- const checks = {};
379
- const skipped = [];
475
+ const run = runAll({
476
+ root,
477
+ commandName: args.commandName,
478
+ flakesDir: args.flakesDir,
479
+ buildCommand: args.buildCommand,
480
+ cacheDir: args.cacheDir,
481
+ });
482
+ const contextCheck = run.checks.context;
483
+ const pathCheck = run.checks.path || null;
484
+ const flakesCheck = run.checks.flakes || null;
485
+ const cacheCheck = run.checks.cache || null;
380
486
 
381
- const contextCheck = runContext(root);
382
- checks.context = contextCheck;
383
- let contextFix = null;
384
- const getContextFix = () => {
385
- if (!contextFix) contextFix = applyContextFix(contextCheck);
386
- return contextFix;
387
- };
388
- if (args.fix && args.json) getContextFix();
389
- const pathCheck = args.commandName ? runPath(args.commandName) : null;
390
- if (pathCheck) checks.path = pathCheck;
391
- else skipped.push('path — pass --command <name>, for example: dev-diagnosis all --command node');
392
- let flakesCheck = null;
393
- if (args.flakesDir) {
394
- try {
395
- flakesCheck = runFlakes(args.flakesDir);
396
- checks.flakes = flakesCheck;
397
- } catch (error) {
398
- checks.flakes = { result: null, passed: false, verdict: `could not run: ${error.message}`, error: error.message };
399
- flakesCheck = checks.flakes;
400
- }
401
- } else {
402
- skipped.push('flakes — pass --flakes <runs-dir>, for example: dev-diagnosis all --flakes examples/runs');
403
- }
404
- let cacheCheck = null;
405
- if (args.buildCommand && args.buildCommand.length > 0) {
406
- try {
407
- cacheCheck = runCache({ buildCommand: args.buildCommand, cacheDir: args.cacheDir, cwd: root });
408
- checks.cache = cacheCheck;
409
- } catch (error) {
410
- if (String(error.message).includes('no cache directory found')) {
411
- // Nothing to measure is not a failure: report it as skipped so
412
- // `all` on a repo without a build cache does not end in FAIL.
413
- skipped.push('cache — no cache directory found in this project; pass --cache-dir <dir> to measure one');
414
- } else {
415
- checks.cache = { result: null, passed: false, verdict: `could not run: ${error.message}`, error: error.message };
416
- cacheCheck = checks.cache;
417
- }
418
- }
419
- } else {
420
- skipped.push('cache — add -- <build-command>, for example: dev-diagnosis all -- node build.mjs');
421
- }
422
-
423
- const overall = Object.values(checks).every((check) => check.passed);
424
487
  if (args.json) {
425
- const out = {
426
- command: 'all', root,
427
- checks: Object.fromEntries(Object.entries(checks).map(([key, check]) => {
428
- let result = check.result;
429
- // Same --top split as the standalone flakes output: the
430
- // embedded tests array is limited, totals stay whole-scan.
431
- if (key === 'flakes' && result && args.top != null) {
432
- result = { ...result, tests: result.tests.slice(0, args.top) };
433
- }
434
- // A check that could not run has no result: keep its reason
435
- // in the JSON instead of emitting null, or a script consumer
436
- // sees overall FAIL with no way to tell why.
437
- if (result == null) result = { error: check.error || check.verdict };
438
- return [key, result];
439
- })),
440
- skipped, overall: overall ? 'PASS' : 'FAIL',
441
- };
442
- console.log(JSON.stringify(out, null, 2));
443
- return overall ? 0 : 1;
488
+ if (args.fix) {
489
+ run.applyContextFix();
490
+ rescanContext(run);
491
+ }
492
+ console.log(JSON.stringify(allJson(run, args.top), null, 2));
493
+ return run.overall ? 0 : 1;
444
494
  }
445
495
 
446
496
  console.log(`dev-diagnosis — ${root}`);
447
497
  console.log(section('context'));
448
498
  printContext(contextCheck.result, false);
449
- if (args.fix) printFixLine(getContextFix(), contextCheck.result.suggestions.length);
450
- console.log(verdictLine(contextCheck.passed, contextCheck.verdict));
499
+ let contextVerdict = contextCheck;
500
+ if (args.fix) {
501
+ printFixLine(run.applyContextFix(), contextCheck.result.suggestions.length);
502
+ const fixed = rescanContext(run);
503
+ console.log(`Re-scanned after fix: Score: ${fixed.result.score}/100 — ${fixed.result.status}`);
504
+ contextVerdict = fixed;
505
+ }
506
+ console.log(verdictLine(contextVerdict.passed, contextVerdict.verdict));
451
507
  if (pathCheck) {
452
508
  console.log(section('path'));
453
509
  printPath(pathCheck.result, false);
@@ -463,13 +519,14 @@ function main(argv) {
463
519
  if (cacheCheck.result) printCache(cacheCheck.result, false);
464
520
  console.log(verdictLine(cacheCheck.passed, cacheCheck.verdict));
465
521
  }
466
- if (skipped.length > 0) {
522
+ if (run.skipped.length > 0) {
467
523
  console.log(section('skipped'));
468
- for (const line of skipped) console.log(` ${line}`);
524
+ for (const line of run.skipped) console.log(` ${line}`);
469
525
  }
470
526
  console.log('');
471
- console.log(overall ? 'Overall: PASS — nothing found to fix' : 'Overall: FAIL — see the checks above');
472
- return overall ? 0 : 1;
527
+ console.log(run.overall ? 'Overall: PASS — nothing found to fix' : 'Overall: FAIL — see the checks above');
528
+ console.log(formatSummary(run.summary));
529
+ return run.overall ? 0 : 1;
473
530
  }
474
531
  console.error(`Error: unknown command: ${command}`);
475
532
  console.error('Run with --help to see the available options.');
@@ -485,4 +542,4 @@ function main(argv) {
485
542
  }
486
543
  }
487
544
 
488
- process.exitCode = main(process.argv.slice(2));
545
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; });