@delegance/claude-autopilot 1.0.0-alpha.6 → 1.0.0-alpha.8
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 +17 -0
- package/README.md +134 -33
- package/package.json +1 -1
- package/scripts/autoregress.ts +133 -16
- package/src/cli/autoregress-bridge.ts +30 -0
- package/src/cli/hook.ts +79 -0
- package/src/cli/index.ts +24 -1
- package/tests/snapshots/baselines/src-formatters-sarif.json +210 -0
- package/tests/snapshots/baselines/src-snapshots-impact-selector.json +32 -0
- package/tests/snapshots/baselines/src-snapshots-import-scanner.json +21 -0
- package/tests/snapshots/baselines/src-snapshots-serializer.json +39 -0
- package/tests/snapshots/import-map.json +138 -1
- package/tests/snapshots/index.json +14 -1
- package/tests/snapshots/src-formatters-sarif.snap.ts +132 -0
- package/tests/snapshots/src-snapshots-impact-selector.snap.ts +95 -0
- package/tests/snapshots/src-snapshots-import-scanner.snap.ts +126 -0
- package/tests/snapshots/src-snapshots-serializer.snap.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-alpha.8
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`autopilot autoregress`** — `autoregress run|diff|update|generate` now a first-class `autopilot` subcommand (no more raw `npx tsx scripts/autoregress.ts`)
|
|
8
|
+
- **GitHub Actions CI** — `.github/workflows/ci.yml` runs typecheck + tests on every PR; auto-publishes to npm on `v*` tags
|
|
9
|
+
- **README rewrite** — full feature documentation covering all alphas (all commands, config, GitHub Actions, snapshot regression, architecture)
|
|
10
|
+
|
|
11
|
+
## 1.0.0-alpha.7
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`autopilot hook install`** — writes a `pre-push` git hook that runs `autoregress run` before every push; `hook uninstall` removes it; `hook status` shows current state; `--force` overwrites existing hook
|
|
16
|
+
- **`autoregress diff`** — colored snapshot viewer showing line-by-line JSON diffs between current output and baselines; exits 1 if any diffs found (never modifies baselines — use `update` for that)
|
|
17
|
+
- **`autoregress generate --files <list>`** — explicit comma-separated file list bypasses git detection; generates baselines for any src file on demand
|
|
18
|
+
- **Real baselines** — `tests/snapshots/*.snap.ts` + baselines for `serializer.ts`, `import-scanner.ts`, `impact-selector.ts`, and `sarif.ts` — alpha.6 infrastructure now self-testing via snapshots
|
|
19
|
+
|
|
3
20
|
## 1.0.0-alpha.6
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -1,58 +1,159 @@
|
|
|
1
|
-
# claude-autopilot
|
|
1
|
+
# @delegance/claude-autopilot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Automated code review pipeline for Claude Code. Runs static rules, an optional LLM review engine, and impact-aware snapshot regression tests — outputs SARIF for GitHub Code Scanning, inline PR annotations, and a pre-push hook for local enforcement.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @delegance/claude-autopilot@alpha
|
|
9
|
+
```
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Requires Node 22+.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- YAML config (`autopilot.config.yaml`) replaces `.autopilot/stack.md`
|
|
14
|
-
- Unified `Finding` type across validate + review-bot, with separate `TriageRecord[]` / `FixAttempt[]` history
|
|
15
|
-
- Merged static-rules phase with global re-check after autofix
|
|
16
|
-
- `AutopilotError` taxonomy with per-code retry policy
|
|
17
|
-
- `apiVersion` + `getCapabilities()` on every adapter
|
|
18
|
-
- Real tests phase — runs `testCommand` from config, emits critical finding on failure
|
|
19
|
-
- NDJSON event log with secret redaction
|
|
13
|
+
## Quick Start
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
```bash
|
|
16
|
+
# Scaffold config
|
|
17
|
+
npx autopilot init
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `OPENAI_API_KEY` in `.env.local`
|
|
19
|
+
# Run on changed files
|
|
20
|
+
npx autopilot run
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
# Watch mode (re-runs on every file save)
|
|
23
|
+
npx autopilot watch
|
|
24
|
+
|
|
25
|
+
# Install pre-push hook
|
|
26
|
+
npx autopilot hook install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
### `autopilot run`
|
|
32
|
+
|
|
33
|
+
Runs the pipeline on git-changed files vs the base ref.
|
|
28
34
|
|
|
29
35
|
```bash
|
|
30
|
-
|
|
36
|
+
npx autopilot run # diff against HEAD~1
|
|
37
|
+
npx autopilot run --base main # diff against main
|
|
38
|
+
npx autopilot run --files src/foo.ts # explicit file list
|
|
39
|
+
npx autopilot run --format sarif --output results.sarif
|
|
40
|
+
npx autopilot run --dry-run # show what would run, no execution
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `autopilot watch`
|
|
44
|
+
|
|
45
|
+
Debounced re-run on every file save.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx autopilot watch
|
|
49
|
+
npx autopilot watch --debounce 500
|
|
31
50
|
```
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
### `autopilot hook`
|
|
34
53
|
|
|
35
|
-
|
|
54
|
+
Manages a `pre-push` git hook that runs snapshot regression tests before every push.
|
|
36
55
|
|
|
37
56
|
```bash
|
|
38
|
-
npx autopilot #
|
|
57
|
+
npx autopilot hook install # write .git/hooks/pre-push
|
|
58
|
+
npx autopilot hook install --force # overwrite existing
|
|
59
|
+
npx autopilot hook uninstall # remove
|
|
60
|
+
npx autopilot hook status # show installed hook content
|
|
39
61
|
```
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
Works in git worktrees (handles `.git` as a file pointer).
|
|
64
|
+
|
|
65
|
+
### `autopilot autoregress`
|
|
66
|
+
|
|
67
|
+
Impact-aware snapshot regression testing. Only fires tests whose source modules (or one-hop importers) were touched by the current branch.
|
|
42
68
|
|
|
43
69
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
npx
|
|
70
|
+
npx autopilot autoregress run # impact-selected snapshots (default)
|
|
71
|
+
npx autopilot autoregress run --all # all snapshots
|
|
72
|
+
npx autopilot autoregress diff # show JSON diffs vs baselines
|
|
73
|
+
npx autopilot autoregress update # overwrite baselines with current output
|
|
74
|
+
npx autopilot autoregress generate # LLM-generate snapshot tests for changed files
|
|
75
|
+
npx autopilot autoregress generate --files src/foo.ts,src/bar.ts
|
|
47
76
|
```
|
|
48
77
|
|
|
49
|
-
|
|
78
|
+
Requires `OPENAI_API_KEY` for `generate` mode.
|
|
79
|
+
|
|
80
|
+
### `autopilot init`
|
|
81
|
+
|
|
82
|
+
Scaffolds `autopilot.config.yaml` from a preset.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx autopilot init
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Available presets: `nextjs-supabase`, `t3`, `python-fastapi`, `rails-postgres`, `go`.
|
|
89
|
+
|
|
90
|
+
### `autopilot preflight`
|
|
91
|
+
|
|
92
|
+
Checks prerequisites (Node version, `gh` CLI auth, `OPENAI_API_KEY`).
|
|
93
|
+
|
|
94
|
+
## GitHub Actions
|
|
95
|
+
|
|
96
|
+
Add to your workflow:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
- uses: axledbetter/claude-autopilot@v1
|
|
100
|
+
with:
|
|
101
|
+
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Runs the pipeline, uploads SARIF to GitHub Code Scanning, and annotates the PR diff inline.
|
|
105
|
+
|
|
106
|
+
## SARIF Output
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx autopilot run --format sarif --output autopilot.sarif
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Compatible with `github/codeql-action/upload-sarif@v3`.
|
|
113
|
+
|
|
114
|
+
## Config (`autopilot.config.yaml`)
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
preset: nextjs-supabase # inherit a base config
|
|
118
|
+
reviewEngine:
|
|
119
|
+
adapter: codex
|
|
120
|
+
options:
|
|
121
|
+
model: gpt-5.3-codex
|
|
122
|
+
testCommand: npm test
|
|
123
|
+
protect:
|
|
124
|
+
- src/core/**
|
|
125
|
+
- data/deltas/**
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Snapshot Regression Testing
|
|
129
|
+
|
|
130
|
+
After each feature lands, generate behavioral baselines:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx autopilot autoregress generate
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Future PRs automatically fail if covered behavior diverges. The impact selector uses `git merge-base` diff + one-hop import graph expansion so only relevant snapshots run — keeping CI token-efficient.
|
|
137
|
+
|
|
138
|
+
High-impact paths (`src/core/pipeline/**`, `src/adapters/**`, `src/core/findings/**`, `src/core/config/**`) always trigger a full run.
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
Four pluggable adapter points:
|
|
143
|
+
|
|
144
|
+
| Point | Built-in | Purpose |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `review-engine` | `codex` | LLM code review |
|
|
147
|
+
| `vcs-host` | `github` | PR comments + SARIF upload |
|
|
148
|
+
| `migration-runner` | `supabase` | DB migration execution |
|
|
149
|
+
| `review-bot-parser` | `cursor` | Parse review bot comments |
|
|
150
|
+
|
|
151
|
+
## Requirements
|
|
50
152
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
- **beta → 1.0.0:** dogfood + npm publish
|
|
153
|
+
- Node ≥ 22
|
|
154
|
+
- `OPENAI_API_KEY` (optional — review engine and `autoregress generate` only)
|
|
155
|
+
- `gh` CLI authenticated (optional — PR creation / vcs-host adapter)
|
|
55
156
|
|
|
56
157
|
## License
|
|
57
158
|
|
|
58
|
-
MIT
|
|
159
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delegance/claude-autopilot",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Code automation pipeline: spec → plan → implement → validate → PR",
|
|
6
6
|
"keywords": ["claude", "autopilot", "ai", "pipeline", "code-review", "cli"],
|
package/scripts/autoregress.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// scripts/autoregress.ts
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
5
6
|
import { execSync, spawnSync } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { selectSnapshots } from '../src/snapshots/impact-selector.ts';
|
|
@@ -19,6 +20,22 @@ function loadJson<T>(p: string, fallback: T): T {
|
|
|
19
20
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')) as T; } catch { return fallback; }
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export function diffBaselines(baselineJson: string, currentJson: string): string[] {
|
|
24
|
+
if (baselineJson === currentJson) return [];
|
|
25
|
+
const baselineLines = baselineJson.split('\n');
|
|
26
|
+
const currentLines = currentJson.split('\n');
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
const maxLen = Math.max(baselineLines.length, currentLines.length);
|
|
29
|
+
for (let i = 0; i < maxLen; i++) {
|
|
30
|
+
const bLine = baselineLines[i];
|
|
31
|
+
const cLine = currentLines[i];
|
|
32
|
+
if (bLine === cLine) continue;
|
|
33
|
+
if (bLine !== undefined) lines.push(`- ${bLine}`);
|
|
34
|
+
if (cLine !== undefined) lines.push(`+ ${cLine}`);
|
|
35
|
+
}
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
function getChangedFiles(since?: string): string[] | null {
|
|
23
40
|
try {
|
|
24
41
|
const base = since
|
|
@@ -155,12 +172,15 @@ Write a snapshot test file. Requirements:
|
|
|
155
172
|
5. Baseline loading pattern (use slug {slug}):
|
|
156
173
|
const SLUG = '{slug}';
|
|
157
174
|
import { fileURLToPath } from 'node:url';
|
|
175
|
+
import * as path from 'node:path';
|
|
158
176
|
const baselineRaw = process.env.CAPTURE_BASELINE === '1' ? '{}' : fs.readFileSync(fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url)), 'utf8');
|
|
159
177
|
const baseline = JSON.parse(baselineRaw);
|
|
160
178
|
const captured: Record<string, unknown> = {};
|
|
161
179
|
process.on('exit', () => {
|
|
162
180
|
if (process.env.CAPTURE_BASELINE === '1') {
|
|
163
|
-
const p =
|
|
181
|
+
const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
|
|
182
|
+
? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, '{slug}.json')
|
|
183
|
+
: fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url));
|
|
164
184
|
fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
|
|
165
185
|
}
|
|
166
186
|
});
|
|
@@ -175,13 +195,24 @@ async function cmdGenerate(args: string[]): Promise<number> {
|
|
|
175
195
|
|
|
176
196
|
const sinceIdx = args.indexOf('--since');
|
|
177
197
|
const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
const filesIdx = args.indexOf('--files');
|
|
199
|
+
const filesArg = filesIdx >= 0 ? args[filesIdx + 1] : undefined;
|
|
200
|
+
|
|
201
|
+
let srcFiles: string[];
|
|
202
|
+
if (filesArg) {
|
|
203
|
+
srcFiles = filesArg.split(',').map(f => f.trim()).filter(f => f.startsWith('src/') && f.endsWith('.ts'));
|
|
204
|
+
if (srcFiles.length === 0) {
|
|
205
|
+
console.error('[autoregress generate] --files must contain at least one src/*.ts path');
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
const changed = getChangedFiles(since);
|
|
210
|
+
if (!changed) { console.error('[autoregress generate] could not determine changed files'); return 1; }
|
|
211
|
+
srcFiles = changed.filter(f => f.startsWith('src/') && f.endsWith('.ts'));
|
|
212
|
+
if (srcFiles.length === 0) {
|
|
213
|
+
console.log('[autoregress generate] no src/*.ts files changed — nothing to generate');
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
185
216
|
}
|
|
186
217
|
|
|
187
218
|
console.log(`[autoregress generate] generating snapshots for ${srcFiles.length} file(s)`);
|
|
@@ -257,12 +288,98 @@ async function cmdGenerate(args: string[]): Promise<number> {
|
|
|
257
288
|
return 0;
|
|
258
289
|
}
|
|
259
290
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
291
|
+
function cmdDiff(args: string[]): number {
|
|
292
|
+
const runAll = args.includes('--all');
|
|
293
|
+
const snapIdx = args.indexOf('--snapshot');
|
|
294
|
+
const slug = snapIdx >= 0 ? args[snapIdx + 1] : undefined;
|
|
295
|
+
const sinceIdx = args.indexOf('--since');
|
|
296
|
+
const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
|
|
297
|
+
|
|
298
|
+
const index = loadJson<Record<string, string[]>>(INDEX_PATH, {});
|
|
299
|
+
const importMap = loadJson<Record<string, string[]>>(IMPORT_MAP_PATH, {});
|
|
300
|
+
const snapFiles = slug
|
|
301
|
+
? [path.join('tests', 'snapshots', `${slug}.snap.ts`)]
|
|
302
|
+
: allSnapFiles();
|
|
303
|
+
|
|
304
|
+
let selected: string[];
|
|
305
|
+
if (runAll || slug || snapFiles.length === 0) {
|
|
306
|
+
selected = snapFiles;
|
|
307
|
+
} else {
|
|
308
|
+
const changed = getChangedFiles(since);
|
|
309
|
+
selected = changed ? selectSnapshots(changed, snapFiles, index, importMap).selected : snapFiles;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (selected.length === 0) {
|
|
313
|
+
console.log('[autoregress diff] no snapshots to diff');
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
318
|
+
const red = useColor ? '\x1b[31m' : '';
|
|
319
|
+
const green = useColor ? '\x1b[32m' : '';
|
|
320
|
+
const reset = useColor ? '\x1b[0m' : '';
|
|
321
|
+
|
|
322
|
+
let changedCount = 0;
|
|
323
|
+
for (const snap of selected) {
|
|
324
|
+
const slug_ = path.basename(snap, '.snap.ts');
|
|
325
|
+
const baselinePath = path.join(BASELINES_DIR, `${slug_}.json`);
|
|
326
|
+
|
|
327
|
+
if (!fs.existsSync(baselinePath)) {
|
|
328
|
+
console.log(` ${snap} — ${red}no baseline${reset}`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const tmpBaselinesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ar-diff-'));
|
|
333
|
+
const tmpBaselinePath = path.join(tmpBaselinesDir, `${slug_}.json`);
|
|
334
|
+
fs.copyFileSync(baselinePath, tmpBaselinePath);
|
|
335
|
+
|
|
336
|
+
const absSnap = path.join(ROOT, snap);
|
|
337
|
+
const captureResult = spawnSync('node', ['--test', '--import', 'tsx', absSnap], {
|
|
338
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
339
|
+
cwd: ROOT,
|
|
340
|
+
env: { ...process.env, CAPTURE_BASELINE: '1', AUTOREGRESS_TEMP_BASELINE_DIR: tmpBaselinesDir },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const baselineJson = fs.readFileSync(baselinePath, 'utf8');
|
|
344
|
+
const captureOk = fs.existsSync(tmpBaselinePath);
|
|
345
|
+
const currentJson = captureOk ? fs.readFileSync(tmpBaselinePath, 'utf8') : null;
|
|
346
|
+
if (!captureOk && captureResult.status !== 0) {
|
|
347
|
+
const stderr = captureResult.stderr?.toString().trim();
|
|
348
|
+
if (stderr) console.error(` ${stderr.slice(0, 120)}`);
|
|
349
|
+
}
|
|
350
|
+
fs.rmSync(tmpBaselinesDir, { recursive: true, force: true });
|
|
351
|
+
|
|
352
|
+
if (!currentJson) {
|
|
353
|
+
console.log(` ${snap} — ${red}capture failed${reset}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const diffLines = diffBaselines(baselineJson, currentJson);
|
|
358
|
+
if (diffLines.length === 0) {
|
|
359
|
+
console.log(` ${snap} — ${green}✓ no changes${reset}`);
|
|
360
|
+
} else {
|
|
361
|
+
changedCount++;
|
|
362
|
+
console.log(` ${snap}`);
|
|
363
|
+
for (const line of diffLines) {
|
|
364
|
+
if (line.startsWith('-')) console.log(` ${red}${line}${reset}`);
|
|
365
|
+
else if (line.startsWith('+')) console.log(` ${green}${line}${reset}`);
|
|
366
|
+
else console.log(` ${line}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return changedCount > 0 ? 1 : 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
|
|
375
|
+
const [,, subcmd, ...rest] = process.argv;
|
|
376
|
+
switch (subcmd) {
|
|
377
|
+
case 'run': process.exit(cmdRun(rest)); break;
|
|
378
|
+
case 'update': process.exit(cmdUpdate(rest)); break;
|
|
379
|
+
case 'generate': process.exit(await cmdGenerate(rest)); break;
|
|
380
|
+
case 'diff': process.exit(cmdDiff(rest)); break;
|
|
381
|
+
default:
|
|
382
|
+
console.error(`[autoregress] unknown subcommand: ${subcmd ?? '(none)'}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
268
385
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/cli/autoregress-bridge.ts
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const SCRIPT = path.resolve(__dirname, '../../scripts/autoregress.ts');
|
|
8
|
+
|
|
9
|
+
const VALID_MODES = ['run', 'update', 'generate', 'diff'];
|
|
10
|
+
|
|
11
|
+
export function buildAutoregressArgs(args: string[]): string[] {
|
|
12
|
+
const mode = args[0] && VALID_MODES.includes(args[0]) ? args[0] : 'run';
|
|
13
|
+
const rest = args[0] && VALID_MODES.includes(args[0]) ? args.slice(1) : args;
|
|
14
|
+
return [mode, ...rest];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function runAutoregress(args: string[]): number {
|
|
18
|
+
const resolvedArgs = buildAutoregressArgs(args);
|
|
19
|
+
const result = spawnSync(
|
|
20
|
+
process.execPath,
|
|
21
|
+
['--import', 'tsx', SCRIPT, ...resolvedArgs],
|
|
22
|
+
{ stdio: 'inherit', cwd: process.cwd() },
|
|
23
|
+
);
|
|
24
|
+
if (result.error) {
|
|
25
|
+
console.error(`[autoregress] failed to launch: ${result.error.message}`);
|
|
26
|
+
console.error(` script: ${SCRIPT}`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
return result.status ?? 1;
|
|
30
|
+
}
|
package/src/cli/hook.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const HOOK_CONTENT = `#!/bin/sh
|
|
5
|
+
# autopilot pre-push hook — runs impact-selected snapshots before push
|
|
6
|
+
npx tsx scripts/autoregress.ts run
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
function findGitDir(cwd: string): string | null {
|
|
10
|
+
let dir = path.resolve(cwd);
|
|
11
|
+
for (let i = 0; i < 10; i++) {
|
|
12
|
+
const candidate = path.join(dir, '.git');
|
|
13
|
+
if (fs.existsSync(candidate)) {
|
|
14
|
+
const stat = fs.statSync(candidate);
|
|
15
|
+
if (stat.isDirectory()) return candidate;
|
|
16
|
+
if (stat.isFile()) {
|
|
17
|
+
const content = fs.readFileSync(candidate, 'utf8');
|
|
18
|
+
const match = content.match(/^gitdir:\s*(.+)/m);
|
|
19
|
+
if (match) return path.resolve(dir, match[1]!.trim());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) return null;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runHook(
|
|
30
|
+
sub: string,
|
|
31
|
+
options: { cwd?: string; force?: boolean } = {},
|
|
32
|
+
): Promise<number> {
|
|
33
|
+
const cwd = options.cwd ?? process.cwd();
|
|
34
|
+
const gitDir = findGitDir(cwd);
|
|
35
|
+
|
|
36
|
+
if (!gitDir) {
|
|
37
|
+
console.error('[hook] not inside a git repository');
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hookPath = path.join(gitDir, 'hooks', 'pre-push');
|
|
42
|
+
|
|
43
|
+
switch (sub) {
|
|
44
|
+
case 'install': {
|
|
45
|
+
if (fs.existsSync(hookPath) && !options.force) {
|
|
46
|
+
console.error(`[hook] pre-push hook already exists at ${hookPath}`);
|
|
47
|
+
console.error(' Use --force to overwrite.');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf8');
|
|
52
|
+
fs.chmodSync(hookPath, 0o755);
|
|
53
|
+
console.log(`[hook] installed pre-push hook at ${hookPath}`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
case 'uninstall': {
|
|
57
|
+
if (!fs.existsSync(hookPath)) {
|
|
58
|
+
console.log('[hook] no pre-push hook installed');
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
fs.rmSync(hookPath);
|
|
62
|
+
console.log(`[hook] removed ${hookPath}`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
case 'status': {
|
|
66
|
+
if (fs.existsSync(hookPath)) {
|
|
67
|
+
console.log(`[hook] installed at ${hookPath}`);
|
|
68
|
+
console.log(fs.readFileSync(hookPath, 'utf8'));
|
|
69
|
+
} else {
|
|
70
|
+
console.log('[hook] not installed');
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
default:
|
|
75
|
+
console.error(`[hook] unknown subcommand: ${sub}`);
|
|
76
|
+
console.error('Usage: autopilot hook <install|uninstall|status> [--force]');
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { runWatch } from './watch.ts';
|
|
|
16
16
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
|
|
19
|
-
const SUBCOMMANDS = ['init', 'run', 'preflight', 'help', '--help', '-h'] as const;
|
|
19
|
+
const SUBCOMMANDS = ['init', 'run', 'watch', 'hook', 'autoregress', 'preflight', 'help', '--help', '-h'] as const;
|
|
20
20
|
const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce'];
|
|
21
21
|
|
|
22
22
|
// Detect first non-flag arg as subcommand, default to 'run'
|
|
@@ -47,6 +47,7 @@ Commands:
|
|
|
47
47
|
watch Watch for file changes and re-run pipeline on each save
|
|
48
48
|
init Scaffold autopilot.config.yaml from a preset
|
|
49
49
|
preflight Check prerequisites
|
|
50
|
+
autoregress Run snapshot regression tests (run|diff|update|generate)
|
|
50
51
|
|
|
51
52
|
Options (run):
|
|
52
53
|
--base <ref> Git base ref for diff (default: HEAD~1)
|
|
@@ -59,6 +60,12 @@ Options (run):
|
|
|
59
60
|
Options (watch):
|
|
60
61
|
--config <path> Path to config file (default: ./autopilot.config.yaml)
|
|
61
62
|
--debounce <ms> Debounce delay in ms (default: 300)
|
|
63
|
+
|
|
64
|
+
Options (autoregress):
|
|
65
|
+
--all Run/diff all snapshots
|
|
66
|
+
--since <ref> Git ref for changed-files detection
|
|
67
|
+
--snapshot <slug> Target a single snapshot
|
|
68
|
+
--files <a,b,c> Explicit file list for generate (skips git detection)
|
|
62
69
|
`);
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -118,6 +125,22 @@ switch (subcommand) {
|
|
|
118
125
|
break;
|
|
119
126
|
}
|
|
120
127
|
|
|
128
|
+
case 'hook': {
|
|
129
|
+
const { runHook } = await import('./hook.ts');
|
|
130
|
+
const hookSub = args[1] ?? 'status';
|
|
131
|
+
const force = boolFlag('force');
|
|
132
|
+
const code = await runHook(hookSub, { force });
|
|
133
|
+
process.exit(code);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'autoregress': {
|
|
138
|
+
const { runAutoregress } = await import('./autoregress-bridge.ts');
|
|
139
|
+
const code = runAutoregress(args.slice(1));
|
|
140
|
+
process.exit(code);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
121
144
|
default:
|
|
122
145
|
console.error(`\x1b[31m[autopilot] Unknown subcommand: "${subcommand}"\x1b[0m`);
|
|
123
146
|
printUsage();
|