@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +5 -1
  3. package/dist/src/cli/autoregress-bridge.js +21 -5
  4. package/dist/src/cli/index.js +130 -2
  5. package/dist/src/cli/init-migrate.d.ts +35 -0
  6. package/dist/src/cli/init-migrate.js +299 -0
  7. package/dist/src/cli/migrate-doctor.d.ts +19 -0
  8. package/dist/src/cli/migrate-doctor.js +191 -0
  9. package/dist/src/core/migrate/alias-resolver.d.ts +18 -0
  10. package/dist/src/core/migrate/alias-resolver.js +150 -0
  11. package/dist/src/core/migrate/audit-log.d.ts +30 -0
  12. package/dist/src/core/migrate/audit-log.js +100 -0
  13. package/dist/src/core/migrate/contract.d.ts +27 -0
  14. package/dist/src/core/migrate/contract.js +35 -0
  15. package/dist/src/core/migrate/detector-rules.d.ts +26 -0
  16. package/dist/src/core/migrate/detector-rules.js +147 -0
  17. package/dist/src/core/migrate/detector.d.ts +16 -0
  18. package/dist/src/core/migrate/detector.js +105 -0
  19. package/dist/src/core/migrate/dispatcher.d.ts +19 -0
  20. package/dist/src/core/migrate/dispatcher.js +358 -0
  21. package/dist/src/core/migrate/doctor-checks.d.ts +19 -0
  22. package/dist/src/core/migrate/doctor-checks.js +304 -0
  23. package/dist/src/core/migrate/envelope.d.ts +25 -0
  24. package/dist/src/core/migrate/envelope.js +84 -0
  25. package/dist/src/core/migrate/executor.d.ts +33 -0
  26. package/dist/src/core/migrate/executor.js +102 -0
  27. package/dist/src/core/migrate/handshake.d.ts +17 -0
  28. package/dist/src/core/migrate/handshake.js +130 -0
  29. package/dist/src/core/migrate/migrator.d.ts +34 -0
  30. package/dist/src/core/migrate/migrator.js +302 -0
  31. package/dist/src/core/migrate/monorepo.d.ts +2 -0
  32. package/dist/src/core/migrate/monorepo.js +114 -0
  33. package/dist/src/core/migrate/policy-enforcer.d.ts +28 -0
  34. package/dist/src/core/migrate/policy-enforcer.js +111 -0
  35. package/dist/src/core/migrate/result-parser.d.ts +16 -0
  36. package/dist/src/core/migrate/result-parser.js +152 -0
  37. package/dist/src/core/migrate/schema-validator.d.ts +11 -0
  38. package/dist/src/core/migrate/schema-validator.js +103 -0
  39. package/dist/src/core/migrate/types.d.ts +49 -0
  40. package/dist/src/core/migrate/types.js +3 -0
  41. package/dist/src/core/phases/tests.js +19 -0
  42. package/package.json +5 -1
  43. package/presets/aliases.lock.json +20 -0
  44. package/presets/python/guardrail.config.yaml +11 -0
  45. package/presets/schemas/migrate.schema.json +134 -0
  46. package/skills/autopilot/SKILL.md +29 -9
  47. package/skills/migrate/skill.manifest.json +7 -0
  48. package/skills/migrate-none/SKILL.md +40 -0
  49. package/skills/migrate-none/skill.manifest.json +7 -0
  50. package/skills/migrate-supabase/SKILL.md +126 -0
  51. 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