dev-diagnosis-cli 0.0.0-stage → 0.1.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 +11 -0
- package/LICENSE +201 -0
- package/README.md +127 -2
- package/bin/dev-diagnosis.js +395 -0
- package/examples/demo-repo/.cache/out.txt +1 -0
- package/examples/demo-repo/.env.example +1 -0
- package/examples/demo-repo/build.mjs +17 -0
- package/examples/demo-repo/data/export.json +20000 -0
- package/examples/demo-repo/dist/app.bundle.js +2 -0
- package/examples/demo-repo/logs/dev.log +2 -0
- package/examples/demo-repo/package-lock.json +1 -0
- package/examples/demo-repo/package.json +1 -0
- package/examples/demo-repo/src/index.js +1 -0
- package/examples/demo.mjs +20 -0
- package/examples/runs/run-01/results.xml +29 -0
- package/examples/runs/run-02/results.xml +22 -0
- package/lib/cache.mjs +159 -0
- package/lib/context.mjs +211 -0
- package/lib/flakes.mjs +265 -0
- package/lib/pathcheck.mjs +393 -0
- package/lib/report.mjs +18 -0
- package/lib/run-command.mjs +73 -0
- package/lib/xml.mjs +221 -0
- package/package.json +1 -6
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dev-diagnosis — one offline CLI that diagnoses a repo the way four small
|
|
4
|
+
* tools do: context noise for AI agents, missing CLI commands, build-cache
|
|
5
|
+
* value, and flaky tests.
|
|
6
|
+
*
|
|
7
|
+
* Standard-library Node.js only, no network, no account. Works on Linux,
|
|
8
|
+
* macOS and Windows: all paths go through node:path, JUnit input is
|
|
9
|
+
* normalised from CRLF before parsing, and build commands run without a
|
|
10
|
+
* shell (see lib/run-command.mjs for the one Windows-only retry).
|
|
11
|
+
*
|
|
12
|
+
* Exit codes: 0 = every check that ran passed; 1 = at least one check
|
|
13
|
+
* found something to fix, or a check could not run; 2 = usage error.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { scanDirectory, formatBytes } from '../lib/context.mjs';
|
|
19
|
+
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';
|
|
26
|
+
|
|
27
|
+
const VERSION = JSON.parse(
|
|
28
|
+
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
29
|
+
).version;
|
|
30
|
+
|
|
31
|
+
const USAGE = `dev-diagnosis: one diagnosis for your repo, your commands, your cache and your tests.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
dev-diagnosis [all] [path] [--command <name>] [--flakes <dir>] [-- <build-command...>]
|
|
35
|
+
dev-diagnosis context [path] [--json]
|
|
36
|
+
dev-diagnosis path <command> [--json]
|
|
37
|
+
dev-diagnosis cache [--cache-dir <dir>] [--cwd <dir>] -- <build-command...>
|
|
38
|
+
dev-diagnosis flakes <runs-dir> [--json] [--top N]
|
|
39
|
+
dev-diagnosis --help
|
|
40
|
+
dev-diagnosis --version
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
all Run every check that has what it needs and print one unified
|
|
44
|
+
report. context always runs on [path] (default: current
|
|
45
|
+
directory). path runs when --command is given, flakes when
|
|
46
|
+
--flakes is given, cache when a build command follows --.
|
|
47
|
+
Checks without their input are listed as skipped, with the
|
|
48
|
+
command that would run them.
|
|
49
|
+
context Find the files an AI coding agent should not read: build
|
|
50
|
+
output, lockfiles, logs and oversized files. Prints a 0-100
|
|
51
|
+
score and the .aiignore-style suggestions that would fix it.
|
|
52
|
+
path Diagnose why a CLI command is missing: one row per shell
|
|
53
|
+
context (current, login, non-login, daemon-like), shadowed
|
|
54
|
+
copies, toolchain installs, and a copy-paste fix.
|
|
55
|
+
cache Time one build command cold (cache moved aside) and warm
|
|
56
|
+
(cache present) and say whether the cache pays. The cache
|
|
57
|
+
directory is restored to its original state afterwards.
|
|
58
|
+
flakes Rank flaky tests from JUnit XML across CI runs, including
|
|
59
|
+
passed-on-retry. Each subdirectory of <runs-dir> is one run;
|
|
60
|
+
each *.xml file directly under it is a run of its own.
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--json machine-readable output (context, path, flakes, all)
|
|
64
|
+
--top N show only the N highest-ranked flaky tests
|
|
65
|
+
-h, --help show this help
|
|
66
|
+
-v, --version print the version
|
|
67
|
+
|
|
68
|
+
Exit codes: 0 = every check that ran passed; 1 = a check found something
|
|
69
|
+
to fix or could not run; 2 = usage error.
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
class UsageError extends Error {
|
|
73
|
+
constructor(message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = 'UsageError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseArgs(argv) {
|
|
80
|
+
const parsed = {
|
|
81
|
+
command: null, positional: [], json: false, help: false, version: false,
|
|
82
|
+
commandName: null, flakesDir: null, cacheDir: null, cwd: null, top: null,
|
|
83
|
+
buildCommand: null, error: null,
|
|
84
|
+
};
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < argv.length) {
|
|
87
|
+
const arg = argv[i];
|
|
88
|
+
if (arg === '--') {
|
|
89
|
+
parsed.buildCommand = argv.slice(i + 1);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (arg === '--json' || arg === '-j') parsed.json = true;
|
|
93
|
+
else if (arg === '--help' || arg === '-h') parsed.help = true;
|
|
94
|
+
else if (arg === '--version' || arg === '-v') parsed.version = true;
|
|
95
|
+
else if (arg === '--command') {
|
|
96
|
+
parsed.commandName = argv[i + 1];
|
|
97
|
+
if (parsed.commandName === undefined) parsed.error = '--command needs a value';
|
|
98
|
+
i += 1;
|
|
99
|
+
} else if (arg.startsWith('--command=')) parsed.commandName = arg.slice('--command='.length);
|
|
100
|
+
else if (arg === '--flakes') {
|
|
101
|
+
parsed.flakesDir = argv[i + 1];
|
|
102
|
+
if (parsed.flakesDir === undefined) parsed.error = '--flakes needs a value';
|
|
103
|
+
i += 1;
|
|
104
|
+
} else if (arg.startsWith('--flakes=')) parsed.flakesDir = arg.slice('--flakes='.length);
|
|
105
|
+
else if (arg === '--cache-dir') {
|
|
106
|
+
parsed.cacheDir = argv[i + 1];
|
|
107
|
+
if (parsed.cacheDir === undefined) parsed.error = '--cache-dir needs a value';
|
|
108
|
+
i += 1;
|
|
109
|
+
} else if (arg.startsWith('--cache-dir=')) parsed.cacheDir = arg.slice('--cache-dir='.length);
|
|
110
|
+
else if (arg === '--cwd') {
|
|
111
|
+
parsed.cwd = argv[i + 1];
|
|
112
|
+
if (parsed.cwd === undefined) parsed.error = '--cwd needs a value';
|
|
113
|
+
i += 1;
|
|
114
|
+
} else if (arg.startsWith('--cwd=')) parsed.cwd = arg.slice('--cwd='.length);
|
|
115
|
+
else if (arg === '--top') {
|
|
116
|
+
const value = Number(argv[i + 1]);
|
|
117
|
+
if (!Number.isInteger(value) || value < 1) parsed.error = '--top needs a positive integer';
|
|
118
|
+
else parsed.top = value;
|
|
119
|
+
i += 1;
|
|
120
|
+
} else if (arg.startsWith('--top=')) {
|
|
121
|
+
const value = Number(arg.slice('--top='.length));
|
|
122
|
+
if (!Number.isInteger(value) || value < 1) parsed.error = '--top needs a positive integer';
|
|
123
|
+
else parsed.top = value;
|
|
124
|
+
} else if (arg.startsWith('-')) {
|
|
125
|
+
parsed.error = `unknown option: ${arg}`;
|
|
126
|
+
} else if (parsed.command === null) parsed.command = arg;
|
|
127
|
+
else parsed.positional.push(arg);
|
|
128
|
+
i += 1;
|
|
129
|
+
}
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printContext(result, json) {
|
|
134
|
+
if (json) {
|
|
135
|
+
console.log(JSON.stringify(result, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log(`Repo: ${result.root}`);
|
|
139
|
+
console.log(`Files: ${result.files} Size: ${formatBytes(result.bytes)} Score: ${result.score}/100 — ${result.status}`);
|
|
140
|
+
if (result.truncated) console.log('Note: scan stopped at the file limit; totals are a lower bound.');
|
|
141
|
+
for (const item of result.breakdown) {
|
|
142
|
+
console.log(` ${item.label}: ${formatBytes(item.bytes)} (${item.share}%) — lost ${item.lost} of ${item.weight}`);
|
|
143
|
+
}
|
|
144
|
+
if (result.noise.length > 0) {
|
|
145
|
+
console.log('Noise to keep away from an AI agent:');
|
|
146
|
+
for (const item of result.noise.slice(0, 20)) {
|
|
147
|
+
console.log(` ${item.label} — ${item.reason} (${formatBytes(item.bytes)})`);
|
|
148
|
+
}
|
|
149
|
+
if (result.noise.length > 20) console.log(` ... and ${result.noise.length - 20} more`);
|
|
150
|
+
}
|
|
151
|
+
if (result.review.length > 0) {
|
|
152
|
+
console.log('Worth a look before sharing:');
|
|
153
|
+
for (const item of result.review.slice(0, 10)) {
|
|
154
|
+
console.log(` ${item.label} — ${item.reason}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (result.suggestions.length > 0) {
|
|
158
|
+
console.log('Fix: add these to .aiignore (or the agent\'s ignore list):');
|
|
159
|
+
for (const suggestion of result.suggestions) console.log(` ${suggestion}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function runContext(target) {
|
|
164
|
+
const result = scanDirectory(target || process.cwd());
|
|
165
|
+
const passed = result.score >= 85;
|
|
166
|
+
return { result, passed, verdict: passed ? 'context is clean' : `${result.status.toLowerCase()} (score ${result.score}/100)` };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function printPath(result, json) {
|
|
170
|
+
if (json) {
|
|
171
|
+
console.log(JSON.stringify(result, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const row of result.contexts) {
|
|
175
|
+
const status = row.resolved ? row.path : `MISS — ${row.reason}`;
|
|
176
|
+
console.log(`${row.name.padEnd(12)} ${status}`);
|
|
177
|
+
}
|
|
178
|
+
if (result.copies.shadowed.length > 0) {
|
|
179
|
+
console.log(`Shadowed by the winning copy: ${result.copies.shadowed.map((c) => c.path).join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
if (result.toolchainHits.length > 0) {
|
|
182
|
+
console.log('Installed, but not on PATH:');
|
|
183
|
+
for (const hit of result.toolchainHits) console.log(` ${hit.path}`);
|
|
184
|
+
}
|
|
185
|
+
if (result.fix) console.log(`Fix: ${result.fix}`);
|
|
186
|
+
const unreliable = result.contexts.filter((row) => !row.reliable);
|
|
187
|
+
for (const row of unreliable) console.log(`Note: ${row.name} context fell back to the current PATH (${row.note}).`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function runPath(commandName) {
|
|
191
|
+
if (!commandName) throw new UsageError('path needs a command, for example: dev-diagnosis path node');
|
|
192
|
+
const contexts = buildContexts();
|
|
193
|
+
const rows = contexts.map((ctx) => {
|
|
194
|
+
const found = lookupCommand(commandName, ctx.pathValue);
|
|
195
|
+
return { ...ctx, resolved: found.resolved, path: found.path, reason: found.reason || null };
|
|
196
|
+
});
|
|
197
|
+
const copies = findCommandCopies(commandName, process.env.PATH || '');
|
|
198
|
+
const toolchainHits = rows[0].resolved ? [] : searchToolchainBins(commandName);
|
|
199
|
+
let fix = null;
|
|
200
|
+
if (!rows[0].resolved && toolchainHits.length > 0) fix = fixForDirectory(toolchainHits[0].directory);
|
|
201
|
+
else if (!rows[0].resolved && rows.some((row) => row.resolved)) {
|
|
202
|
+
fix = 'it resolves in another shell context — check which startup file sets your PATH';
|
|
203
|
+
}
|
|
204
|
+
const passed = rows[0].resolved;
|
|
205
|
+
return {
|
|
206
|
+
result: { command: commandName, contexts: rows, copies, toolchainHits, fix },
|
|
207
|
+
passed,
|
|
208
|
+
verdict: passed ? `${commandName} resolves to ${rows[0].path}` : `${commandName} is missing in the current shell`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printFlakes(result, top, json) {
|
|
213
|
+
if (json) {
|
|
214
|
+
console.log(JSON.stringify(result, null, 2));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
console.log(formatFlakesTable(result, top));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function runFlakes(dir) {
|
|
221
|
+
if (!dir) throw new UsageError('flakes needs a runs directory, for example: dev-diagnosis flakes examples/runs');
|
|
222
|
+
const result = scanRuns(dir);
|
|
223
|
+
const passed = result.totals.flaky === 0 && result.totals.alwaysFailing === 0;
|
|
224
|
+
return {
|
|
225
|
+
result,
|
|
226
|
+
passed,
|
|
227
|
+
verdict: passed
|
|
228
|
+
? `no flaky tests across ${result.totals.runs} runs`
|
|
229
|
+
: `${result.totals.flaky} flaky, ${result.totals.alwaysFailing} always-failing across ${result.totals.runs} runs`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function runCache(options) {
|
|
234
|
+
if (!options.buildCommand || options.buildCommand.length === 0) {
|
|
235
|
+
throw new UsageError('cache needs a build command after --, for example: dev-diagnosis cache -- node build.mjs');
|
|
236
|
+
}
|
|
237
|
+
const result = runCacheCheck({
|
|
238
|
+
root: options.cwd || process.cwd(),
|
|
239
|
+
cacheDir: options.cacheDir,
|
|
240
|
+
command: options.buildCommand,
|
|
241
|
+
});
|
|
242
|
+
const passed = result.verdict !== 'cache costs';
|
|
243
|
+
return {
|
|
244
|
+
result,
|
|
245
|
+
passed,
|
|
246
|
+
verdict: `${result.verdict}: cold ${formatMs(result.coldMs)}, warm ${formatMs(result.warmMs)} (${result.verdictDetail})`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function printCache(result, json) {
|
|
251
|
+
if (json) {
|
|
252
|
+
const { coldStdout, warmStdout, ...rest } = result;
|
|
253
|
+
console.log(JSON.stringify(rest, null, 2));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
console.log(`Cache dir: ${result.cacheDir}`);
|
|
257
|
+
console.log(`Command: ${result.command.join(' ')}`);
|
|
258
|
+
console.log(`Cold: ${formatMs(result.coldMs)} Warm: ${formatMs(result.warmMs)} Saved: ${formatMs(Math.max(0, result.savedMs))} (${result.savedPct}%)`);
|
|
259
|
+
console.log(`Verdict: ${result.verdict} — ${result.verdictDetail}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function main(argv) {
|
|
263
|
+
const args = parseArgs(argv);
|
|
264
|
+
if (args.error) {
|
|
265
|
+
console.error(`Error: ${args.error}`);
|
|
266
|
+
console.error('Run with --help to see the available options.');
|
|
267
|
+
return 2;
|
|
268
|
+
}
|
|
269
|
+
if (args.help) {
|
|
270
|
+
console.log(USAGE);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
if (args.version) {
|
|
274
|
+
console.log(VERSION);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const command = args.command || 'all';
|
|
279
|
+
try {
|
|
280
|
+
if (command === 'context') {
|
|
281
|
+
const target = args.positional[0];
|
|
282
|
+
const check = runContext(target);
|
|
283
|
+
printContext(check.result, args.json);
|
|
284
|
+
if (!args.json) console.log(verdictLine(check.passed, check.verdict));
|
|
285
|
+
return check.passed ? 0 : 1;
|
|
286
|
+
}
|
|
287
|
+
if (command === 'path') {
|
|
288
|
+
const name = args.positional[0];
|
|
289
|
+
const check = runPath(name);
|
|
290
|
+
printPath(check.result, args.json);
|
|
291
|
+
if (!args.json) console.log(verdictLine(check.passed, check.verdict));
|
|
292
|
+
return check.passed ? 0 : 1;
|
|
293
|
+
}
|
|
294
|
+
if (command === 'flakes') {
|
|
295
|
+
const dir = args.positional[0];
|
|
296
|
+
const check = runFlakes(dir);
|
|
297
|
+
printFlakes(check.result, args.top, args.json);
|
|
298
|
+
if (!args.json) console.log(verdictLine(check.passed, check.verdict));
|
|
299
|
+
return check.passed ? 0 : 1;
|
|
300
|
+
}
|
|
301
|
+
if (command === 'cache') {
|
|
302
|
+
const check = runCache({ buildCommand: args.buildCommand, cacheDir: args.cacheDir, cwd: args.cwd });
|
|
303
|
+
printCache(check.result, args.json);
|
|
304
|
+
if (!args.json) console.log(verdictLine(check.passed, check.verdict));
|
|
305
|
+
return check.passed ? 0 : 1;
|
|
306
|
+
}
|
|
307
|
+
if (command === 'all') {
|
|
308
|
+
const target = args.positional[0];
|
|
309
|
+
const root = target ? path.resolve(target) : process.cwd();
|
|
310
|
+
const checks = {};
|
|
311
|
+
const skipped = [];
|
|
312
|
+
|
|
313
|
+
const contextCheck = runContext(root);
|
|
314
|
+
checks.context = contextCheck;
|
|
315
|
+
const pathCheck = args.commandName ? runPath(args.commandName) : null;
|
|
316
|
+
if (pathCheck) checks.path = pathCheck;
|
|
317
|
+
else skipped.push('path — pass --command <name>, for example: dev-diagnosis all --command node');
|
|
318
|
+
let flakesCheck = null;
|
|
319
|
+
if (args.flakesDir) {
|
|
320
|
+
try {
|
|
321
|
+
flakesCheck = runFlakes(args.flakesDir);
|
|
322
|
+
checks.flakes = flakesCheck;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
checks.flakes = { result: null, passed: false, verdict: `could not run: ${error.message}`, error: error.message };
|
|
325
|
+
flakesCheck = checks.flakes;
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
skipped.push('flakes — pass --flakes <runs-dir>, for example: dev-diagnosis all --flakes examples/runs');
|
|
329
|
+
}
|
|
330
|
+
let cacheCheck = null;
|
|
331
|
+
if (args.buildCommand && args.buildCommand.length > 0) {
|
|
332
|
+
try {
|
|
333
|
+
cacheCheck = runCache({ buildCommand: args.buildCommand, cacheDir: args.cacheDir, cwd: root });
|
|
334
|
+
checks.cache = cacheCheck;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
checks.cache = { result: null, passed: false, verdict: `could not run: ${error.message}`, error: error.message };
|
|
337
|
+
cacheCheck = checks.cache;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
skipped.push('cache — add -- <build-command>, for example: dev-diagnosis all -- node build.mjs');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const overall = Object.values(checks).every((check) => check.passed);
|
|
344
|
+
if (args.json) {
|
|
345
|
+
const out = {
|
|
346
|
+
command: 'all', root,
|
|
347
|
+
checks: Object.fromEntries(Object.entries(checks).map(([key, check]) => [key, check.result])),
|
|
348
|
+
skipped, overall: overall ? 'PASS' : 'FAIL',
|
|
349
|
+
};
|
|
350
|
+
console.log(JSON.stringify(out, null, 2));
|
|
351
|
+
return overall ? 0 : 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log(`dev-diagnosis — ${root}`);
|
|
355
|
+
console.log(section('context'));
|
|
356
|
+
printContext(contextCheck.result, false);
|
|
357
|
+
console.log(verdictLine(contextCheck.passed, contextCheck.verdict));
|
|
358
|
+
if (pathCheck) {
|
|
359
|
+
console.log(section('path'));
|
|
360
|
+
printPath(pathCheck.result, false);
|
|
361
|
+
console.log(verdictLine(pathCheck.passed, pathCheck.verdict));
|
|
362
|
+
}
|
|
363
|
+
if (flakesCheck) {
|
|
364
|
+
console.log(section('flakes'));
|
|
365
|
+
if (flakesCheck.result) printFlakes(flakesCheck.result, args.top, false);
|
|
366
|
+
console.log(verdictLine(flakesCheck.passed, flakesCheck.verdict));
|
|
367
|
+
}
|
|
368
|
+
if (cacheCheck) {
|
|
369
|
+
console.log(section('cache'));
|
|
370
|
+
if (cacheCheck.result) printCache(cacheCheck.result, false);
|
|
371
|
+
console.log(verdictLine(cacheCheck.passed, cacheCheck.verdict));
|
|
372
|
+
}
|
|
373
|
+
if (skipped.length > 0) {
|
|
374
|
+
console.log(section('skipped'));
|
|
375
|
+
for (const line of skipped) console.log(` ${line}`);
|
|
376
|
+
}
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(overall ? 'Overall: PASS — nothing found to fix' : 'Overall: FAIL — see the checks above');
|
|
379
|
+
return overall ? 0 : 1;
|
|
380
|
+
}
|
|
381
|
+
console.error(`Error: unknown command: ${command}`);
|
|
382
|
+
console.error('Run with --help to see the available options.');
|
|
383
|
+
return 2;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error instanceof UsageError) {
|
|
386
|
+
console.error(`Error: ${error.message}`);
|
|
387
|
+
console.error('Run with --help to see the available options.');
|
|
388
|
+
return 2;
|
|
389
|
+
}
|
|
390
|
+
console.error(`Error: ${error.message}`);
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
built
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
API_KEY=your-key-here
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Demo build: slow on a cold cache, fast on a warm one.
|
|
2
|
+
// If .cache/out.txt exists the work is already done; otherwise this
|
|
3
|
+
// script simulates 150 ms of real work and writes the cache file.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const cacheDir = path.join(process.cwd(), '.cache');
|
|
8
|
+
const out = path.join(cacheDir, 'out.txt');
|
|
9
|
+
if (fs.existsSync(out)) {
|
|
10
|
+
console.log('cache hit');
|
|
11
|
+
} else {
|
|
12
|
+
const until = Date.now() + 150;
|
|
13
|
+
while (Date.now() < until) { /* simulate work the cache would skip */ }
|
|
14
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
15
|
+
fs.writeFileSync(out, 'built\n');
|
|
16
|
+
console.log('cache miss');
|
|
17
|
+
}
|