← Public packages
@kentcdodds/skills
Store, retrieve, edit, version, and revert reusable agent skill documents via skillList then skillGet.
src/skill-revert.ts
196 lines · 4.8 KB · TypeScriptimport { getBackend } from './backend.ts'
import {
ensureSchema,
getFile,
getSkillOrThrow,
getSkillRevision,
insertSkillRevision,
nowIso,
snapshotFile,
sqlRows,
sqlRun,
upsertSkillsIndex,
} from './db.ts'
import {
commitAndPublish,
withSkillsRepoSession,
parseSkillMeta,
readRepoFile,
restoreAndPublish,
serializeSkillMeta,
skillFilePath,
skillMetaPath,
writeRepoFiles,
} from './repo.ts'
export type SkillRevertInput = {
versionId: number
}
export type SkillRevertResult = {
id: string
path: string
versionId: number
versionSaved: boolean
commitOid?: string
}
type SqlVersionRow = {
id: number
skill_id: string
path: string
content: string
replaced_at: string
}
async function revertLegacy(versionId: number): Promise<SkillRevertResult> {
await ensureSchema()
const versions = await sqlRows<SqlVersionRow>(
`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 versionId=${versionId}.`)
}
await getSkillOrThrow(version.skill_id)
const timestamp = nowIso()
const previous = await getFile(version.skill_id, version.path)
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,
versionId: Number(version.id),
versionSaved,
}
}
async function revertRepo(versionId: number): Promise<SkillRevertResult> {
const revision = await getSkillRevision(versionId)
if (!revision) {
throw new Error(`skill_revisions row not found for versionId=${versionId}.`)
}
await getSkillOrThrow(revision.skillId)
const previous = await withSkillsRepoSession(async (session) => {
const filePath = skillFilePath(revision.skillId, revision.path)
const content = await readRepoFile(session.id, filePath)
const timestamp = nowIso()
await restoreAndPublish({
sessionId: session.id,
paths: [filePath],
commit: revision.commitOid,
message: `skill-revert: ${revision.skillId} ${revision.path} from ${revision.commitOid.slice(0, 12)}`,
})
return { content, timestamp, baseCommit: session.baseCommit }
})
// Refresh skill.json (files list + updatedAt) in a follow-up session.
const commitOid = await withSkillsRepoSession(async (metaSession) => {
const meta = parseSkillMeta(
await readRepoFile(metaSession.id, skillMetaPath(revision.skillId)),
)
let oid = metaSession.baseCommit
if (meta) {
const files = meta.files.includes(revision.path)
? meta.files
: [...meta.files, revision.path].sort()
const next = { ...meta, files, updatedAt: previous.timestamp }
await writeRepoFiles(metaSession.id, [
{
path: skillMetaPath(revision.skillId),
content: serializeSkillMeta(next),
},
])
const published = await commitAndPublish(
metaSession.id,
`skill-revert-meta: ${revision.skillId}`,
)
oid = published.oid
await upsertSkillsIndex({
id: revision.skillId,
name: next.name,
description: next.description,
files: next.files,
createdAt: next.createdAt,
updatedAt: next.updatedAt,
})
}
return oid
})
let versionSaved = false
if (previous.content != null) {
await insertSkillRevision({
commitOid: previous.baseCommit,
skillId: revision.skillId,
path: revision.path,
replacedAt: previous.timestamp,
contentLength: previous.content.length,
})
versionSaved = true
}
return {
id: revision.skillId,
path: revision.path,
versionId: Number(revision.id),
versionSaved,
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:@kentcdodds/skills/skill-revert'
* const result = await skillRevert({ versionId: 12 })
*/
export default async function skillRevert(
params: SkillRevertInput = {} as SkillRevertInput,
): Promise<SkillRevertResult> {
const versionId = Number(params.versionId)
if (!Number.isFinite(versionId) || versionId <= 0) {
throw new Error('skillRevert requires a positive numeric `versionId`.')
}
const backend = await getBackend()
return backend === 'repo' ? revertRepo(versionId) : revertLegacy(versionId)
}