/** * Uninstall GNO agent skill from supported agent targets. * Includes safety checks before deletion. * * @module src/cli/commands/skill/uninstall */ import { rm } from "node:fs/promises"; import { join } from "node:path"; import { CliError } from "../../errors.js"; import { getGlobals } from "../../program.js"; import { resolveSkillPaths, SKILL_TARGETS, type SkillScope, type SkillTarget, validatePathForDeletion, } from "./paths.js"; // ───────────────────────────────────────────────────────────────────────────── // Uninstall Command // ───────────────────────────────────────────────────────────────────────────── export interface UninstallOptions { scope?: SkillScope; target?: SkillTarget | "all"; /** Override for testing */ cwd?: string; /** Override for testing */ homeDir?: string; /** JSON output (defaults to globals.json) */ json?: boolean; /** Quiet mode (defaults to globals.quiet) */ quiet?: boolean; } interface UninstallResult { target: SkillTarget; scope: SkillScope; path: string; } /** * Uninstall skill from a single target. */ async function uninstallFromTarget( scope: SkillScope, target: SkillTarget, overrides?: { cwd?: string; homeDir?: string } ): Promise { const paths = resolveSkillPaths({ scope, target, ...overrides }); // Check if exists const exists = await Bun.file(join(paths.gnoDir, "SKILL.md")).exists(); if (!exists) { return null; } // Safety validation const validationError = validatePathForDeletion( paths.gnoDir, paths.base, paths.gnoDir ); if (validationError) { throw new CliError( "RUNTIME", `Safety check failed for ${paths.gnoDir}: ${validationError}` ); } // Remove directory try { await rm(paths.gnoDir, { recursive: true, force: true }); return { target, scope, path: paths.gnoDir }; } catch (err) { throw new CliError( "RUNTIME", `Failed to remove skill: ${err instanceof Error ? err.message : String(err)}` ); } } /** * Get globals with fallback for testing. */ function safeGetGlobals(): { json: boolean; quiet: boolean } { try { return getGlobals(); } catch { return { json: false, quiet: false }; } } /** * Uninstall GNO skill. */ export async function uninstallSkill( opts: UninstallOptions = {} ): Promise { const scope = opts.scope ?? "project"; const target = opts.target ?? "claude"; const globals = safeGetGlobals(); const json = opts.json ?? globals.json; const quiet = opts.quiet ?? globals.quiet; const targets: SkillTarget[] = target === "all" ? SKILL_TARGETS : [target]; const results: UninstallResult[] = []; const notFound: string[] = []; for (const t of targets) { const result = await uninstallFromTarget(scope, t, { cwd: opts.cwd, homeDir: opts.homeDir, }); if (result) { results.push(result); } else { notFound.push(t); } } // If nothing was uninstalled if (results.length === 0) { throw new CliError( "VALIDATION", `GNO skill not found for ${targets.join(", ")} (${scope} scope)` ); } // Output if (json) { process.stdout.write( `${JSON.stringify({ uninstalled: results }, null, 2)}\n` ); } else if (!quiet) { for (const r of results) { process.stdout.write(`Uninstalled GNO skill from ${r.path}\n`); } if (notFound.length > 0) { process.stdout.write(`(Not found for: ${notFound.join(", ")})\n`); } } }