@delegance/claude-autopilot 5.0.8 → 5.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 +42 -0
- package/README.md +5 -1
- package/dist/src/cli/autoregress-bridge.js +21 -5
- package/dist/src/cli/index.js +130 -2
- package/dist/src/cli/init-migrate.d.ts +35 -0
- package/dist/src/cli/init-migrate.js +299 -0
- package/dist/src/cli/migrate-doctor.d.ts +19 -0
- package/dist/src/cli/migrate-doctor.js +191 -0
- package/dist/src/core/migrate/alias-resolver.d.ts +18 -0
- package/dist/src/core/migrate/alias-resolver.js +150 -0
- package/dist/src/core/migrate/audit-log.d.ts +30 -0
- package/dist/src/core/migrate/audit-log.js +100 -0
- package/dist/src/core/migrate/contract.d.ts +27 -0
- package/dist/src/core/migrate/contract.js +35 -0
- package/dist/src/core/migrate/detector-rules.d.ts +26 -0
- package/dist/src/core/migrate/detector-rules.js +147 -0
- package/dist/src/core/migrate/detector.d.ts +16 -0
- package/dist/src/core/migrate/detector.js +105 -0
- package/dist/src/core/migrate/dispatcher.d.ts +19 -0
- package/dist/src/core/migrate/dispatcher.js +358 -0
- package/dist/src/core/migrate/doctor-checks.d.ts +19 -0
- package/dist/src/core/migrate/doctor-checks.js +304 -0
- package/dist/src/core/migrate/envelope.d.ts +25 -0
- package/dist/src/core/migrate/envelope.js +84 -0
- package/dist/src/core/migrate/executor.d.ts +33 -0
- package/dist/src/core/migrate/executor.js +102 -0
- package/dist/src/core/migrate/handshake.d.ts +17 -0
- package/dist/src/core/migrate/handshake.js +130 -0
- package/dist/src/core/migrate/migrator.d.ts +34 -0
- package/dist/src/core/migrate/migrator.js +302 -0
- package/dist/src/core/migrate/monorepo.d.ts +2 -0
- package/dist/src/core/migrate/monorepo.js +114 -0
- package/dist/src/core/migrate/policy-enforcer.d.ts +28 -0
- package/dist/src/core/migrate/policy-enforcer.js +111 -0
- package/dist/src/core/migrate/result-parser.d.ts +16 -0
- package/dist/src/core/migrate/result-parser.js +152 -0
- package/dist/src/core/migrate/schema-validator.d.ts +11 -0
- package/dist/src/core/migrate/schema-validator.js +103 -0
- package/dist/src/core/migrate/types.d.ts +49 -0
- package/dist/src/core/migrate/types.js +3 -0
- package/dist/src/core/phases/tests.js +19 -0
- package/package.json +5 -1
- package/presets/aliases.lock.json +20 -0
- package/presets/python/guardrail.config.yaml +11 -0
- package/presets/schemas/migrate.schema.json +134 -0
- package/skills/autopilot/SKILL.md +29 -9
- package/skills/migrate/skill.manifest.json +7 -0
- package/skills/migrate-none/SKILL.md +40 -0
- package/skills/migrate-none/skill.manifest.json +7 -0
- package/skills/migrate-supabase/SKILL.md +126 -0
- package/skills/migrate-supabase/skill.manifest.json +7 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// src/core/migrate/doctor-checks.ts
|
|
2
|
+
//
|
|
3
|
+
// Pure check functions for `claude-autopilot doctor` (migrate-specific).
|
|
4
|
+
// Each check is read-only and returns { ok, message?, fixHint? }.
|
|
5
|
+
// Mutations live behind `--fix` (see src/cli/migrate-doctor.ts).
|
|
6
|
+
//
|
|
7
|
+
// Spec: docs/superpowers/specs/2026-04-29-migrate-skill-generalization-design.md
|
|
8
|
+
// (§ "claude-autopilot doctor", checks 1–8)
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import * as yaml from 'js-yaml';
|
|
13
|
+
import { validateStackMd } from "./schema-validator.js";
|
|
14
|
+
import { resolveSkill } from "./alias-resolver.js";
|
|
15
|
+
import { DETECTION_RULES } from "./detector-rules.js";
|
|
16
|
+
function stackPath(repoRoot) {
|
|
17
|
+
return path.join(repoRoot, '.autopilot', 'stack.md');
|
|
18
|
+
}
|
|
19
|
+
function readStackMdRaw(repoRoot) {
|
|
20
|
+
const p = stackPath(repoRoot);
|
|
21
|
+
if (!fs.existsSync(p))
|
|
22
|
+
return null;
|
|
23
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = yaml.load(raw);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!parsed || typeof parsed !== 'object')
|
|
32
|
+
return null;
|
|
33
|
+
return { raw, parsed: parsed };
|
|
34
|
+
}
|
|
35
|
+
// 1. stack.md exists
|
|
36
|
+
export function stackMdExists(repoRoot) {
|
|
37
|
+
const p = stackPath(repoRoot);
|
|
38
|
+
if (fs.existsSync(p))
|
|
39
|
+
return { ok: true };
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
message: `.autopilot/stack.md not found in ${repoRoot}`,
|
|
43
|
+
fixHint: 'run `claude-autopilot init` to scaffold one',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// 2. JSON Schema validates
|
|
47
|
+
export function schemaValidates(repoRoot) {
|
|
48
|
+
const data = readStackMdRaw(repoRoot);
|
|
49
|
+
if (!data) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: 'cannot validate schema: stack.md missing or unparseable',
|
|
53
|
+
fixHint: 'run `claude-autopilot init`',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const result = validateStackMd(data.raw);
|
|
57
|
+
if (result.valid)
|
|
58
|
+
return { ok: true };
|
|
59
|
+
const summary = result.errors
|
|
60
|
+
.map(e => (e.path ? `${e.path}: ${e.message}` : e.message))
|
|
61
|
+
.join('; ');
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
message: `schema validation failed — ${summary}`,
|
|
65
|
+
fixHint: 'run `claude-autopilot doctor --fix` to auto-fix simple cases (missing schema_version, default policy keys)',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 3. migrate.skill resolves to an installed skill
|
|
69
|
+
export function skillResolves(repoRoot) {
|
|
70
|
+
const data = readStackMdRaw(repoRoot);
|
|
71
|
+
if (!data) {
|
|
72
|
+
return { ok: false, message: 'cannot resolve skill: stack.md missing' };
|
|
73
|
+
}
|
|
74
|
+
const skill = data.parsed.migrate?.skill;
|
|
75
|
+
if (!skill || typeof skill !== 'string') {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: 'migrate.skill is missing or not a string',
|
|
79
|
+
fixHint: 'set `migrate.skill` to a stable ID (e.g. "migrate@1", "migrate.supabase@1", "none@1")',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const res = resolveSkill(skill, { repoRoot });
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
if (res.normalizedFromRaw) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
message: `migrate.skill "${skill}" is a raw alias; resolves to stable ID "${res.stableId}"`,
|
|
88
|
+
fixHint: `change \`migrate.skill\` to "${res.stableId}" (auto-fixable via --fix)`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
message: `migrate.skill "${skill}" failed to resolve: ${res.message}`,
|
|
96
|
+
fixHint: 'use a stable ID listed in presets/aliases.lock.json',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// 4. Per-env commands explicit (no env reuses envs.dev.command)
|
|
100
|
+
export function perEnvCommandsExplicit(repoRoot) {
|
|
101
|
+
const data = readStackMdRaw(repoRoot);
|
|
102
|
+
if (!data) {
|
|
103
|
+
return { ok: false, message: 'cannot check envs: stack.md missing' };
|
|
104
|
+
}
|
|
105
|
+
const envs = data.parsed.migrate?.envs;
|
|
106
|
+
if (!envs)
|
|
107
|
+
return { ok: true };
|
|
108
|
+
const dev = envs.dev?.command;
|
|
109
|
+
if (!dev)
|
|
110
|
+
return { ok: true };
|
|
111
|
+
const devKey = JSON.stringify(dev);
|
|
112
|
+
const offenders = [];
|
|
113
|
+
for (const [name, spec] of Object.entries(envs)) {
|
|
114
|
+
if (name === 'dev')
|
|
115
|
+
continue;
|
|
116
|
+
if (spec?.command && JSON.stringify(spec.command) === devKey) {
|
|
117
|
+
offenders.push(name);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (offenders.length === 0)
|
|
121
|
+
return { ok: true };
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
message: `envs.${offenders.join(', envs.')} reuse envs.dev.command — running a dev migration against a non-dev env is destructive`,
|
|
125
|
+
fixHint: `set an explicit \`command\` for each non-dev env (e.g. \`prisma migrate deploy\` for prod)`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// 5. policy.* fields are booleans
|
|
129
|
+
export function policyFieldsValid(repoRoot) {
|
|
130
|
+
const data = readStackMdRaw(repoRoot);
|
|
131
|
+
if (!data) {
|
|
132
|
+
return { ok: false, message: 'cannot check policy: stack.md missing' };
|
|
133
|
+
}
|
|
134
|
+
const policy = data.parsed.migrate?.policy;
|
|
135
|
+
if (!policy || typeof policy !== 'object')
|
|
136
|
+
return { ok: true };
|
|
137
|
+
const offenders = [];
|
|
138
|
+
for (const [k, v] of Object.entries(policy)) {
|
|
139
|
+
if (typeof v !== 'boolean') {
|
|
140
|
+
offenders.push(`${k}=${JSON.stringify(v)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (offenders.length === 0)
|
|
144
|
+
return { ok: true };
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
message: `policy fields must be booleans; offenders: ${offenders.join(', ')}`,
|
|
148
|
+
fixHint: 'edit `.autopilot/stack.md` so each policy.* field is `true` or `false`',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function findRuleForSkill(skill) {
|
|
152
|
+
// Match by defaultSkill. For migrate@1 there are many rules; we
|
|
153
|
+
// can't disambiguate without re-detecting, so we only verify
|
|
154
|
+
// narrow skill IDs (migrate.supabase@1) and return undefined for
|
|
155
|
+
// generic migrate@1 (which can have any toolchain).
|
|
156
|
+
return DETECTION_RULES.find(r => r.defaultSkill === skill);
|
|
157
|
+
}
|
|
158
|
+
// 6. project_root has expected toolchain files for the resolved skill.
|
|
159
|
+
export function projectRootHasToolchain(repoRoot) {
|
|
160
|
+
const data = readStackMdRaw(repoRoot);
|
|
161
|
+
if (!data) {
|
|
162
|
+
return { ok: false, message: 'cannot verify toolchain: stack.md missing' };
|
|
163
|
+
}
|
|
164
|
+
const skill = data.parsed.migrate?.skill;
|
|
165
|
+
if (!skill)
|
|
166
|
+
return { ok: false, message: 'migrate.skill is missing' };
|
|
167
|
+
// Skip the no-op skill — nothing to verify.
|
|
168
|
+
if (skill === 'none@1')
|
|
169
|
+
return { ok: true };
|
|
170
|
+
const projectRoot = data.parsed.migrate?.project_root ?? '.';
|
|
171
|
+
const projectAbs = path.resolve(repoRoot, projectRoot);
|
|
172
|
+
if (!fs.existsSync(projectAbs)) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
message: `project_root "${projectRoot}" does not exist (resolved: ${projectAbs})`,
|
|
176
|
+
fixHint: 'set `migrate.project_root` to a path that exists, or run `init`',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Rule-based toolchain check. For migrate.supabase@1 there's exactly
|
|
180
|
+
// one matching rule (nextjs-supabase). For migrate@1, multiple rules
|
|
181
|
+
// share the skill; we only enforce the toolchain when stack.md
|
|
182
|
+
// explicitly declares a supabase or shape-narrowed skill.
|
|
183
|
+
if (skill === 'migrate.supabase@1') {
|
|
184
|
+
const rule = DETECTION_RULES.find(r => r.defaultSkill === 'migrate.supabase@1');
|
|
185
|
+
if (!rule)
|
|
186
|
+
return { ok: true };
|
|
187
|
+
const missing = rule.requireAll.filter(p => !fs.existsSync(path.join(projectAbs, p)));
|
|
188
|
+
if (missing.length === 0)
|
|
189
|
+
return { ok: true };
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
message: `project_root missing expected toolchain files for ${skill}: ${missing.join(', ')}`,
|
|
193
|
+
fixHint: 'verify `migrate.project_root` points to the correct workspace',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// For migrate@1, attempt best-effort detection via existing rules to
|
|
197
|
+
// catch obvious misalignments. We require AT LEAST one migrate@1 rule
|
|
198
|
+
// to satisfy its requireAll/requireAny set; if none do, the stack.md
|
|
199
|
+
// claims a tool that isn't present.
|
|
200
|
+
if (skill === 'migrate@1') {
|
|
201
|
+
const candidates = DETECTION_RULES.filter(r => r.defaultSkill === 'migrate@1');
|
|
202
|
+
const anySatisfied = candidates.some(r => {
|
|
203
|
+
const allOk = r.requireAll.every(p => fs.existsSync(path.join(projectAbs, p)));
|
|
204
|
+
const anyOk = !r.requireAny || r.requireAny.some(p => fs.existsSync(path.join(projectAbs, p)));
|
|
205
|
+
return allOk && anyOk && r.requireAll.length + (r.requireAny?.length ?? 0) > 0;
|
|
206
|
+
});
|
|
207
|
+
if (anySatisfied)
|
|
208
|
+
return { ok: true };
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
message: `project_root "${projectRoot}" does not contain any recognized migration toolchain files for ${skill}`,
|
|
212
|
+
fixHint: 'verify `migrate.project_root` or change `migrate.skill` to match your stack',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Unknown skill — let skillResolves report the issue.
|
|
216
|
+
const rule = findRuleForSkill(skill);
|
|
217
|
+
if (!rule)
|
|
218
|
+
return { ok: true };
|
|
219
|
+
return { ok: true };
|
|
220
|
+
}
|
|
221
|
+
// 7. Deprecated keys reported (read-only).
|
|
222
|
+
export function deprecatedKeysAbsent(repoRoot) {
|
|
223
|
+
const data = readStackMdRaw(repoRoot);
|
|
224
|
+
if (!data) {
|
|
225
|
+
return { ok: false, message: 'cannot check deprecated keys: stack.md missing' };
|
|
226
|
+
}
|
|
227
|
+
const offenders = [];
|
|
228
|
+
if ('dev_command' in data.parsed) {
|
|
229
|
+
offenders.push('dev_command (top-level)');
|
|
230
|
+
}
|
|
231
|
+
if (offenders.length === 0)
|
|
232
|
+
return { ok: true };
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
message: `deprecated keys present: ${offenders.join(', ')}`,
|
|
236
|
+
fixHint: 'run `claude-autopilot doctor --fix` to migrate `dev_command` → `migrate.envs.dev.command`',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// 8. env_file safety: relative to project_root, no `..`, NOT git-tracked.
|
|
240
|
+
export function envFileSafety(repoRoot) {
|
|
241
|
+
const data = readStackMdRaw(repoRoot);
|
|
242
|
+
if (!data) {
|
|
243
|
+
return { ok: false, message: 'cannot check env_file: stack.md missing' };
|
|
244
|
+
}
|
|
245
|
+
const envs = data.parsed.migrate?.envs;
|
|
246
|
+
if (!envs)
|
|
247
|
+
return { ok: true };
|
|
248
|
+
const projectRoot = data.parsed.migrate?.project_root ?? '.';
|
|
249
|
+
const projectAbs = path.resolve(repoRoot, projectRoot);
|
|
250
|
+
const issues = [];
|
|
251
|
+
for (const [name, spec] of Object.entries(envs)) {
|
|
252
|
+
const ef = spec?.env_file;
|
|
253
|
+
if (!ef)
|
|
254
|
+
continue;
|
|
255
|
+
if (path.isAbsolute(ef)) {
|
|
256
|
+
issues.push(`envs.${name}.env_file is absolute (${ef}); must be relative to project_root`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const segments = ef.split(/[/\\]/);
|
|
260
|
+
if (segments.some(s => s === '..')) {
|
|
261
|
+
issues.push(`envs.${name}.env_file contains ".." traversal (${ef}); reject for safety`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const efAbs = path.resolve(projectAbs, ef);
|
|
265
|
+
const rel = path.relative(projectAbs, efAbs);
|
|
266
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
267
|
+
issues.push(`envs.${name}.env_file resolves outside project_root (${ef})`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// Existence is not required (the env file may live only on the
|
|
271
|
+
// operator's machine), but if the repo is a git repo and the file
|
|
272
|
+
// IS tracked, that's a leak we must warn about.
|
|
273
|
+
try {
|
|
274
|
+
const out = execFileSync('git', ['ls-files', '--error-unmatch', '--', ef], { cwd: projectAbs, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
275
|
+
if (out) {
|
|
276
|
+
issues.push(`envs.${name}.env_file is git-tracked (${ef}); secrets in env files must be gitignored`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Not tracked, or not a git repo — both are fine for the safety
|
|
281
|
+
// check. Doctor surfaces tracked files only.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (issues.length === 0)
|
|
285
|
+
return { ok: true };
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
message: issues.join('; '),
|
|
289
|
+
fixHint: 'remove the offending env_file from git (`git rm --cached <file>`) and add it to .gitignore',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
export function runAllChecks(repoRoot) {
|
|
293
|
+
return [
|
|
294
|
+
{ name: 'stackMdExists', result: stackMdExists(repoRoot) },
|
|
295
|
+
{ name: 'schemaValidates', result: schemaValidates(repoRoot) },
|
|
296
|
+
{ name: 'skillResolves', result: skillResolves(repoRoot) },
|
|
297
|
+
{ name: 'perEnvCommandsExplicit', result: perEnvCommandsExplicit(repoRoot) },
|
|
298
|
+
{ name: 'policyFieldsValid', result: policyFieldsValid(repoRoot) },
|
|
299
|
+
{ name: 'projectRootHasToolchain', result: projectRootHasToolchain(repoRoot) },
|
|
300
|
+
{ name: 'deprecatedKeysAbsent', result: deprecatedKeysAbsent(repoRoot) },
|
|
301
|
+
{ name: 'envFileSafety', result: envFileSafety(repoRoot) },
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=doctor-checks.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InvocationEnvelope } from './types.ts';
|
|
2
|
+
export interface BuildEnvelopeOptions {
|
|
3
|
+
changedFiles: string[];
|
|
4
|
+
env: string;
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
ci?: boolean;
|
|
9
|
+
projectId?: string;
|
|
10
|
+
attempt?: number;
|
|
11
|
+
trigger?: 'cli' | 'ci';
|
|
12
|
+
}
|
|
13
|
+
export interface CIDetectionResult {
|
|
14
|
+
ci: boolean;
|
|
15
|
+
provider: string | null;
|
|
16
|
+
overridden: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detect whether we're running in CI and which provider, based on
|
|
20
|
+
* standard env-var markers. AUTOPILOT_CI_PROVIDER overrides the
|
|
21
|
+
* detected value (with audit-log evidence).
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectCI(): CIDetectionResult;
|
|
24
|
+
export declare function buildEnvelope(opts: BuildEnvelopeOptions): InvocationEnvelope;
|
|
25
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/core/migrate/envelope.ts
|
|
2
|
+
//
|
|
3
|
+
// Builds an InvocationEnvelope for a migrate dispatch. Generates a
|
|
4
|
+
// per-call invocationId (UUID v4) and nonce (32-byte hex), reads
|
|
5
|
+
// gitBase/gitHead via git rev-parse, auto-detects CI from env vars.
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { randomUUID, randomBytes } from 'node:crypto';
|
|
8
|
+
import { ENVELOPE_CONTRACT_VERSION } from "./contract.js";
|
|
9
|
+
/**
|
|
10
|
+
* Detect whether we're running in CI and which provider, based on
|
|
11
|
+
* standard env-var markers. AUTOPILOT_CI_PROVIDER overrides the
|
|
12
|
+
* detected value (with audit-log evidence).
|
|
13
|
+
*/
|
|
14
|
+
export function detectCI() {
|
|
15
|
+
const override = process.env.AUTOPILOT_CI_PROVIDER;
|
|
16
|
+
if (override) {
|
|
17
|
+
return { ci: true, provider: override, overridden: true };
|
|
18
|
+
}
|
|
19
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
20
|
+
return { ci: true, provider: 'github-actions', overridden: false };
|
|
21
|
+
}
|
|
22
|
+
if (process.env.GITLAB_CI === 'true') {
|
|
23
|
+
return { ci: true, provider: 'gitlab', overridden: false };
|
|
24
|
+
}
|
|
25
|
+
if (process.env.CIRCLECI === 'true') {
|
|
26
|
+
return { ci: true, provider: 'circleci', overridden: false };
|
|
27
|
+
}
|
|
28
|
+
if (process.env.BUILDKITE === 'true') {
|
|
29
|
+
return { ci: true, provider: 'buildkite', overridden: false };
|
|
30
|
+
}
|
|
31
|
+
if (process.env.JENKINS_URL) {
|
|
32
|
+
return { ci: true, provider: 'jenkins', overridden: false };
|
|
33
|
+
}
|
|
34
|
+
// CI=true alone (no recognized provider) — ci:true, provider:null.
|
|
35
|
+
// Policy enforcer treats this as "missing provider" and blocks prod.
|
|
36
|
+
if (process.env.CI === 'true') {
|
|
37
|
+
return { ci: true, provider: null, overridden: false };
|
|
38
|
+
}
|
|
39
|
+
return { ci: false, provider: null, overridden: false };
|
|
40
|
+
}
|
|
41
|
+
function readGitRef(ref, cwd) {
|
|
42
|
+
try {
|
|
43
|
+
return execFileSync('git', ['rev-parse', ref], {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
47
|
+
}).trim();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw new Error(`buildEnvelope: not in a git repo or rev-parse failed for ${ref}: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function buildEnvelope(opts) {
|
|
54
|
+
const ciInfo = detectCI();
|
|
55
|
+
const ci = opts.ci ?? ciInfo.ci;
|
|
56
|
+
const trigger = opts.trigger ?? (ci ? 'ci' : 'cli');
|
|
57
|
+
const cwd = opts.cwd ?? opts.repoRoot;
|
|
58
|
+
const gitHead = readGitRef('HEAD', opts.repoRoot);
|
|
59
|
+
// Use HEAD~1 as base; if no parent (initial commit), reuse HEAD.
|
|
60
|
+
let gitBase;
|
|
61
|
+
try {
|
|
62
|
+
gitBase = readGitRef('HEAD~1', opts.repoRoot);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
gitBase = gitHead;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
contractVersion: ENVELOPE_CONTRACT_VERSION,
|
|
69
|
+
invocationId: randomUUID(),
|
|
70
|
+
nonce: randomBytes(32).toString('hex'),
|
|
71
|
+
trigger,
|
|
72
|
+
attempt: opts.attempt ?? 1,
|
|
73
|
+
repoRoot: opts.repoRoot,
|
|
74
|
+
cwd,
|
|
75
|
+
changedFiles: opts.changedFiles,
|
|
76
|
+
env: opts.env,
|
|
77
|
+
dryRun: opts.dryRun ?? false,
|
|
78
|
+
ci,
|
|
79
|
+
gitBase,
|
|
80
|
+
gitHead,
|
|
81
|
+
...(opts.projectId !== undefined ? { projectId: opts.projectId } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CommandSpec } from './types.ts';
|
|
2
|
+
export interface ExecuteOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export interface ExecuteResult {
|
|
7
|
+
exitCode: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ExecutableResolution {
|
|
12
|
+
found: boolean;
|
|
13
|
+
absolutePath?: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve an executable name against PATH (or as a workspace-relative
|
|
18
|
+
* script if the name starts with ./ or ../). Returns absolute path if
|
|
19
|
+
* found.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveExecutable(exec: string, cwd: string): ExecutableResolution;
|
|
22
|
+
export declare function executeCommand(spec: CommandSpec, opts: ExecuteOptions): Promise<ExecuteResult>;
|
|
23
|
+
export interface LegacyParseResult {
|
|
24
|
+
spec: CommandSpec;
|
|
25
|
+
warning: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a legacy string-form command via shell-quote. Rejects strings
|
|
29
|
+
* containing any shell metachar — those are valid in shell but dangerous
|
|
30
|
+
* in a structured-argv contract. Emits a deprecation warning.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseLegacyCommand(raw: string): LegacyParseResult;
|
|
33
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/core/migrate/executor.ts
|
|
2
|
+
//
|
|
3
|
+
// Runs CommandSpec via spawn(shell:false). Structured argv is the only
|
|
4
|
+
// non-deprecated path; legacy string form goes through shell-quote with
|
|
5
|
+
// metachar rejection. PATH resolution is explicit; relative paths are
|
|
6
|
+
// resolved against the workspace cwd.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { parse as shellParse } from 'shell-quote';
|
|
11
|
+
import { SHELL_METACHARS } from "./contract.js";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve an executable name against PATH (or as a workspace-relative
|
|
14
|
+
* script if the name starts with ./ or ../). Returns absolute path if
|
|
15
|
+
* found.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveExecutable(exec, cwd) {
|
|
18
|
+
// Workspace-relative form
|
|
19
|
+
if (exec.startsWith('./') || exec.startsWith('../') || path.isAbsolute(exec)) {
|
|
20
|
+
const abs = path.isAbsolute(exec) ? exec : path.resolve(cwd, exec);
|
|
21
|
+
if (fs.existsSync(abs)) {
|
|
22
|
+
return { found: true, absolutePath: abs };
|
|
23
|
+
}
|
|
24
|
+
return { found: false, reason: `script not found at ${abs}` };
|
|
25
|
+
}
|
|
26
|
+
// PATH lookup
|
|
27
|
+
const PATH = process.env.PATH ?? '';
|
|
28
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
29
|
+
const exts = process.platform === 'win32'
|
|
30
|
+
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').map(e => e.toLowerCase())
|
|
31
|
+
: [''];
|
|
32
|
+
for (const dir of PATH.split(sep)) {
|
|
33
|
+
if (!dir)
|
|
34
|
+
continue;
|
|
35
|
+
for (const ext of exts) {
|
|
36
|
+
const candidate = path.join(dir, exec + ext);
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(candidate);
|
|
39
|
+
if (stat.isFile()) {
|
|
40
|
+
return { found: true, absolutePath: candidate };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// try next
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { found: false, reason: `'${exec}' not found in PATH` };
|
|
49
|
+
}
|
|
50
|
+
export async function executeCommand(spec, opts) {
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
const env = { ...process.env, ...(opts.env ?? {}) };
|
|
53
|
+
let stdout = '';
|
|
54
|
+
let stderr = '';
|
|
55
|
+
let child;
|
|
56
|
+
try {
|
|
57
|
+
child = spawn(spec.exec, spec.args, {
|
|
58
|
+
cwd: opts.cwd,
|
|
59
|
+
env,
|
|
60
|
+
shell: false,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
resolve({ exitCode: -1, stdout: '', stderr: `spawn failed: ${err.message}` });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
child.stdout?.on('data', (b) => { stdout += b.toString(); });
|
|
69
|
+
child.stderr?.on('data', (b) => { stderr += b.toString(); });
|
|
70
|
+
child.on('error', (err) => {
|
|
71
|
+
resolve({ exitCode: -1, stdout, stderr: stderr + `\nspawn error: ${err.message}` });
|
|
72
|
+
});
|
|
73
|
+
child.on('exit', (code) => {
|
|
74
|
+
resolve({ exitCode: code ?? -1, stdout, stderr });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse a legacy string-form command via shell-quote. Rejects strings
|
|
80
|
+
* containing any shell metachar — those are valid in shell but dangerous
|
|
81
|
+
* in a structured-argv contract. Emits a deprecation warning.
|
|
82
|
+
*/
|
|
83
|
+
export function parseLegacyCommand(raw) {
|
|
84
|
+
if (SHELL_METACHARS.test(raw)) {
|
|
85
|
+
throw new Error(`shell metachar in legacy command: '${raw}'. Forbidden — convert to structured argv form { exec, args[] }.`);
|
|
86
|
+
}
|
|
87
|
+
const tokens = shellParse(raw);
|
|
88
|
+
if (tokens.length === 0) {
|
|
89
|
+
throw new Error('empty command string');
|
|
90
|
+
}
|
|
91
|
+
// shell-quote returns string | { op: '...' } | { comment: '...' };
|
|
92
|
+
// we already filtered metachars so all tokens should be strings.
|
|
93
|
+
if (tokens.some(t => typeof t !== 'string')) {
|
|
94
|
+
throw new Error(`unparseable legacy command: '${raw}'`);
|
|
95
|
+
}
|
|
96
|
+
const [exec, ...args] = tokens;
|
|
97
|
+
return {
|
|
98
|
+
spec: { exec: exec, args },
|
|
99
|
+
warning: `legacy string command form is deprecated; convert to { exec, args } structured argv. Auto-fix available via \`claude-autopilot doctor --fix\`.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SkillManifest } from './types.ts';
|
|
2
|
+
export interface HandshakeOptions {
|
|
3
|
+
skillPath: string;
|
|
4
|
+
runtimeVersion: string;
|
|
5
|
+
envelopeContractVersion: string;
|
|
6
|
+
}
|
|
7
|
+
export type HandshakeResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
manifest: SkillManifest;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reasonCode: HandshakeReason;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
export type HandshakeReason = 'manifest-missing' | 'manifest-invalid' | 'runtime-below-min' | 'runtime-above-max' | 'api-version-mismatch';
|
|
16
|
+
export declare function performHandshake(opts: HandshakeOptions): HandshakeResult;
|
|
17
|
+
//# sourceMappingURL=handshake.d.ts.map
|