@delegance/claude-autopilot 1.1.0 → 1.2.1
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 +7 -0
- package/README.md +10 -2
- package/bin/autopilot.js +11 -6
- package/package.json +1 -1
- package/src/cli/detector.ts +5 -1
- package/src/cli/index.ts +9 -5
- package/src/cli/preflight.ts +134 -118
- package/src/cli/setup.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] — 2026-04-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `autopilot doctor` — prerequisite checker: verifies Node 22+, tsx, gh CLI auth, claude CLI, OPENAI_API_KEY, git config, superpowers plugin; shows exact fix command for each failure; exits 1 if any blockers
|
|
7
|
+
- `autopilot setup` now runs `doctor` automatically at the end so users immediately see what still needs attention
|
|
8
|
+
- `autopilot preflight` kept as alias for `doctor`
|
|
9
|
+
|
|
3
10
|
## [1.1.0] — 2026-04-21
|
|
4
11
|
|
|
5
12
|
### Added
|
package/README.md
CHANGED
|
@@ -94,9 +94,17 @@ npx autopilot init
|
|
|
94
94
|
|
|
95
95
|
Available presets: `nextjs-supabase`, `t3`, `python-fastapi`, `rails-postgres`, `go`.
|
|
96
96
|
|
|
97
|
-
### `autopilot
|
|
97
|
+
### `autopilot doctor`
|
|
98
98
|
|
|
99
|
-
Checks prerequisites
|
|
99
|
+
Checks prerequisites and shows exact fix commands for each failure.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx autopilot doctor # Check prerequisites and show exact fix commands
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Verifies: Node 22+, tsx, gh CLI auth, claude CLI, OPENAI_API_KEY, git user config, superpowers plugin. Exits 1 if any blockers are found. Also runs automatically at the end of `autopilot setup`.
|
|
106
|
+
|
|
107
|
+
`autopilot preflight` is kept as an alias for `doctor`.
|
|
100
108
|
|
|
101
109
|
## GitHub Actions
|
|
102
110
|
|
package/bin/autopilot.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Thin launcher that uses tsx to run the TypeScript CLI entry point.
|
|
3
|
-
// This is what `npx autopilot` resolves to — it hands off to tsx so TypeScript
|
|
4
|
-
// source can execute without a separate build step during alpha.
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
2
|
import { fileURLToPath } from 'node:url';
|
|
7
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
8
5
|
import * as path from 'node:path';
|
|
9
6
|
|
|
10
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const tsxBin = path.resolve(__dirname, '..', 'node_modules', '.bin', 'tsx');
|
|
12
8
|
const entrypoint = path.resolve(__dirname, '..', 'src', 'cli', 'index.ts');
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
// Locate tsx: own node_modules (dev) → consumer's node_modules/.bin → PATH
|
|
11
|
+
function findTsx() {
|
|
12
|
+
const own = path.resolve(__dirname, '..', 'node_modules', '.bin', 'tsx');
|
|
13
|
+
if (fs.existsSync(own)) return own;
|
|
14
|
+
const consumer = path.resolve(__dirname, '..', '..', '..', '.bin', 'tsx');
|
|
15
|
+
if (fs.existsSync(consumer)) return consumer;
|
|
16
|
+
return 'tsx';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = spawnSync(findTsx(), [entrypoint, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
15
20
|
process.exit(result.status ?? 1);
|
package/package.json
CHANGED
package/src/cli/detector.ts
CHANGED
|
@@ -24,10 +24,14 @@ function fileContains(filePath: string, needle: string): boolean {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const DEFAULT_TEST_PLACEHOLDER = /^echo .error: no test specified/i;
|
|
28
|
+
|
|
27
29
|
function nodeTestCommand(cwd: string): string {
|
|
28
30
|
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
29
31
|
const scripts = pkg?.['scripts'] as Record<string, string> | undefined;
|
|
30
|
-
|
|
32
|
+
const cmd = scripts?.['test'];
|
|
33
|
+
if (!cmd || DEFAULT_TEST_PLACEHOLDER.test(cmd)) return 'npm test';
|
|
34
|
+
return cmd;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export function detectProject(cwd: string): DetectionResult {
|
package/src/cli/index.ts
CHANGED
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
* autopilot run --base main diff against a specific branch
|
|
9
9
|
* autopilot run --dry-run show what would run, no execution
|
|
10
10
|
* autopilot watch re-run pipeline on every file save (debounced)
|
|
11
|
-
* autopilot
|
|
11
|
+
* autopilot doctor check prerequisites (alias: preflight)
|
|
12
12
|
*/
|
|
13
13
|
import { runInit } from './init.ts';
|
|
14
14
|
import { runCommand } from './run.ts';
|
|
15
15
|
import { runWatch } from './watch.ts';
|
|
16
16
|
import { runSetup } from './setup.ts';
|
|
17
|
+
import { runDoctor } from './preflight.ts';
|
|
17
18
|
|
|
18
19
|
const args = process.argv.slice(2);
|
|
19
20
|
|
|
20
|
-
const SUBCOMMANDS = ['init', 'run', 'watch', 'hook', 'autoregress', 'preflight', 'setup', 'help', '--help', '-h'] as const;
|
|
21
|
+
const SUBCOMMANDS = ['init', 'run', 'watch', 'hook', 'autoregress', 'doctor', 'preflight', 'setup', 'help', '--help', '-h'] as const;
|
|
21
22
|
const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce'];
|
|
22
23
|
|
|
23
24
|
// Detect first non-flag arg as subcommand, default to 'run'
|
|
@@ -47,7 +48,7 @@ Commands:
|
|
|
47
48
|
run Run the pipeline on git-changed files (default)
|
|
48
49
|
watch Watch for file changes and re-run pipeline on each save
|
|
49
50
|
init Scaffold autopilot.config.yaml from a preset
|
|
50
|
-
|
|
51
|
+
doctor Check prerequisites and show exact fix commands (alias: preflight)
|
|
51
52
|
autoregress Run snapshot regression tests (run|diff|update|generate)
|
|
52
53
|
|
|
53
54
|
Options (run):
|
|
@@ -75,9 +76,12 @@ switch (subcommand) {
|
|
|
75
76
|
await runInit(process.cwd());
|
|
76
77
|
break;
|
|
77
78
|
|
|
78
|
-
case '
|
|
79
|
-
|
|
79
|
+
case 'doctor':
|
|
80
|
+
case 'preflight': {
|
|
81
|
+
const result = await runDoctor();
|
|
82
|
+
process.exit(result.blockers > 0 ? 1 : 0);
|
|
80
83
|
break;
|
|
84
|
+
}
|
|
81
85
|
|
|
82
86
|
case 'help':
|
|
83
87
|
case '--help':
|
package/src/cli/preflight.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { runSafe } from '../core/shell.ts';
|
|
5
6
|
|
|
6
7
|
const PASS = '\x1b[32m✓\x1b[0m';
|
|
@@ -30,125 +31,140 @@ function loadEnvFile(filePath: string): Record<string, string> {
|
|
|
30
31
|
return vars;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
34
|
+
export interface DoctorResult {
|
|
35
|
+
blockers: number;
|
|
36
|
+
warnings: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runDoctor(): Promise<DoctorResult> {
|
|
40
|
+
const checks: Check[] = [];
|
|
41
|
+
|
|
42
|
+
// 1. Node version
|
|
43
|
+
const nodeVersion = process.version;
|
|
44
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]!, 10);
|
|
45
|
+
checks.push({
|
|
46
|
+
name: `Node.js ${nodeVersion}`,
|
|
47
|
+
result: nodeMajor >= 22 ? 'pass' : 'fail',
|
|
48
|
+
message: nodeMajor < 22 ? `Node 22+ required — current: ${nodeVersion}. Install via nvm: nvm install 22` : undefined,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 2. tsx available
|
|
52
|
+
const localTsx = path.join(process.cwd(), 'node_modules', '.bin', 'tsx');
|
|
53
|
+
const tsxVersion = fs.existsSync(localTsx)
|
|
54
|
+
? runSafe(localTsx, ['--version'])
|
|
55
|
+
: runSafe('tsx', ['--version']);
|
|
56
|
+
checks.push({
|
|
57
|
+
name: 'tsx available',
|
|
58
|
+
result: tsxVersion ? 'pass' : 'fail',
|
|
59
|
+
message: !tsxVersion ? 'tsx not found — run: npm install @delegance/claude-autopilot (includes tsx)' : undefined,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 3. gh CLI authenticated
|
|
63
|
+
const ghAuth = runSafe('gh', ['auth', 'status']);
|
|
64
|
+
checks.push({
|
|
65
|
+
name: 'gh CLI authenticated',
|
|
66
|
+
result: ghAuth !== null ? 'pass' : 'fail',
|
|
67
|
+
message: ghAuth === null ? 'gh CLI not authenticated — run: gh auth login' : undefined,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 4. autopilot.config.yaml in cwd
|
|
71
|
+
const configYaml = path.join(process.cwd(), 'autopilot.config.yaml');
|
|
72
|
+
checks.push({
|
|
73
|
+
name: 'autopilot.config.yaml',
|
|
74
|
+
result: fs.existsSync(configYaml) ? 'pass' : 'warn',
|
|
75
|
+
message: !fs.existsSync(configYaml)
|
|
76
|
+
? 'autopilot.config.yaml not found in current directory — copy from a preset: presets/nextjs-supabase/autopilot.config.yaml'
|
|
77
|
+
: undefined,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 5. Local env file exists
|
|
81
|
+
const envFile = ENV_CANDIDATES.find(f => fs.existsSync(f));
|
|
82
|
+
checks.push({
|
|
83
|
+
name: `Local env file (${envFile ?? 'none found'})`,
|
|
84
|
+
result: envFile ? 'pass' : 'warn',
|
|
85
|
+
message: !envFile
|
|
86
|
+
? `No env file found. Looked for: ${ENV_CANDIDATES.join(', ')}. Create one with your OPENAI_API_KEY.`
|
|
87
|
+
: undefined,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 6. OPENAI_API_KEY set
|
|
91
|
+
const envVars = envFile ? loadEnvFile(envFile) : {};
|
|
92
|
+
const hasOpenAI = !!process.env.OPENAI_API_KEY || !!envVars['OPENAI_API_KEY'];
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'OPENAI_API_KEY',
|
|
95
|
+
result: hasOpenAI ? 'pass' : 'warn',
|
|
96
|
+
message: !hasOpenAI
|
|
97
|
+
? `OPENAI_API_KEY not set — Codex review steps will be skipped`
|
|
98
|
+
: undefined,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 7. claude CLI available
|
|
102
|
+
const claudeVersion = runSafe('claude', ['--version']);
|
|
103
|
+
checks.push({
|
|
104
|
+
name: 'claude CLI',
|
|
105
|
+
result: claudeVersion ? 'pass' : 'fail',
|
|
106
|
+
message: !claudeVersion
|
|
107
|
+
? 'claude CLI not found — required for autofix. Install Claude Code: https://claude.ai/claude-code'
|
|
108
|
+
: undefined,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 8. git user config
|
|
112
|
+
const gitName = runSafe('git', ['config', 'user.name']);
|
|
113
|
+
const gitEmail = runSafe('git', ['config', 'user.email']);
|
|
114
|
+
const gitConfigOk = !!(gitName?.trim()) && !!(gitEmail?.trim());
|
|
115
|
+
checks.push({
|
|
116
|
+
name: 'git user config',
|
|
117
|
+
result: gitConfigOk ? 'pass' : 'warn',
|
|
118
|
+
message: !gitConfigOk
|
|
119
|
+
? 'git user.name / user.email not set — commits will fail.'
|
|
120
|
+
: undefined,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 9. superpowers plugin
|
|
124
|
+
const home = process.env.HOME ?? '';
|
|
125
|
+
const superpowersPaths = [
|
|
126
|
+
path.join(home, '.claude', 'plugins', 'cache', 'claude-plugins-official', 'superpowers'),
|
|
127
|
+
path.join(home, '.claude', 'plugins', 'cache', 'superpowers-marketplace', 'superpowers'),
|
|
128
|
+
path.join(home, '.claude', 'plugins', 'superpowers'),
|
|
129
|
+
];
|
|
130
|
+
const superpowersOk = superpowersPaths.some(p => fs.existsSync(p));
|
|
131
|
+
checks.push({
|
|
132
|
+
name: 'superpowers plugin',
|
|
133
|
+
result: superpowersOk ? 'pass' : 'warn',
|
|
134
|
+
message: !superpowersOk
|
|
135
|
+
? 'superpowers plugin not detected — install: /plugin install superpowers@claude-plugins-official'
|
|
136
|
+
: undefined,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Print results
|
|
140
|
+
console.log('\n\x1b[1m[doctor] Autopilot prerequisite check\x1b[0m\n');
|
|
141
|
+
let blockers = 0;
|
|
142
|
+
let warnings = 0;
|
|
143
|
+
for (const check of checks) {
|
|
144
|
+
const icon = check.result === 'pass' ? PASS : check.result === 'warn' ? WARN : FAIL;
|
|
145
|
+
console.log(` ${icon} ${check.name}`);
|
|
146
|
+
if (check.message) {
|
|
147
|
+
console.log(` \x1b[2m${check.message}\x1b[0m`);
|
|
148
|
+
}
|
|
149
|
+
if (check.result === 'fail') blockers++;
|
|
150
|
+
if (check.result === 'warn') warnings++;
|
|
141
151
|
}
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
|
|
153
|
+
console.log('');
|
|
154
|
+
if (blockers > 0) {
|
|
155
|
+
console.log(`\x1b[31m[doctor] ${blockers} blocker(s) — fix before running npx autopilot run\x1b[0m\n`);
|
|
156
|
+
} else if (warnings > 0) {
|
|
157
|
+
console.log(`\x1b[33m[doctor] ${warnings} warning(s) — pipeline will run but some steps may be skipped\x1b[0m\n`);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`\x1b[32m[doctor] All checks passed — ready to run\x1b[0m\n`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { blockers, warnings };
|
|
144
163
|
}
|
|
145
164
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log(`\x1b[33m[preflight] ${warnings} warning(s) — pipeline will run but some steps may be degraded\x1b[0m\n`);
|
|
152
|
-
} else {
|
|
153
|
-
console.log(`\x1b[32m[preflight] All checks passed\x1b[0m\n`);
|
|
165
|
+
// Run when invoked directly
|
|
166
|
+
const isMain = process.argv[1] !== undefined &&
|
|
167
|
+
fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
168
|
+
if (isMain) {
|
|
169
|
+
runDoctor().then(r => process.exit(r.blockers > 0 ? 1 : 0));
|
|
154
170
|
}
|
package/src/cli/setup.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { detectProject } from './detector.ts';
|
|
6
6
|
import { runHook } from './hook.ts';
|
|
7
|
+
import { runDoctor } from './preflight.ts';
|
|
7
8
|
|
|
8
9
|
const PASS = '\x1b[32m✓\x1b[0m';
|
|
9
10
|
const WARN = '\x1b[33m!\x1b[0m';
|
|
@@ -77,5 +78,8 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
console.log('\n[setup] Checking prerequisites...');
|
|
82
|
+
await runDoctor();
|
|
83
|
+
|
|
80
84
|
console.log('\n[setup] Done. Run: npx autopilot run\n');
|
|
81
85
|
}
|