import { execSync } from 'child_process'; import type { SkillInstallerSpec } from './types.js'; export interface InstallerPlanStep { installerType: SkillInstallerSpec['type']; command: string; } export interface InstallerPlanSkip { installerType: SkillInstallerSpec['type']; reason: string; } export interface InstallerPlan { mode: 'dry-run'; steps: InstallerPlanStep[]; skipped: InstallerPlanSkip[]; } export interface InstallerPlanningOptions { hasBinary?: (name: string) => boolean; } function hasBinaryOnPath(name: string): boolean { const cmd = process.platform === 'win32' ? 'where' : 'which'; try { execSync(`${cmd} ${name}`, { stdio: 'ignore' }); return true; } catch { return false; } } export function buildInstallerPlan( installers: SkillInstallerSpec[] | undefined, opts: InstallerPlanningOptions = {}, ): InstallerPlan { const hasBinary = opts.hasBinary ?? hasBinaryOnPath; const steps: InstallerPlanStep[] = []; const skipped: InstallerPlanSkip[] = []; for (const installer of installers ?? []) { if (installer.type === 'brew') { if (!hasBinary('brew')) { skipped.push({ installerType: 'brew', reason: 'brew not available in PATH' }); continue; } steps.push({ installerType: 'brew', command: `brew install ${installer.packages.join(' ')}` }); continue; } if (installer.type === 'node') { if (hasBinary('pnpm')) { steps.push({ installerType: 'node', command: `pnpm add -g ${installer.packages.join(' ')}` }); continue; } if (hasBinary('npm')) { steps.push({ installerType: 'node', command: `npm install -g ${installer.packages.join(' ')}` }); continue; } skipped.push({ installerType: 'node', reason: 'neither pnpm nor npm available in PATH' }); continue; } if (installer.type === 'go') { if (!hasBinary('go')) { skipped.push({ installerType: 'go', reason: 'go not available in PATH' }); continue; } for (const pkg of installer.packages) { steps.push({ installerType: 'go', command: `go install ${pkg}@latest` }); } continue; } const destination = installer.destination ?? ''; steps.push({ installerType: 'download', command: `download ${installer.url} -> ${destination}`, }); } return { mode: 'dry-run', steps, skipped, }; }