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<SkillRevertResult> {
await ensureSchema()
const versions = await sqlRows<SkillVersionRow>(
`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<SkillRevertResult> {
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<SkillRevertResult> {
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)
}