import { getBackend } from './backend.ts' import { isDryRun } from './dry-run.ts' import { ensureSchema, getFile, getSkillOrThrow, listFilesForSkill, nowIso, snapshotFile, sqlRun, deleteSkillsIndex, insertSkillRevision, upsertSkillsIndex, } from './db.ts' import { commitAndPublish, deleteRepoPaths, withSkillsRepoSession, parseSkillMeta, readRepoFile, serializeSkillMeta, skillFilePath, skillMetaPath, writeRepoFiles, } from './repo.ts' export type SkillDeleteInput = { id: string path?: string dryRun?: boolean } export type SkillDeleteResult = { id: string deleted: 'file' | 'skill' path?: string files_deleted: number versions_saved: number dryRun?: true } async function deleteLegacy(params: { id: string path: string dryRun: boolean }): Promise { await ensureSchema() const { id, path } = params await getSkillOrThrow(id) const timestamp = nowIso() if (path) { const file = await getFile(id, path) if (!file) { const files = await listFilesForSkill(id) const available = files.length > 0 ? files.map((row) => `\`${row.path}\``).join(', ') : '(none)' throw new Error( `File not found: \`${path}\` in skill \`${id}\`. Valid paths: ${available}.`, ) } if (params.dryRun) { return { id, deleted: 'file', path, files_deleted: 1, versions_saved: 1, dryRun: true, } } await snapshotFile(id, path, file.content, timestamp) await sqlRun(`DELETE FROM skill_files WHERE skill_id = ? AND path = ?`, [ id, path, ]) await sqlRun(`UPDATE skills SET updated_at = ? WHERE id = ?`, [ timestamp, id, ]) return { id, deleted: 'file', path, files_deleted: 1, versions_saved: 1, } } const files = await listFilesForSkill(id) if (params.dryRun) { return { id, deleted: 'skill', files_deleted: files.length, versions_saved: files.length, dryRun: true, } } for (const file of files) { await snapshotFile(id, file.path, file.content, timestamp) } await sqlRun(`DELETE FROM skill_files WHERE skill_id = ?`, [id]) await sqlRun(`DELETE FROM skills WHERE id = ?`, [id]) return { id, deleted: 'skill', files_deleted: files.length, versions_saved: files.length, } } async function deleteRepo(params: { id: string path: string dryRun: boolean }): Promise { const { id, path } = params return withSkillsRepoSession(async (session) => { const meta = parseSkillMeta(await readRepoFile(session.id, skillMetaPath(id))) if (!meta) { await getSkillOrThrow(id) throw new Error(`Skill not found in repo: \`${id}\`.`) } const timestamp = nowIso() if (path) { const filePath = skillFilePath(id, path) const previous = await readRepoFile(session.id, filePath) if (previous == null) { const available = meta.files.length > 0 ? meta.files.map((value) => `\`${value}\``).join(', ') : '(none)' throw new Error( `File not found: \`${path}\` in skill \`${id}\`. Valid paths: ${available}.`, ) } if (params.dryRun) { return { id, deleted: 'file', path, files_deleted: 1, versions_saved: 1, dryRun: true, } } const nextFiles = meta.files.filter((value) => value !== path) await deleteRepoPaths(session.id, [filePath]) await writeRepoFiles(session.id, [ { path: skillMetaPath(id), content: serializeSkillMeta({ ...meta, files: nextFiles, updated_at: timestamp, }), }, ]) await commitAndPublish(session.id, `skill-delete-file: ${id} ${path}`) await insertSkillRevision({ commit_oid: session.base_commit, skill_id: id, path, replaced_at: timestamp, content_length: previous.length, }) await upsertSkillsIndex({ id, name: meta.name, description: meta.description, files: nextFiles, created_at: meta.created_at, updated_at: timestamp, }) return { id, deleted: 'file', path, files_deleted: 1, versions_saved: 1, } } const paths = [ skillMetaPath(id), ...meta.files.map((filePath) => skillFilePath(id, filePath)), ] const contents: Array<{ path: string; content: string }> = [] for (const filePath of meta.files) { const content = await readRepoFile(session.id, skillFilePath(id, filePath)) if (content != null) contents.push({ path: filePath, content }) } if (params.dryRun) { return { id, deleted: 'skill', files_deleted: contents.length, versions_saved: contents.length, dryRun: true, } } await deleteRepoPaths(session.id, paths) await commitAndPublish(session.id, `skill-delete: ${id}`) for (const file of contents) { await insertSkillRevision({ commit_oid: session.base_commit, skill_id: id, path: file.path, replaced_at: timestamp, content_length: file.content.length, }) } await deleteSkillsIndex(id) return { id, deleted: 'skill', files_deleted: contents.length, versions_saved: contents.length, } }) } /** * Delete one skill file (`path` set) or an entire skill (no `path`). * Previous content is recorded as a git revision (repo) or skill_versions row (legacy). * * @example * import skillDelete from 'kody:@kody/skills/skill-delete' * await skillDelete({ id: 'temp-skill', path: 'notes.md', dryRun: true }) * await skillDelete({ id: 'temp-skill', dryRun: true }) */ export default async function skillDelete( params: SkillDeleteInput = {} as SkillDeleteInput, ): Promise { const id = String(params.id ?? '').trim() if (!id) throw new Error('skill_delete requires `id`.') const path = params.path == null ? '' : String(params.path).trim() const dryRun = isDryRun(params.dryRun) const backend = await getBackend() return backend === 'repo' ? deleteRepo({ id, path, dryRun }) : deleteLegacy({ id, path, dryRun }) }