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<MigrateToRepoResult> {
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 } : {}),
}
}