import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { stringify } from "yaml"; import { PLAYBOOK_DIR } from "./playbooks.js"; import { renderValidationErrors } from "./render.js"; import type { LoadedPlaybook, PlaybookDefinition } from "./types.js"; import { parsePlaybookYaml, validatePlaybook } from "./validation.js"; export interface DraftSaveUi { notify(message: string, level: "info" | "warning" | "error"): void; confirm?(title: string, message: string): Promise; } export function definitionToYaml(definition: PlaybookDefinition): string { return `${stringify(definition)}\n`; } export function previewDraft(definition: PlaybookDefinition, targetPath: string): string { const yaml = definitionToYaml(definition); return [`Target: ${targetPath}`, "", yaml.trimEnd()].join("\n"); } export function validateDraftDefinition( definition: PlaybookDefinition, targetPath: string, availableSkills: ReadonlySet, ): { loaded: LoadedPlaybook; result: ReturnType } { const loaded = parsePlaybookYaml(definitionToYaml(definition), targetPath); const result = validatePlaybook(loaded, availableSkills, { requireSkills: true }); return { loaded, result }; } export async function savePlaybookDraft( cwd: string, definition: PlaybookDefinition, availableSkills: ReadonlySet, ui: DraftSaveUi | undefined, options: { sourceLabel: string }, ): Promise { const targetPath = join(cwd, PLAYBOOK_DIR, `${definition.id}.yml`); const preview = previewDraft(definition, targetPath); const { result } = validateDraftDefinition(definition, targetPath, availableSkills); notify(ui, preview, "info"); if (!result.valid) { notify(ui, `${options.sourceLabel} draft validation failed:\n${renderValidationErrors(result.errors)}`, "error"); return false; } if (!ui?.confirm) { notify(ui, "Confirmation UI is required before saving a recorded draft.", "error"); return false; } const confirmed = await ui.confirm( `Save ${options.sourceLabel} playbook draft?`, `Write ${definition.id}.yml to ${PLAYBOOK_DIR}/ after validation.`, ); if (!confirmed) { notify(ui, `${options.sourceLabel} draft save skipped.`, "info"); return false; } await mkdir(join(cwd, PLAYBOOK_DIR), { recursive: true }); try { await writeFile(targetPath, definitionToYaml(definition), { encoding: "utf8", flag: "wx" }); } catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") { notify(ui, `${options.sourceLabel} draft already exists at ${targetPath}.`, "error"); return false; } throw error; } notify(ui, `Saved playbook draft to ${targetPath}.`, "info"); return true; } function notify(ui: DraftSaveUi | undefined, message: string, level: "info" | "warning" | "error"): void { ui?.notify(message, level); }