import { getBackend } from './backend.ts' import { isDryRun } from './dry-run.ts' import { ensureSchema, getFile, getSkillOrThrow, getSkillRevision, insertSkillRevision, nowIso, snapshotFile, sqlRows, sqlRun, upsertSkillsIndex, type SkillVersionRow, } from './db.ts' import { commitAndPublish, withSkillsRepoSession, parseSkillMeta, readRepoFile, restoreAndPublish, serializeSkillMeta, skillFilePath, skillMetaPath, writeRepoFiles, } from './repo.ts' export type SkillRevertInput = { version_id: number dryRun?: boolean } export type SkillRevertResult = { id: string path: string version_id: number version_saved: boolean commit_oid?: string dryRun?: true } async function revertLegacy( versionId: number, dryRun: boolean, ): Promise { await ensureSchema() const versions = await sqlRows( `SELECT id, skill_id, path, content, replaced_at FROM skill_versions WHERE id = ?`, [versionId], ) const version = versions[0] if (!version) { throw new Error(`skill_versions row not found for version_id=${versionId}.`) } await getSkillOrThrow(version.skill_id) const timestamp = nowIso() const previous = await getFile(version.skill_id, version.path) if (dryRun) { return { id: version.skill_id, path: version.path, version_id: Number(version.id), version_saved: previous != null, dryRun: true, } } let versionSaved = false if (previous) { await snapshotFile( version.skill_id, version.path, previous.content, timestamp, ) versionSaved = true await sqlRun( `UPDATE skill_files SET content = ?, updated_at = ? WHERE skill_id = ? AND path = ?`, [version.content, timestamp, version.skill_id, version.path], ) } else { await sqlRun( `INSERT INTO skill_files (skill_id, path, content, updated_at) VALUES (?, ?, ?, ?)`, [version.skill_id, version.path, version.content, timestamp], ) } await sqlRun(`UPDATE skills SET updated_at = ? WHERE id = ?`, [ timestamp, version.skill_id, ]) return { id: version.skill_id, path: version.path, version_id: Number(version.id), version_saved: versionSaved, } } async function revertRepo( versionId: number, dryRun: boolean, ): Promise { const revision = await getSkillRevision(versionId) if (!revision) { throw new Error(`skill_revisions row not found for version_id=${versionId}.`) } await getSkillOrThrow(revision.skill_id) if (dryRun) { const previousContent = await withSkillsRepoSession(async (session) => { return readRepoFile( session.id, skillFilePath(revision.skill_id, revision.path), ) }) return { id: revision.skill_id, path: revision.path, version_id: Number(revision.id), version_saved: previousContent != null, commit_oid: revision.commit_oid, dryRun: true, } } const previous = await withSkillsRepoSession(async (session) => { const filePath = skillFilePath(revision.skill_id, revision.path) const content = await readRepoFile(session.id, filePath) const timestamp = nowIso() await restoreAndPublish({ sessionId: session.id, paths: [filePath], commit: revision.commit_oid, message: `skill-revert: ${revision.skill_id} ${revision.path} from ${revision.commit_oid.slice(0, 12)}`, }) return { content, timestamp, baseCommit: session.base_commit } }) // Refresh skill.json (files list + updated_at) in a follow-up session. const commitOid = await withSkillsRepoSession(async (metaSession) => { const meta = parseSkillMeta( await readRepoFile(metaSession.id, skillMetaPath(revision.skill_id)), ) let oid = metaSession.base_commit if (meta) { const files = meta.files.includes(revision.path) ? meta.files : [...meta.files, revision.path].sort() const next = { ...meta, files, updated_at: previous.timestamp } await writeRepoFiles(metaSession.id, [ { path: skillMetaPath(revision.skill_id), content: serializeSkillMeta(next), }, ]) const published = await commitAndPublish( metaSession.id, `skill-revert-meta: ${revision.skill_id}`, ) oid = published.oid await upsertSkillsIndex({ id: revision.skill_id, name: next.name, description: next.description, files: next.files, created_at: next.created_at, updated_at: next.updated_at, }) } return oid }) let versionSaved = false if (previous.content != null) { await insertSkillRevision({ commit_oid: previous.baseCommit, skill_id: revision.skill_id, path: revision.path, replaced_at: previous.timestamp, content_length: previous.content.length, }) versionSaved = true } return { id: revision.skill_id, path: revision.path, version_id: Number(revision.id), version_saved: versionSaved, commit_oid: commitOid, } } /** * Restore a prior skill version as the current file content. * Repo backend: `repo_restore` from the revision's git commit, then publish. * Legacy backend: restore from skill_versions SQLite snapshot. * * @example * import skillRevert from 'kody:@kody/skills/skill-revert' * const result = await skillRevert({ version_id: 12, dryRun: true }) */ export default async function skillRevert( params: SkillRevertInput = {} as SkillRevertInput, ): Promise { const versionId = Number(params.version_id) if (!Number.isFinite(versionId) || versionId <= 0) { throw new Error('skill_revert requires a positive numeric `version_id`.') } const dryRun = isDryRun(params.dryRun) const backend = await getBackend() return backend === 'repo' ? revertRepo(versionId, dryRun) : revertLegacy(versionId, dryRun) }