import { Command } from "commander"; import chalk from "chalk"; import ora from "ora"; import { apiRequest } from "../lib/api-client.js"; import { installSkill } from "./install.js"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import { detectInstallTarget } from "../lib/install-path.js"; import type { BundleDetail } from "@skills-hub-ai/shared"; interface SkillResult { slug: string; status: "installed" | "skipped" | "failed"; error?: string; } export async function installBundle( bundleSlug: string, options: { target?: string }, ): Promise<{ bundle: BundleDetail; results: SkillResult[] }> { const bundle = await apiRequest( `/api/v1/bundles/${encodeURIComponent(bundleSlug)}`, ); const target = detectInstallTarget(); const targetPath = options.target === "cursor" ? join(homedir(), ".cursor", "skills") : target.path; const results: SkillResult[] = []; for (const skill of bundle.skills) { // Skip already-installed skills if (existsSync(join(targetPath, skill.slug, "SKILL.md"))) { results.push({ slug: skill.slug, status: "skipped" }); continue; } try { await installSkill(skill.slug, { target: options.target }); results.push({ slug: skill.slug, status: "installed" }); } catch (err) { const msg = err instanceof Error ? err.message : "Unknown error"; results.push({ slug: skill.slug, status: "failed", error: msg }); } } return { bundle, results }; } export const installBundleCommand = new Command("install-bundle") .description("Install all skills from a bundle") .argument("", "Bundle slug") .option("-t, --target ", "Install target: claude-code, cursor") .action(async (slug: string, options) => { const spinner = ora(`Fetching bundle ${slug}...`).start(); try { const bundle = await apiRequest( `/api/v1/bundles/${encodeURIComponent(slug)}`, ); spinner.text = `Installing ${bundle.skills.length} skills from "${bundle.name}"...`; const target = detectInstallTarget(); const targetPath = options.target === "cursor" ? join(homedir(), ".cursor", "skills") : target.path; const results: SkillResult[] = []; for (let i = 0; i < bundle.skills.length; i++) { const skill = bundle.skills[i]; spinner.text = `Installing ${i + 1}/${bundle.skills.length}: ${skill.slug}...`; if (existsSync(join(targetPath, skill.slug, "SKILL.md"))) { results.push({ slug: skill.slug, status: "skipped" }); continue; } try { await installSkill(skill.slug, { target: options.target }); results.push({ slug: skill.slug, status: "installed" }); } catch (err) { const msg = err instanceof Error ? err.message : "Unknown error"; results.push({ slug: skill.slug, status: "failed", error: msg }); } } const installed = results.filter((r) => r.status === "installed").length; const skipped = results.filter((r) => r.status === "skipped").length; const failed = results.filter((r) => r.status === "failed").length; spinner.succeed(`Bundle "${chalk.bold(bundle.name)}" installed`); const parts: string[] = []; if (installed > 0) parts.push(`${installed} installed`); if (skipped > 0) parts.push(`${skipped} already installed`); if (failed > 0) parts.push(chalk.yellow(`${failed} failed`)); console.log(` Skills: ${parts.join(", ")}`); if (failed > 0) { for (const r of results.filter((r) => r.status === "failed")) { console.log(chalk.red(` ${r.slug}: ${r.error}`)); } process.exitCode = 1; } } catch (err) { spinner.fail( chalk.red(err instanceof Error ? err.message : "Install failed"), ); process.exit(1); } });