import inquirer from 'inquirer'; import chalk from 'chalk'; import ora from 'ora'; import { promisify } from 'util'; import * as fs from 'fs-extra'; import * as path from 'path'; import { parseGitHubUrl, shallowCloneRepo, getSkillsInDir, findSkillDirectory } from './git'; import { getTempDir } from './paths'; import { CLIType } from '../types'; const execAsync = promisify(require('child_process').exec); interface InstallSkillOptions { repoUrl: string; gitPath?: string; cliTargets?: CLIType[]; skipConfirmation?: boolean; installPath?: 'global' | 'local'; currentDir?: string; } export async function installSkill(options: InstallSkillOptions): Promise { const { repoUrl, gitPath = '', cliTargets, skipConfirmation = false, installPath: initialInstallPath, currentDir, } = options; const spinner = ora('Cloning repository...').start(); // Clone repository let tempDir: string | null = null; try { tempDir = path.join(getTempDir(), Date.now().toString()); await fs.ensureDir(getTempDir()); await shallowCloneRepo(repoUrl, tempDir); spinner.succeed('Repository cloned successfully'); } catch (error: any) { spinner.fail('Failed to clone repository'); console.error(chalk.red(error.message)); if (tempDir) { await fs.remove(tempDir).catch(() => {}); } throw error; } // Find skill(s) to install spinner.start('Finding skills...'); let skillsToInstall: string[] = []; if (gitPath) { // Specific path provided const targetPath = path.join(tempDir, gitPath); if (await fs.pathExists(targetPath)) { const skillDir = await findSkillDirectory(targetPath); if (skillDir) { skillsToInstall.push(skillDir); } else { spinner.fail('No valid skill found at specified path'); throw new Error('No valid skill found'); } } else { spinner.fail('Specified path does not exist in repository'); throw new Error('Path does not exist'); } } else { // No specific path, find all skills const allSkills = await getSkillsInDir(tempDir); if (allSkills.length === 0) { spinner.fail('No skills found in repository'); throw new Error('No skills found'); } if (allSkills.length === 1) { skillsToInstall.push(allSkills[0]); } else if (!skipConfirmation) { spinner.stop(); const { selectedSkills } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedSkills', message: 'Select skills to install:', choices: allSkills.map(s => ({ name: path.basename(s), value: s, })), }, ]); if (selectedSkills.length === 0) { console.log(chalk.yellow('No skills selected')); await fs.remove(tempDir); return; } skillsToInstall = selectedSkills; } else { // Auto-select all if skipConfirmation skillsToInstall = allSkills; } } spinner.succeed(`Found ${skillsToInstall.length} skill(s)`); // Select target CLI(s) let finalCliTargets: CLIType[] = []; if (cliTargets && cliTargets.length > 0) { finalCliTargets = cliTargets; } else if (!skipConfirmation) { const { selectedCli } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedCli', message: 'Select target CLI(s):', choices: [ { name: 'Antigravity', value: 'antigravity' }, { name: 'Claude Code', value: 'claude' }, { name: 'Codex CLI', value: 'codex' }, { name: 'Cursor', value: 'cursor' }, { name: 'Gemini CLI', value: 'gemini' }, { name: 'Kiro CLI', value: 'kiro' }, { name: 'OpenCode', value: 'opencode' }, ], default: ['claude'], }, ]); if (selectedCli.length === 0) { console.log(chalk.yellow('No CLI selected')); await fs.remove(tempDir); return; } finalCliTargets = selectedCli; } else { // Default to claude if skipConfirmation finalCliTargets = ['claude']; } // Select install path (global or local) let finalInstallPath: 'global' | 'local'; if (initialInstallPath) { finalInstallPath = initialInstallPath; } else if (!skipConfirmation) { const { installPath } = await inquirer.prompt([ { type: 'list', name: 'installPath', message: 'Select install path:', choices: [ { name: 'Global (home directory: ~/./skills)', value: 'global', }, { name: 'Local (current directory: ././skills)', value: 'local', }, ], default: 'global', }, ]); finalInstallPath = installPath; } else { finalInstallPath = 'global'; } // Clean up on cancellation const cleanup = async () => { if (tempDir) { await fs.remove(tempDir).catch(() => {}); } }; process.on('SIGINT', async () => { console.log(chalk.yellow('\nCancelling...')); await cleanup(); process.exit(0); }); // Install skills const currentWorkingDir = currentDir || process.cwd(); for (const skillPath of skillsToInstall) { const skillName = path.basename(skillPath); console.log(`\n${chalk.bold('Installing:')} ${skillName}`); for (const cli of finalCliTargets) { const installSpinner = ora(`Installing to ${cli}...`).start(); try { // Use Python backend to install const pythonScript = path.join(__dirname, '../../python/skills_router/core.py'); const installResult = await execAsync( `python3 ${pythonScript} install '${JSON.stringify({ sourcePath: skillPath, targetCli: cli, installPath: finalInstallPath, currentDir: currentWorkingDir, })}'` ); const result = JSON.parse(installResult.stdout); if (result.success) { installSpinner.succeed(`Installed to ${cli} (${finalInstallPath})`); } else { installSpinner.fail(`Failed to install to ${cli}: ${result.error}`); } } catch (error: any) { installSpinner.fail(`Failed to install to ${cli}: ${error.message}`); } } } // Cleanup spinner.start('Cleaning up...'); await cleanup(); spinner.succeed('Cleanup complete'); console.log(chalk.green('\n✓ All operations completed successfully!')); }