#!/usr/bin/env node
/**
* rebuild-refs — reconcile refs.json against published-index.json.
*
* The legacy addRef path appended refs forever. After the retention-
* policy work the index keeps only N most recent versions per app,
* but refs.json still claims app X references every tree ever
* published by X, including ones from versions long-trimmed from
* the index. This script:
*
* 1. Reads the published-index (now capped to MAX_VERSIONS per app)
* 2. Walks each retained lockfile + collects every dep.tree hash
* (plus per-arch prebuild hashes + the rootTreeHash)
* 3. Rebuilds refs.json so each app references only the hashes
* its retained versions actually need
*
* Then `pgh store gc` correctly identifies historical-version trees
* as unreferenced and frees them.
*
* Usage:
* node scripts/rebuild-refs.js [--dry-run]
*
* SAFETY:
* - Backs up refs.json before mutating.
* - Does NOT touch any identity files, the corestore, the registry,
* or any app install dir.
* - Idempotent.
*/
import fs from 'fs'
import path from 'path'
import { homedir } from 'os'
const STORE_ROOT = process.env.PGH_STORE_ROOT ||
path.join(homedir(), '.local', 'share', 'store')
const INDEX_PATH = process.env.PGH_INDEX_PATH ||
path.join(homedir(), '.config', 'pgh', 'published-index.json')
const REFS_PATH = path.join(STORE_ROOT, 'refs.json')
const dryRun = process.argv.includes('--dry-run')
if (!fs.existsSync(INDEX_PATH)) {
console.error('no index at ' + INDEX_PATH + ' — nothing to rebuild from')
process.exit(1)
}
if (!fs.existsSync(REFS_PATH)) {
console.error('no refs.json at ' + REFS_PATH)
process.exit(1)
}
const index = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'))
const before = JSON.parse(fs.readFileSync(REFS_PATH, 'utf8'))
console.log('=== rebuild-refs ===')
console.log(' index: ' + INDEX_PATH)
console.log(' refs: ' + REFS_PATH)
console.log(' dryRun: ' + dryRun)
console.log('')
// Build the canonical (app -> Set<hash>) from the index.
const want = {} // appName -> Set of tree hashes
for (const [appName, app] of Object.entries(index.apps || {})) {
const set = new Set()
for (const v of (app.versions || [])) {
if (v.rootTreeHash) set.add(v.rootTreeHash)
const lockfile = v.lockfile || {}
for (const dep of (lockfile.deps || [])) {
if (dep.tree) set.add(dep.tree)
if (dep.prebuilds && typeof dep.prebuilds === 'object') {
for (const archHash of Object.values(dep.prebuilds)) {
if (archHash) set.add(archHash)
}
}
}
}
want[appName] = set
}
// Build new refs.json: for every hash referenced by ANY kept version
// of some app, list that app under it. Hashes referenced by no app
// don't appear (they become gc-able).
const after = {}
for (const [appName, hashes] of Object.entries(want)) {
for (const hash of hashes) {
if (!after[hash]) after[hash] = []
if (!after[hash].includes(appName)) after[hash].push(appName)
}
}
// Stats
let appStats = []
for (const [appName, hashes] of Object.entries(want)) {
const beforeCount = Object.values(before).filter(arr => arr.includes(appName)).length
appStats.push({ app: appName, beforeRefs: beforeCount, afterRefs: hashes.size })
}
appStats.sort((a, b) => b.beforeRefs - a.beforeRefs)
console.log(' per-app ref counts:')
console.log(' ' + 'app'.padEnd(28) + 'before after delta')
for (const s of appStats) {
const delta = s.beforeRefs - s.afterRefs
console.log(' ' + s.app.padEnd(28) +
String(s.beforeRefs).padStart(6) + ' ' +
String(s.afterRefs).padStart(5) + ' ' +
(delta > 0 ? '-' + delta : '0'))
}
const beforeTotal = Object.keys(before).length
const afterTotal = Object.keys(after).length
console.log('')
console.log(' total referenced hashes: ' + beforeTotal + ' → ' + afterTotal +
' (-' + (beforeTotal - afterTotal) + ')')
if (dryRun) {
console.log('')
console.log(' dry-run: no changes written.')
console.log(' next: `pgh store gc --dry-run --max-age 0` to see how many bytes free up.')
process.exit(0)
}
// Write — back up first.
const backup = REFS_PATH + '.backup-' + new Date().toISOString().replace(/[:.]/g, '-')
fs.copyFileSync(REFS_PATH, backup)
console.log(' backup: ' + backup)
fs.writeFileSync(REFS_PATH, JSON.stringify(after, null, 2))
console.log(' wrote new refs.json')
console.log(' next: `pgh store gc --max-age 0` to actually reclaim disk')