← Public packages
@kentcdodds/skills
Store, retrieve, edit, version, and revert reusable agent skill documents via skillList then skillGet.
src/migrate-to-repo.ts
134 lines · 3.4 KB · TypeScriptimport {
deleteSkillsIndex,
ensureSchema,
getMeta,
insertSkillRevision,
listSkillsIndex,
setMeta,
upsertSkillsIndex,
} from './db.ts'
import {
listSkillIdsFromRepo,
withSkillsRepoSession,
parseSkillMeta,
readRepoFile,
SKILLS_REPO_NAME,
skillMetaPath,
} from './repo.ts'
export type MigrateToRepoInput = {
/**
* Revision map produced while replaying legacy skill_versions as git commits.
* Each entry preserves the legacy versionId and points at the commit that
* holds that snapshot's content.
*/
revisions?: Array<{
id: number
commitOid: string
skillId: string
path: string
replacedAt: string
contentLength: number
}>
/** When true, flip registry_meta.backend to `repo` after syncing the index. */
switchReads?: boolean
}
export type MigrateToRepoResult = {
repo: string
backendBefore: string | null
backendAfter: string | null
skillsIndexed: number
revisionsUpserted: number
skillIds: string[]
legacyPointer: string
switched: boolean
}
/**
* After the plain `skills` repo has been populated (copy-first), sync the
* packageStorage index/revision projections and optionally switch reads from
* the legacy SQLite store to the repo. Never deletes legacy tables.
*/
export default async function migrateToRepo(
input: MigrateToRepoInput = {},
): Promise<MigrateToRepoResult> {
await ensureSchema()
const backendBefore = await getMeta('backend')
// Mint a fresh session per sync. A shared conversationId plus discard-after-use
// lets concurrent repo.pushed handlers (or a later resume) hit a discarded
// session. Session-branch refs are already ignored in on-repo-pushed.
const ids = await withSkillsRepoSession(async (session) => {
const skillIds = await listSkillIdsFromRepo(session.id)
for (const id of skillIds) {
const meta = parseSkillMeta(
await readRepoFile(session.id, skillMetaPath(id)),
)
if (!meta) {
throw new Error(
`Missing or invalid ${skillMetaPath(id)} in skills repo.`,
)
}
await upsertSkillsIndex({
id,
name: meta.name,
description: meta.description,
files: meta.files,
createdAt: meta.createdAt,
updatedAt: meta.updatedAt,
})
}
return skillIds
})
const liveIds = new Set(ids)
// Drop stale index rows left by interrupted deletes or bulk git edits.
const indexed = await listSkillsIndex()
for (const row of indexed) {
if (!liveIds.has(row.id)) {
await deleteSkillsIndex(row.id)
}
}
let revisionsUpserted = 0
for (const revision of input.revisions ?? []) {
await insertSkillRevision({
id: revision.id,
commitOid: revision.commitOid,
skillId: revision.skillId,
path: revision.path,
replacedAt: revision.replacedAt,
contentLength: revision.contentLength,
})
revisionsUpserted += 1
}
await setMeta(
'legacyStore',
'intact; durable home is plain-repo:skills. Legacy skills/skill_files/skill_versions tables were not deleted.',
)
await setMeta('durableHome', `plain-repo:${SKILLS_REPO_NAME}`)
await setMeta('migratedAt', new Date().toISOString())
let switched = false
if (input.switchReads) {
await setMeta('backend', 'repo')
switched = true
}
const indexedAfter = await listSkillsIndex()
const backendAfter = await getMeta('backend')
return {
repo: SKILLS_REPO_NAME,
backendBefore,
backendAfter,
skillsIndexed: indexedAfter.length,
revisionsUpserted,
skillIds: ids,
legacyPointer:
'registry_meta.durableHome=plain-repo:skills; legacy SQLite tables left intact',
switched,
}
}