dev-diagnosis-cli 0.1.0 → 0.1.2
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 +24 -0
- package/README.md +14 -9
- package/bin/dev-diagnosis.js +58 -6
- package/examples/demo-repo/build.mjs +4 -2
- package/lib/context.mjs +101 -3
- package/package.json +1 -1
- /package/examples/runs/{run-01 → run-1}/results.xml +0 -0
- /package/examples/runs/{run-02 → run-2}/results.xml +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-11-11
|
|
4
|
+
|
|
5
|
+
- New: `context --fix` (and `all --fix`) writes the `.aiignore`
|
|
6
|
+
suggestions into `<path>/.aiignore` instead of only printing them —
|
|
7
|
+
entries already there are left alone, nothing else in the file is
|
|
8
|
+
touched, and no file is created when there is nothing to add. The fix
|
|
9
|
+
outcome is included in `--json` output.
|
|
10
|
+
- Fix: the context scan now honours the repo's `.aiignore` — files it
|
|
11
|
+
lists are excluded from the scan entirely, so applying the fix and
|
|
12
|
+
re-scanning comes back clean instead of failing on the same noise
|
|
13
|
+
forever. (50 tests.)
|
|
14
|
+
|
|
15
|
+
## 0.1.1 — 2026-11-11
|
|
16
|
+
|
|
17
|
+
- Docs: README now has npm version, CI, license and Node badges, and the
|
|
18
|
+
example output is regenerated verbatim — its paths match the repo layout.
|
|
19
|
+
- Fix: the demo build and the cache test fixture now simulate 400 ms of
|
|
20
|
+
work (was 150 ms), so the cache check's verdict stays decisive even on
|
|
21
|
+
loaded CI machines — 0.1.1's first CI run failed one job (macOS, Node 22)
|
|
22
|
+
because a 41 ms saving fell under the 50 ms "cache pays" threshold.
|
|
23
|
+
- Fix: `all` on a repo with no build cache no longer ends in FAIL — a
|
|
24
|
+
build command with no cache directory to measure is listed as skipped,
|
|
25
|
+
with the `--cache-dir` hint, instead of a failed cache check. (41 tests.)
|
|
26
|
+
|
|
3
27
|
## 0.1.0 — 2026-11-10
|
|
4
28
|
|
|
5
29
|
- First release: one CLI combining the four diagnostic checks.
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# dev-diagnosis
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dev-diagnosis-cli)
|
|
4
|
+
[](https://github.com/WJDdidi-1/dev-doctor/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/WJDdidi-1/dev-doctor/blob/main/LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/dev-diagnosis-cli)
|
|
7
|
+
|
|
3
8
|
One command that diagnoses your repo, your commands, your cache and your tests. It runs four checks that used to be four separate tools — whether an AI coding agent would read files it should not (`context`), why a CLI command is missing (`path`), whether a build cache actually pays (`cache`), and which tests are flaky (`flakes`) — and prints one report with a verdict per check. It is offline, has no runtime dependencies (Node.js standard library only), and works on Linux, macOS and Windows.
|
|
4
9
|
|
|
5
10
|
## Install and run
|
|
@@ -25,7 +30,7 @@ dev-diagnosis flakes <junit-dir> # flaky tests from JUnit XML
|
|
|
25
30
|
dev-diagnosis all [path] --command <name> --flakes <dir> -- <build-command...>
|
|
26
31
|
```
|
|
27
32
|
|
|
28
|
-
`all` always runs `context` on `[path]` (default: the current directory) and runs `path`, `flakes` and `cache` when their input is given. Checks without their input are listed as skipped, with the command that would run them, instead of silently pretending they ran. `--json` gives machine-readable output for `context`, `path`, `flakes` and `all`.
|
|
33
|
+
`all` always runs `context` on `[path]` (default: the current directory) and runs `path`, `flakes` and `cache` when their input is given. Checks without their input are listed as skipped, with the command that would run them, instead of silently pretending they ran. A build command on a repo with no cache directory to measure is also listed as skipped rather than failing: nothing to measure is not a problem. `--json` gives machine-readable output for `context`, `path`, `flakes` and `all`. Add `--fix` to `context` (or `all`) to write the suggestions into `<path>/.aiignore` instead of only printing them: entries already there are left alone, nothing else in the file is touched, and a later scan skips everything `.aiignore` lists — so `context --fix` followed by `context` comes back clean.
|
|
29
34
|
|
|
30
35
|
Exit codes: `0` every check that ran passed; `1` a check found something to fix or could not run; `2` usage error.
|
|
31
36
|
|
|
@@ -35,10 +40,10 @@ The repo ships a small demo project in `examples/demo-repo` (a build output, a l
|
|
|
35
40
|
|
|
36
41
|
```
|
|
37
42
|
$ dev-diagnosis all examples/demo-repo --command node --flakes examples/runs -- node build.mjs
|
|
38
|
-
dev-diagnosis — /home/hatch/workspace/dev-
|
|
43
|
+
dev-diagnosis — /home/hatch/workspace/dev-doctor-build/dev-doctor/examples/demo-repo
|
|
39
44
|
|
|
40
45
|
== context ==
|
|
41
|
-
Repo: /home/hatch/workspace/dev-
|
|
46
|
+
Repo: /home/hatch/workspace/dev-doctor-build/dev-doctor/examples/demo-repo
|
|
42
47
|
Files: 9 Size: 1.68 MB Score: 75/100 — Some context noise found
|
|
43
48
|
build output: 98 B (0%) — lost 0 of 40
|
|
44
49
|
large files: 1.68 MB (99.9%) — lost 25 of 25
|
|
@@ -72,11 +77,11 @@ com.example.CheckoutTest::appliesCoupon 1/2 2 new passed-o
|
|
|
72
77
|
FAIL 1 flaky, 1 always-failing across 2 runs
|
|
73
78
|
|
|
74
79
|
== cache ==
|
|
75
|
-
Cache dir: /home/hatch/workspace/dev-
|
|
80
|
+
Cache dir: /home/hatch/workspace/dev-doctor-build/dev-doctor/examples/demo-repo/.cache
|
|
76
81
|
Command: node build.mjs
|
|
77
|
-
Cold:
|
|
78
|
-
Verdict: cache pays — warm run is
|
|
79
|
-
PASS cache pays: cold
|
|
82
|
+
Cold: 491 ms Warm: 90 ms Saved: 401 ms (82%)
|
|
83
|
+
Verdict: cache pays — warm run is 401 ms (82%) faster than cold
|
|
84
|
+
PASS cache pays: cold 491 ms, warm 90 ms (warm run is 401 ms (82%) faster than cold)
|
|
80
85
|
|
|
81
86
|
Overall: FAIL — see the checks above
|
|
82
87
|
```
|
|
@@ -87,7 +92,7 @@ The demo repo is intentionally noisy, so `all` exits 1: the context and flakes c
|
|
|
87
92
|
|
|
88
93
|
### `context` — what an AI agent should not read
|
|
89
94
|
|
|
90
|
-
Walks the repo (skipping `.git`, `node_modules` and other hidden directories) and classifies every file: build output, large files over 1 MB, lockfiles, logs, binary payloads, secret-like files and environment templates. It prints a 0–100 score, the bytes lost per category, the noisy files, the files worth a look before sharing, and the `.aiignore` entries that would fix it.
|
|
95
|
+
Walks the repo (skipping `.git`, `node_modules` and other hidden directories) and classifies every file: build output, large files over 1 MB, lockfiles, logs, binary payloads, secret-like files and environment templates. It prints a 0–100 score, the bytes lost per category, the noisy files, the files worth a look before sharing, and the `.aiignore` entries that would fix it. Files the repo's `.aiignore` already lists are excluded from the scan entirely — they are what the agent will not be handed, so they are not noise any more. `context --fix` writes the suggestions into `.aiignore` for you; `--json` includes the fix outcome in the output.
|
|
91
96
|
|
|
92
97
|
The score is the *noise-only* part of ai-context-doctor's score: 100 minus the category-weighted share of noise bytes (build 40, large 25, lockfile 20, log 15). The full ai-context-doctor score also weighs context volume, one file dominating, file sprawl and binary payloads, so its composite number reads lower on the same repo. The file classification itself is the same.
|
|
93
98
|
|
|
@@ -106,7 +111,7 @@ Reads JUnit XML across CI runs — each subdirectory of `<junit-dir>` is one run
|
|
|
106
111
|
## Development
|
|
107
112
|
|
|
108
113
|
```sh
|
|
109
|
-
npm test # node:test, no extra dependencies —
|
|
114
|
+
npm test # node:test, no extra dependencies — 50 tests
|
|
110
115
|
npm run example # run the CLI against the demo fixture
|
|
111
116
|
```
|
|
112
117
|
|
package/bin/dev-diagnosis.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
-
import { scanDirectory, formatBytes } from '../lib/context.mjs';
|
|
18
|
+
import { scanDirectory, applyAiignore, formatBytes } from '../lib/context.mjs';
|
|
19
19
|
import {
|
|
20
20
|
buildContexts, findCommandCopies, fixForDirectory, lookupCommand,
|
|
21
21
|
searchToolchainBins,
|
|
@@ -31,8 +31,8 @@ const VERSION = JSON.parse(
|
|
|
31
31
|
const USAGE = `dev-diagnosis: one diagnosis for your repo, your commands, your cache and your tests.
|
|
32
32
|
|
|
33
33
|
Usage:
|
|
34
|
-
dev-diagnosis [all] [path] [--command <name>] [--flakes <dir>] [-- <build-command...>]
|
|
35
|
-
dev-diagnosis context [path] [--json]
|
|
34
|
+
dev-diagnosis [all] [path] [--command <name>] [--flakes <dir>] [--fix] [-- <build-command...>]
|
|
35
|
+
dev-diagnosis context [path] [--json] [--fix]
|
|
36
36
|
dev-diagnosis path <command> [--json]
|
|
37
37
|
dev-diagnosis cache [--cache-dir <dir>] [--cwd <dir>] -- <build-command...>
|
|
38
38
|
dev-diagnosis flakes <runs-dir> [--json] [--top N]
|
|
@@ -61,6 +61,10 @@ Commands:
|
|
|
61
61
|
|
|
62
62
|
Options:
|
|
63
63
|
--json machine-readable output (context, path, flakes, all)
|
|
64
|
+
--fix write the context check's .aiignore suggestions into
|
|
65
|
+
<path>/.aiignore (context and all only). Entries that
|
|
66
|
+
are already there are left alone; nothing else in the
|
|
67
|
+
file is changed.
|
|
64
68
|
--top N show only the N highest-ranked flaky tests
|
|
65
69
|
-h, --help show this help
|
|
66
70
|
-v, --version print the version
|
|
@@ -78,7 +82,7 @@ class UsageError extends Error {
|
|
|
78
82
|
|
|
79
83
|
function parseArgs(argv) {
|
|
80
84
|
const parsed = {
|
|
81
|
-
command: null, positional: [], json: false, help: false, version: false,
|
|
85
|
+
command: null, positional: [], json: false, fix: false, help: false, version: false,
|
|
82
86
|
commandName: null, flakesDir: null, cacheDir: null, cwd: null, top: null,
|
|
83
87
|
buildCommand: null, error: null,
|
|
84
88
|
};
|
|
@@ -90,6 +94,7 @@ function parseArgs(argv) {
|
|
|
90
94
|
break;
|
|
91
95
|
}
|
|
92
96
|
if (arg === '--json' || arg === '-j') parsed.json = true;
|
|
97
|
+
else if (arg === '--fix') parsed.fix = true;
|
|
93
98
|
else if (arg === '--help' || arg === '-h') parsed.help = true;
|
|
94
99
|
else if (arg === '--version' || arg === '-v') parsed.version = true;
|
|
95
100
|
else if (arg === '--command') {
|
|
@@ -166,6 +171,28 @@ function runContext(target) {
|
|
|
166
171
|
return { result, passed, verdict: passed ? 'context is clean' : `${result.status.toLowerCase()} (score ${result.score}/100)` };
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
// Apply the context check's .aiignore suggestions and record the outcome
|
|
175
|
+
// on the check result, so --json output carries it too.
|
|
176
|
+
function applyContextFix(check) {
|
|
177
|
+
const fix = applyAiignore(check.result.root, check.result.suggestions);
|
|
178
|
+
check.result.fix = fix;
|
|
179
|
+
return fix;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function printFixLine(fix, suggestionCount) {
|
|
183
|
+
if (fix.added.length > 0) {
|
|
184
|
+
const noun = fix.added.length === 1 ? 'entry' : 'entries';
|
|
185
|
+
const lead = fix.created
|
|
186
|
+
? `Created .aiignore with ${fix.added.length} ${noun}`
|
|
187
|
+
: `Added ${fix.added.length} ${noun} to .aiignore`;
|
|
188
|
+
console.log(`${lead}: ${fix.added.join(', ')}`);
|
|
189
|
+
} else if (suggestionCount === 0) {
|
|
190
|
+
console.log('Nothing to add to .aiignore: no suggestions.');
|
|
191
|
+
} else {
|
|
192
|
+
console.log('Nothing added: .aiignore already has all suggestions.');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
function printPath(result, json) {
|
|
170
197
|
if (json) {
|
|
171
198
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -276,11 +303,23 @@ function main(argv) {
|
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
const command = args.command || 'all';
|
|
306
|
+
if (args.fix && command !== 'context' && command !== 'all') {
|
|
307
|
+
console.error('Error: --fix only applies to context (and all).');
|
|
308
|
+
console.error('Run with --help to see the available options.');
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
279
311
|
try {
|
|
280
312
|
if (command === 'context') {
|
|
281
313
|
const target = args.positional[0];
|
|
282
314
|
const check = runContext(target);
|
|
315
|
+
// In --json mode the fix must be part of the printed object, so it
|
|
316
|
+
// runs before printing; in text mode the report prints first, so a
|
|
317
|
+
// failed write still leaves the diagnosis on screen.
|
|
318
|
+
if (args.fix && args.json) applyContextFix(check);
|
|
283
319
|
printContext(check.result, args.json);
|
|
320
|
+
if (args.fix && !args.json) {
|
|
321
|
+
printFixLine(applyContextFix(check), check.result.suggestions.length);
|
|
322
|
+
}
|
|
284
323
|
if (!args.json) console.log(verdictLine(check.passed, check.verdict));
|
|
285
324
|
return check.passed ? 0 : 1;
|
|
286
325
|
}
|
|
@@ -312,6 +351,12 @@ function main(argv) {
|
|
|
312
351
|
|
|
313
352
|
const contextCheck = runContext(root);
|
|
314
353
|
checks.context = contextCheck;
|
|
354
|
+
let contextFix = null;
|
|
355
|
+
const getContextFix = () => {
|
|
356
|
+
if (!contextFix) contextFix = applyContextFix(contextCheck);
|
|
357
|
+
return contextFix;
|
|
358
|
+
};
|
|
359
|
+
if (args.fix && args.json) getContextFix();
|
|
315
360
|
const pathCheck = args.commandName ? runPath(args.commandName) : null;
|
|
316
361
|
if (pathCheck) checks.path = pathCheck;
|
|
317
362
|
else skipped.push('path — pass --command <name>, for example: dev-diagnosis all --command node');
|
|
@@ -333,8 +378,14 @@ function main(argv) {
|
|
|
333
378
|
cacheCheck = runCache({ buildCommand: args.buildCommand, cacheDir: args.cacheDir, cwd: root });
|
|
334
379
|
checks.cache = cacheCheck;
|
|
335
380
|
} catch (error) {
|
|
336
|
-
|
|
337
|
-
|
|
381
|
+
if (String(error.message).includes('no cache directory found')) {
|
|
382
|
+
// Nothing to measure is not a failure: report it as skipped so
|
|
383
|
+
// `all` on a repo without a build cache does not end in FAIL.
|
|
384
|
+
skipped.push('cache — no cache directory found in this project; pass --cache-dir <dir> to measure one');
|
|
385
|
+
} else {
|
|
386
|
+
checks.cache = { result: null, passed: false, verdict: `could not run: ${error.message}`, error: error.message };
|
|
387
|
+
cacheCheck = checks.cache;
|
|
388
|
+
}
|
|
338
389
|
}
|
|
339
390
|
} else {
|
|
340
391
|
skipped.push('cache — add -- <build-command>, for example: dev-diagnosis all -- node build.mjs');
|
|
@@ -354,6 +405,7 @@ function main(argv) {
|
|
|
354
405
|
console.log(`dev-diagnosis — ${root}`);
|
|
355
406
|
console.log(section('context'));
|
|
356
407
|
printContext(contextCheck.result, false);
|
|
408
|
+
if (args.fix) printFixLine(getContextFix(), contextCheck.result.suggestions.length);
|
|
357
409
|
console.log(verdictLine(contextCheck.passed, contextCheck.verdict));
|
|
358
410
|
if (pathCheck) {
|
|
359
411
|
console.log(section('path'));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Demo build: slow on a cold cache, fast on a warm one.
|
|
2
2
|
// If .cache/out.txt exists the work is already done; otherwise this
|
|
3
|
-
// script simulates
|
|
3
|
+
// script simulates 400 ms of real work and writes the cache file.
|
|
4
|
+
// 400 ms (not 150) so the cold-vs-warm gap dwarfs Node startup time
|
|
5
|
+
// and stays decisive on loaded CI machines.
|
|
4
6
|
import fs from 'node:fs';
|
|
5
7
|
import path from 'node:path';
|
|
6
8
|
|
|
@@ -9,7 +11,7 @@ const out = path.join(cacheDir, 'out.txt');
|
|
|
9
11
|
if (fs.existsSync(out)) {
|
|
10
12
|
console.log('cache hit');
|
|
11
13
|
} else {
|
|
12
|
-
const until = Date.now() +
|
|
14
|
+
const until = Date.now() + 400;
|
|
13
15
|
while (Date.now() < until) { /* simulate work the cache would skip */ }
|
|
14
16
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
15
17
|
fs.writeFileSync(out, 'built\n');
|
package/lib/context.mjs
CHANGED
|
@@ -125,6 +125,56 @@ export function classifyPath(relativePath, size = 0) {
|
|
|
125
125
|
return { suggestion: null };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function patternToRegExp(pattern) {
|
|
129
|
+
let out = '';
|
|
130
|
+
for (const ch of pattern) {
|
|
131
|
+
if (ch === '*') out += '[^/]*';
|
|
132
|
+
else if (ch === '?') out += '[^/]';
|
|
133
|
+
else out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
134
|
+
}
|
|
135
|
+
return new RegExp(`^${out}$`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Does a .aiignore pattern match this repo-relative path? Patterns are
|
|
140
|
+
* the small subset the tool itself writes: `dir/` matches everything
|
|
141
|
+
* under that directory, a pattern with a slash matches the whole path,
|
|
142
|
+
* anything else matches the basename as a glob (`*` and `?` only).
|
|
143
|
+
* Comment, blank and negation (`!`) lines never match: negation is not
|
|
144
|
+
* supported, and silently treating one as an ignore would hide files
|
|
145
|
+
* the user explicitly wanted back.
|
|
146
|
+
*/
|
|
147
|
+
export function matchesAiignore(relativePath, patterns) {
|
|
148
|
+
const normalized = String(relativePath).split(path.sep).join('/').split('\\').join('/');
|
|
149
|
+
const basename = normalized.split('/').pop();
|
|
150
|
+
for (const raw of patterns) {
|
|
151
|
+
const pattern = String(raw).trim();
|
|
152
|
+
if (!pattern || pattern.startsWith('#') || pattern.startsWith('!')) continue;
|
|
153
|
+
if (pattern.endsWith('/')) {
|
|
154
|
+
const dir = pattern.slice(0, -1);
|
|
155
|
+
if (normalized === dir || normalized.startsWith(`${dir}/`)) return true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (pattern.includes('/')) {
|
|
159
|
+
if (patternToRegExp(pattern).test(normalized)) return true;
|
|
160
|
+
} else if (patternToRegExp(pattern).test(basename)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readAiignore(root) {
|
|
168
|
+
try {
|
|
169
|
+
return fs.readFileSync(path.join(root, '.aiignore'), 'utf8')
|
|
170
|
+
.split(/\r?\n/)
|
|
171
|
+
.map((line) => line.trim())
|
|
172
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('!'));
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
128
178
|
function walk(root, dir, out, state) {
|
|
129
179
|
let entries;
|
|
130
180
|
try {
|
|
@@ -137,13 +187,17 @@ function walk(root, dir, out, state) {
|
|
|
137
187
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
138
188
|
for (const entry of entries) {
|
|
139
189
|
const full = path.join(dir, entry.name);
|
|
140
|
-
|
|
190
|
+
// Always report forward slashes, so output (and tests) are identical
|
|
191
|
+
// on Windows, macOS and Linux.
|
|
192
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
141
193
|
if (entry.isDirectory()) {
|
|
142
194
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
195
|
+
if (state.ignore.length > 0 && matchesAiignore(rel, state.ignore)) continue;
|
|
143
196
|
walk(root, full, out, state);
|
|
144
197
|
continue;
|
|
145
198
|
}
|
|
146
199
|
if (!entry.isFile()) continue;
|
|
200
|
+
if (state.ignore.length > 0 && matchesAiignore(rel, state.ignore)) continue;
|
|
147
201
|
if (state.files >= state.maxFiles) {
|
|
148
202
|
state.truncated = true;
|
|
149
203
|
continue;
|
|
@@ -164,7 +218,10 @@ function walk(root, dir, out, state) {
|
|
|
164
218
|
* Scan a directory and describe how much of it is context noise.
|
|
165
219
|
* Synchronous and dependency-free, like the original. `maxFiles` caps the
|
|
166
220
|
* walk; a capped walk reports `truncated: true` instead of silently
|
|
167
|
-
* under-counting.
|
|
221
|
+
* under-counting. Files listed in the repo's `.aiignore` are excluded
|
|
222
|
+
* from the scan entirely — they are what the agent will not be handed,
|
|
223
|
+
* so they are not noise any more — which is also what makes
|
|
224
|
+
* `context --fix` followed by `context` come back clean.
|
|
168
225
|
*/
|
|
169
226
|
export function scanDirectory(root, options = {}) {
|
|
170
227
|
const absolute = path.resolve(root);
|
|
@@ -176,7 +233,8 @@ export function scanDirectory(root, options = {}) {
|
|
|
176
233
|
}
|
|
177
234
|
if (!stat.isDirectory()) throw new Error(`not a directory: ${root}`);
|
|
178
235
|
const maxFiles = options.maxFiles == null ? 10000 : options.maxFiles;
|
|
179
|
-
const
|
|
236
|
+
const ignore = readAiignore(absolute);
|
|
237
|
+
const state = { files: 0, bytes: 0, maxFiles, truncated: false, ignore };
|
|
180
238
|
const files = [];
|
|
181
239
|
walk(absolute, absolute, files, state);
|
|
182
240
|
|
|
@@ -209,3 +267,43 @@ export function scanDirectory(root, options = {}) {
|
|
|
209
267
|
status: statusFor(scored.score),
|
|
210
268
|
};
|
|
211
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Write the scan's .aiignore suggestions into `<root>/.aiignore`.
|
|
273
|
+
* Only appends entries that are not already there (compared after
|
|
274
|
+
* trimming); existing content is never removed or reordered, and no file
|
|
275
|
+
* is created when there is nothing to add. Returns what happened so the
|
|
276
|
+
* CLI can report it: `{ path, added, alreadyPresent, created }`.
|
|
277
|
+
*/
|
|
278
|
+
export function applyAiignore(root, suggestions) {
|
|
279
|
+
const target = path.join(path.resolve(root), '.aiignore');
|
|
280
|
+
const wanted = [...new Set(suggestions)].sort();
|
|
281
|
+
if (wanted.length === 0) {
|
|
282
|
+
return { path: target, added: [], alreadyPresent: [], created: false };
|
|
283
|
+
}
|
|
284
|
+
let content = '';
|
|
285
|
+
let created = false;
|
|
286
|
+
try {
|
|
287
|
+
content = fs.readFileSync(target, 'utf8');
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error.code !== 'ENOENT') throw new Error(`cannot read .aiignore: ${error.message}`);
|
|
290
|
+
created = true;
|
|
291
|
+
}
|
|
292
|
+
const present = new Set(
|
|
293
|
+
content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
|
|
294
|
+
);
|
|
295
|
+
const added = wanted.filter((entry) => !present.has(entry));
|
|
296
|
+
if (added.length === 0) {
|
|
297
|
+
return { path: target, added: [], alreadyPresent: wanted, created: false };
|
|
298
|
+
}
|
|
299
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
300
|
+
let next = content;
|
|
301
|
+
if (next.length > 0 && !next.endsWith('\n')) next += newline;
|
|
302
|
+
next += added.map((entry) => entry + newline).join('');
|
|
303
|
+
try {
|
|
304
|
+
fs.writeFileSync(target, next);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
throw new Error(`cannot write .aiignore: ${error.message}`);
|
|
307
|
+
}
|
|
308
|
+
return { path: target, added, alreadyPresent: wanted.filter((entry) => present.has(entry)), created };
|
|
309
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"WJDdidi-1","bin":{"dev-diagnosis":"bin/dev-diagnosis.js"},"bugs":{"url":"https://github.com/WJDdidi-1/dev-doctor/issues"},"description":"One offline CLI that diagnoses a repo the way four small tools do: context noise for AI agents, missing CLI commands, build-cache value, and flaky tests.","engines":{"node":">=18"},"files":["bin/","lib/","examples/","README.md","LICENSE","CHANGELOG.md"],"homepage":"https://github.com/WJDdidi-1/dev-doctor#readme","keywords":["cli","diagnostics","ai-coding","context","build-cache","flaky-tests","path","developer-tools","offline"],"license":"Apache-2.0","name":"dev-diagnosis-cli","repository":{"type":"git","url":"git+https://github.com/WJDdidi-1/dev-doctor.git"},"scripts":{"example":"node examples/demo.mjs","test":"node --test"},"type":"module","version":"0.1.
|
|
1
|
+
{"author":"WJDdidi-1","bin":{"dev-diagnosis":"bin/dev-diagnosis.js"},"bugs":{"url":"https://github.com/WJDdidi-1/dev-doctor/issues"},"description":"One offline CLI that diagnoses a repo the way four small tools do: context noise for AI agents, missing CLI commands, build-cache value, and flaky tests.","engines":{"node":">=18"},"files":["bin/","lib/","examples/","README.md","LICENSE","CHANGELOG.md"],"homepage":"https://github.com/WJDdidi-1/dev-doctor#readme","keywords":["cli","diagnostics","ai-coding","context","build-cache","flaky-tests","path","developer-tools","offline"],"license":"Apache-2.0","name":"dev-diagnosis-cli","repository":{"type":"git","url":"git+https://github.com/WJDdidi-1/dev-doctor.git"},"scripts":{"example":"node examples/demo.mjs","test":"node --test"},"type":"module","version":"0.1.2"}
|
|
File without changes
|
|
File without changes
|