readfirst 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/cli.mjs +51 -3
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ Commit `.readfirst/map.json`. From then on:
|
|
|
24
24
|
npx readfirst read <file>... # before changing files
|
|
25
25
|
npx readfirst diff # everything your branch changed since main
|
|
26
26
|
npx readfirst diff --json # the same, for tools and agents
|
|
27
|
+
npx readfirst verify # run the checks the map lists for everything your branch changed (exit 1 if one fails)
|
|
27
28
|
npx readfirst fix # after moving code: place new files, drop patterns for deleted ones
|
|
28
29
|
```
|
|
29
30
|
|
|
@@ -33,7 +34,8 @@ npx readfirst fix # after moving code: place new files, drop patt
|
|
|
33
34
|
|
|
34
35
|
```md
|
|
35
36
|
Before changing a file, run `npx readfirst read <file>` and follow what it says: keep the rules, read the risks, run the
|
|
36
|
-
checks it lists. Before you finish, run `npx readfirst
|
|
37
|
+
checks it lists. Before you finish, run `npx readfirst verify`: it runs the checks the map lists for everything you
|
|
38
|
+
changed; fix what fails.
|
|
37
39
|
When you learn something a newcomer would get wrong, add it as a risk in .readfirst/map.json.
|
|
38
40
|
```
|
|
39
41
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
// readfirst fix place new files and drop patterns for deleted ones, then check
|
|
7
7
|
// readfirst read <file>... the read list for files you are about to change
|
|
8
8
|
// readfirst diff [--base <ref>] the read list for the files changed since <ref> (default: origin/main, else main)
|
|
9
|
+
// readfirst verify [--base <ref>] run the checks the read list names for the changed files; exit 1 if one fails
|
|
9
10
|
// readfirst hook [--install] a Claude Code PreToolUse hook: the read list for the file an Edit or Write is about to change
|
|
10
11
|
// readfirst view [--out <file>] the map as a city in one HTML page (default .readfirst/local/view.html)
|
|
11
12
|
// Options: --root <dir> (default: the current git repository), --json, --limit <n>.
|
|
12
|
-
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
15
|
import { tmpdir } from 'node:os';
|
|
15
16
|
import { basename, isAbsolute, join, relative } from 'node:path';
|
|
@@ -29,11 +30,12 @@ const HELP = `readfirst — what to read and check before changing a file
|
|
|
29
30
|
readfirst fix place new files, drop patterns for deleted ones
|
|
30
31
|
readfirst read <file>... read list for files you are about to change
|
|
31
32
|
readfirst diff [--base <ref>] read list for files changed since <ref>
|
|
33
|
+
readfirst verify [--base <ref>] run the checks for the files changed since <ref> (--dry-run lists them)
|
|
32
34
|
readfirst hook [--install] Claude Code hook: the read list before each edit (--install adds it to .claude/settings.json)
|
|
33
35
|
readfirst view [--out <file>] the map as a city in one HTML page
|
|
34
36
|
readfirst prompt --labels <lang> brief for your agent to write personal labels for the view (e.g. ko, en)
|
|
35
37
|
|
|
36
|
-
Options: --root <dir>, --json, --limit <n>, --force (init: replace the map),
|
|
38
|
+
Options: --root <dir>, --json, --limit <n>, --timeout <s> (verify, per check; default 900), --force (init: replace the map),
|
|
37
39
|
--agent (init: add the read-first line to CLAUDE.md / AGENTS.md / .cursorrules)`;
|
|
38
40
|
|
|
39
41
|
function parse(argv) {
|
|
@@ -44,6 +46,7 @@ function parse(argv) {
|
|
|
44
46
|
else if (a === '--force') args.force = true;
|
|
45
47
|
else if (a === '--agent') args.agent = true;
|
|
46
48
|
else if (a === '--install') args.install = true;
|
|
49
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
47
50
|
else if (a === '-h' || a === '--help') args.help = true;
|
|
48
51
|
else if (a.startsWith('--')) { const v = argv[++i]; if (v === undefined) throw new Error(`${a} needs a value`); args[a.slice(2)] = v; }
|
|
49
52
|
else args._.push(a);
|
|
@@ -87,7 +90,7 @@ function loadMapLenient(root, files, io) {
|
|
|
87
90
|
|
|
88
91
|
// The one line that makes a coding agent use readfirst, added to the agent notes the project already has (CLAUDE.md,
|
|
89
92
|
// AGENTS.md, .cursorrules), or to a new AGENTS.md when it has none. A file that already mentions readfirst is left alone.
|
|
90
|
-
export const AGENT_LINE = 'Before changing a file, run `npx readfirst read <file>` and follow what it says: keep the rules, read the risks, run the checks it lists. Before you finish, run `npx readfirst
|
|
93
|
+
export const AGENT_LINE = 'Before changing a file, run `npx readfirst read <file>` and follow what it says: keep the rules, read the risks, run the checks it lists. Before you finish, run `npx readfirst verify`: it runs the checks the map lists for everything you changed; fix what fails. When you learn something a newcomer would get wrong, add it as a risk in .readfirst/map.json.';
|
|
91
94
|
function addAgentLine(root) {
|
|
92
95
|
const notes = ['CLAUDE.md', 'AGENTS.md', '.cursorrules'].filter((f) => existsSync(join(root, f)));
|
|
93
96
|
const wrote = [];
|
|
@@ -112,6 +115,37 @@ function changedFiles(root, base) {
|
|
|
112
115
|
return { ref, files: [...names].sort() };
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
// Runs each check with the shell in the repository root, one after another, and keeps the end of the output of the ones
|
|
119
|
+
// that fail. readfirst writes nothing itself; the commands are the map's own and may write, as they would by hand.
|
|
120
|
+
export function verifyChecks(root, checks, { dryRun = false, timeout = 900, progress = () => {} } = {}) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const c of checks) {
|
|
123
|
+
const row = { run: c.run, why: c.why, for: c.for ?? [] };
|
|
124
|
+
if (dryRun) { out.push({ ...row, status: 'listed' }); continue; }
|
|
125
|
+
progress(`running ${c.run}\n`);
|
|
126
|
+
const started = Date.now();
|
|
127
|
+
const r = spawnSync(c.run, { cwd: root, shell: true, encoding: 'utf8', timeout: timeout * 1000, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, CI: process.env.CI ?? '1' } });
|
|
128
|
+
const seconds = Math.round((Date.now() - started) / 100) / 10;
|
|
129
|
+
const timedOut = r.error?.code === 'ETIMEDOUT';
|
|
130
|
+
const status = timedOut ? 'timeout' : r.status === 0 ? 'pass' : 'fail';
|
|
131
|
+
const tail = status === 'pass' ? undefined : `${r.stdout ?? ''}${r.stderr ?? ''}${r.error && !timedOut ? r.error.message : ''}`.trimEnd().split('\n').slice(-30).join('\n');
|
|
132
|
+
out.push({ ...row, status, code: r.status, seconds, ...(tail !== undefined ? { output: tail } : {}) });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function verifyText(report, dryRun) {
|
|
138
|
+
if (!report.files.length) return `no files changed since ${report.base}: nothing to verify.\n`;
|
|
139
|
+
if (!report.checks.length) return `${report.files.length} changed file(s) since ${report.base}; the map lists no checks for them.\n`;
|
|
140
|
+
if (dryRun) return `checks for ${report.files.length} changed file(s) since ${report.base}:\n${report.checks.map((c) => `- ${c.run}${c.why ? ` — ${c.why}` : ''}`).join('\n')}\n`;
|
|
141
|
+
const mark = { pass: 'pass', fail: 'FAIL', timeout: 'TIMEOUT' };
|
|
142
|
+
let s = `\nverify: ${report.checks.length} check(s) for ${report.files.length} changed file(s) since ${report.base}\n`;
|
|
143
|
+
for (const c of report.checks) s += `${mark[c.status]} ${c.run} (${c.seconds}s)\n`;
|
|
144
|
+
for (const c of report.checks.filter((x) => x.status !== 'pass')) s += `\n--- ${c.run} (${c.status}${c.code != null ? `, exit ${c.code}` : ''}); last lines:\n${c.output}\n`;
|
|
145
|
+
s += report.ok ? '\nall checks passed.\n' : `\n${report.checks.filter((c) => c.status !== 'pass').length} check(s) failed.\n`;
|
|
146
|
+
return s;
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
// The read list as a few factual lines for an agent's context: rules, risks and checks, nothing else.
|
|
116
150
|
export function brief(list) {
|
|
117
151
|
const lines = [];
|
|
@@ -258,5 +292,19 @@ export function main(argv, io = { out: (s) => process.stdout.write(s), err: (s)
|
|
|
258
292
|
io.out(args.json ? `${JSON.stringify(list, null, 2)}\n` : `${toMarkdown(list)}\n`);
|
|
259
293
|
return 0;
|
|
260
294
|
}
|
|
295
|
+
if (cmd === 'verify') {
|
|
296
|
+
const map = loadMapLenient(root, files, io);
|
|
297
|
+
const changed = changedFiles(root, args.base);
|
|
298
|
+
const list = changed.files.length
|
|
299
|
+
? readList({ map, files, imports: importEdges(files, readText(root)), targets: changed.files, commit, limit: 1 })
|
|
300
|
+
: emptyList(commit);
|
|
301
|
+
const timeout = Number(args.timeout ?? 900);
|
|
302
|
+
if (!(timeout > 0)) throw new Error('--timeout needs a number of seconds above 0');
|
|
303
|
+
const result = verifyChecks(root, list.checks, { dryRun: args.dryRun, timeout, progress: args.json ? () => {} : io.out });
|
|
304
|
+
const report = { format: 'readfirst/verify@1', commit, base: changed.ref, files: changed.files, ok: result.every((c) => c.status === 'pass' || c.status === 'listed'), checks: result };
|
|
305
|
+
if (args.json) io.out(`${JSON.stringify(report, null, 2)}\n`);
|
|
306
|
+
else io.out(verifyText(report, args.dryRun));
|
|
307
|
+
return report.ok ? 0 : 1;
|
|
308
|
+
}
|
|
261
309
|
throw new Error(`unknown command ${cmd}\n\n${HELP}`);
|
|
262
310
|
}
|