import { isDryRun } from './dry-run.ts' import { 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 version_id and points at the commit that * holds that snapshot's content. */ revisions?: Array<{ id: number commit_oid: string skill_id: string path: string replaced_at: string content_length: number }> /** When true, flip registry_meta.backend to `repo` after syncing the index. */ switch_reads?: boolean /** Preview index/revision work without writing package storage. */ dryRun?: boolean } export type MigrateToRepoResult = { repo: string backend_before: string | null backend_after: string | null skills_indexed: number revisions_upserted: number skill_ids: string[] legacy_pointer: string switched: boolean dryRun?: true } /** * 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 { await ensureSchema() const backendBefore = await getMeta('backend') const dryRun = isDryRun(input.dryRun) 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.`, ) } if (dryRun) continue await upsertSkillsIndex({ id, name: meta.name, description: meta.description, files: meta.files, created_at: meta.created_at, updated_at: meta.updated_at, }) } return skillIds }, 'skills-index-sync') const liveIds = new Set(ids) // Drop stale index rows left by interrupted deletes or bulk git edits. const indexed = await listSkillsIndex() if (!dryRun) { for (const row of indexed) { if (!liveIds.has(row.id)) { await deleteSkillsIndex(row.id) } } } let revisionsUpserted = 0 for (const revision of input.revisions ?? []) { if (dryRun) { revisionsUpserted += 1 continue } await insertSkillRevision({ id: revision.id, commit_oid: revision.commit_oid, skill_id: revision.skill_id, path: revision.path, replaced_at: revision.replaced_at, content_length: revision.content_length, }) revisionsUpserted += 1 } if (!dryRun) { await setMeta( 'legacy_store', 'intact; durable home is plain-repo:skills. Legacy skills/skill_files/skill_versions tables were not deleted.', ) await setMeta('durable_home', `plain-repo:${SKILLS_REPO_NAME}`) await setMeta('migrated_at', new Date().toISOString()) } let switched = false if (input.switch_reads && !dryRun) { await setMeta('backend', 'repo') switched = true } const indexedAfter = dryRun ? ids : (await listSkillsIndex()).map((row) => row.id) const backendAfter = dryRun ? backendBefore : await getMeta('backend') return { repo: SKILLS_REPO_NAME, backend_before: backendBefore, backend_after: backendAfter, skills_indexed: indexedAfter.length, revisions_upserted: revisionsUpserted, skill_ids: ids, legacy_pointer: 'registry_meta.durable_home=plain-repo:skills; legacy SQLite tables left intact', switched, ...(dryRun ? { dryRun: true as const } : {}), } }