/** * Studio action executor (U7). Wires the existing skill backend * (templates, doctor) into a single action runner the Studio action pane * calls. Destructive paths (overwrite) route through the supplied * `confirm` callback so the action pane can render the same * confirm step it already shows for delete/overwrite elsewhere. */ import { existsSync } from "node:fs"; import { join } from "node:path"; import { SkillNameError, writeSkillFromTemplate, type SkillTemplateId, } from "../extension/skills/skill-templates.ts"; import { collectSkillDoctorInputs, diagnoseSkills } from "../extension/skills/skill-doctor.ts"; export type StudioAction = | { type: "create"; name: string; template: SkillTemplateId; skillsRoot: string; } | { type: "doctor"; cwd: string; }; export interface ActionContext { confirm: (message: string) => Promise; } export type ActionResultKind = "ok" | "error" | "declined"; export interface ActionResult { kind: ActionResultKind; message: string; filePath?: string; } export async function runStudioAction( action: StudioAction, ctx: ActionContext, ): Promise { if (action.type === "create") { return runCreate(action, ctx); } if (action.type === "doctor") { return runDoctor(action); } return { kind: "error", message: `Unknown action` }; } async function runCreate( action: Extract, ctx: ActionContext, ): Promise { const safe = action.name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-"); if (!safe || safe === "-" || safe.includes("..")) { return { kind: "error", message: `Invalid skill name: ${action.name}` }; } const filePath = join(action.skillsRoot, safe, "SKILL.md"); if (existsSync(filePath)) { const ok = await ctx.confirm(`Skill "${safe}" exists. Overwrite?`); if (!ok) { return { kind: "declined", message: `Cancelled: ${safe} unchanged`, filePath }; } try { // Atomic overwrite: the replacement is written to a temp file and // renamed over SKILL.md only after it succeeds, so a failed write // never destroys the existing skill. writeSkillFromTemplate(safe, action.template, action.skillsRoot, { overwrite: true, }); return { kind: "ok", message: `Overwrote ${safe}`, filePath }; } catch (err) { return { kind: "error", message: errorMessage(err) }; } } try { writeSkillFromTemplate(safe, action.template, action.skillsRoot); return { kind: "ok", message: `Created ${safe}`, filePath }; } catch (err) { return { kind: "error", message: errorMessage(err) }; } } function runDoctor(action: Extract): ActionResult { const { entries, usage, contents } = collectSkillDoctorInputs(action.cwd); const rows = diagnoseSkills(entries, usage, contents); const ok = rows.filter((r) => r.status === "ok").length; const warn = rows.filter((r) => r.status === "warn").length; const fail = rows.filter((r) => r.status === "fail").length; return { kind: "ok", message: `Doctor: ${rows.length} skills · ${ok} ok · ${warn} warn · ${fail} fail`, }; } function errorMessage(err: unknown): string { if (err instanceof SkillNameError) return err.message; if (err instanceof Error) return err.message; return String(err); }