#!/usr/bin/env node
/*
* migrate-v1-issues.mjs — Phase 2 of the v1 → v3 migration.
*
* Reads the JSON dump produced by migrate-v1-extract.mjs (Phase 1,
* which runs under bare) and writes into a v3 IssueRegistry.
*
* Usage:
* node scripts/migrate-v1-issues.mjs \
* --in /tmp/v1-migration.json \
* --v3-storage /home/fleeky/pgv3-storage \
* [--dry-run] \
* [--repos repo1,repo2]
*
* v3 daemon MUST be stopped (RocksDB exclusive lock on the corestore).
* Idempotent — re-running skips already-migrated entries.
*/
import fs from 'fs'
import path from 'path'
import Corestore from 'corestore'
import { IssueRegistry } from '../lib/issue-registry.js'
import { RepoEventLog } from '../lib/services/repo-event-log.js'
function parseArgs (argv) {
const args = { inPath: null, v3Storage: null, dryRun: false, repos: null }
for (let i = 2; i < argv.length; i++) {
const a = argv[i]
if (a === '--dry-run') args.dryRun = true
else if (a === '--in') args.inPath = argv[++i]
else if (a === '--v3-storage') args.v3Storage = argv[++i]
else if (a === '--repos') args.repos = argv[++i].split(',').map(s => s.trim()).filter(Boolean)
}
if (!args.inPath) throw new Error('--in is required')
if (!args.v3Storage) throw new Error('--v3-storage is required')
return args
}
async function main () {
const args = parseArgs(process.argv)
console.log('[migrate] input :', args.inPath)
console.log('[migrate] v3 storage:', args.v3Storage)
console.log('[migrate] dry run :', args.dryRun)
if (args.repos) console.log('[migrate] repos :', args.repos.join(', '))
const dump = JSON.parse(fs.readFileSync(args.inPath, 'utf8'))
const entries = Object.entries(dump).filter(([r]) => !args.repos || args.repos.includes(r))
let totalIssues = 0, totalComments = 0
for (const [repo, b] of entries) {
totalIssues += b.issues.length
totalComments += b.comments.length
console.log(`[migrate] ${repo}: ${b.issues.length} issues, ${b.comments.length} comments`)
}
console.log(`[migrate] total: ${totalIssues} issues, ${totalComments} comments across ${entries.length} repos`)
if (args.dryRun) {
console.log('[migrate] DRY RUN — no writes performed')
return
}
const v3Store = new Corestore(path.join(args.v3Storage, 'store'))
await v3Store.ready()
const registry = new IssueRegistry(v3Store, {
indexPath: path.join(args.v3Storage, 'issue-registry-index.json')
})
console.log('[migrate] v3 IssueRegistry opened')
let wroteIssues = 0, wroteComments = 0
for (const [repo, b] of entries) {
const tracker = await registry.getForRepo(repo)
b.issues.sort((a, x) => a.id - x.id)
for (const v1 of b.issues) {
await tracker.createWithId(v1.id, {
title: v1.title || '(no title)',
body: v1.body || '',
author: v1.author || 'unknown',
status: v1.status === 'closed' ? 'closed' : 'open',
labels: Array.isArray(v1.labels) ? v1.labels : [],
images: Array.isArray(v1.images) ? v1.images : [],
createdAt: v1.createdAt,
updatedAt: v1.updatedAt
})
wroteIssues++
}
b.comments.sort((a, x) => {
if (a.issueId !== x.issueId) return a.issueId - x.issueId
return (a.createdAt || 0) - (x.createdAt || 0)
})
for (const c of b.comments) {
await tracker.commentWithId(c.issueId, c.commentId, {
body: c.body || '',
author: c.author || 'unknown',
createdAt: c.createdAt
})
wroteComments++
}
console.log(`[migrate] ${repo}: wrote ${b.issues.length} issues + ${b.comments.length} comments`)
}
// ── Emit repo-created events so mirrors learn about the new trackers.
//
// The migration writes trackers directly via IssueRegistry, bypassing
// the normal `create-repo` path that appends to RepoEventLog. Mirrors
// (e.g. stortron) subscribe to this log through RepoSync — without
// events, they never learn about migrated trackers and can't
// replicate them. Idempotent: we read existing events first and skip
// repos already announced so re-running the migration doesn't dupe.
const log = new RepoEventLog({ corestore: v3Store })
await log.ready()
const alreadyAnnounced = new Set()
if (log.length > 0) {
for await (const entry of log.entries({ start: 0 })) {
if (entry && entry.name) alreadyAnnounced.add(entry.name)
}
}
let emitted = 0
for (const [repo] of entries) {
if (alreadyAnnounced.has(repo)) continue
const tracker = await registry.getForRepo(repo)
const keyHex = tracker.keyHex
if (!keyHex) {
console.warn(`[migrate] ${repo}: tracker has no keyHex; skipping event`)
continue
}
await log.append({
op: 0, // created
name: repo,
head: '',
issueBootstrapKey: Buffer.from(keyHex, 'hex')
})
emitted++
}
console.log(`[migrate] appended ${emitted} repo-created event(s) to RepoEventLog (${log.length} total)`)
try { await log.close() } catch {}
console.log('[migrate] verifying counts...')
let verifyIssues = 0, verifyComments = 0
for (const [repo] of entries) {
const tracker = await registry.getForRepo(repo)
const list = await tracker.list({ status: 'all' })
verifyIssues += list.length
for (const issue of list) {
const comments = await tracker.getComments(issue.id)
verifyComments += comments.length
}
}
console.log(`[migrate] v3 has ${verifyIssues} issues, ${verifyComments} comments post-migration`)
if (verifyIssues === wroteIssues && verifyComments === wroteComments) {
console.log('[migrate] counts match')
} else {
console.error(`[migrate] MISMATCH wrote=${wroteIssues}/${wroteComments} v3=${verifyIssues}/${verifyComments}`)
}
try { await v3Store.close() } catch {}
}
main().catch(err => {
console.error('[migrate] ERROR:', err.message)
console.error(err.stack)
process.exit(1)
})