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.
- package/CHANGELOG.md +113 -1
- package/README.md +127 -17
- package/bin/dev-diagnosis.js +239 -182
- package/lib/checks.mjs +280 -0
- package/lib/context.mjs +199 -11
- package/lib/mcp.mjs +271 -0
- package/lib/report.mjs +12 -0
- package/lib/setup.mjs +249 -0
- package/lib/skill.mjs +81 -0
- package/package.json +2 -2
- /package/examples/runs/{run-1 → run-01}/results.xml +0 -0
- /package/examples/runs/{run-2 → run-02}/results.xml +0 -0
package/bin/dev-diagnosis.js
CHANGED
|
@@ -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 {
|
|
25
|
+
import { formatBytes } from '../lib/context.mjs';
|
|
19
26
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
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)
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
379
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
545
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|