#!/usr/bin/env node
/**
* gen-modules-manifest.js — regenerate hyper-fleeky/modules.json
*
* Scans hyper-fleeky/ for git repos and records where each one lives, which
* bare repo backs it, and what it currently is. The manifest is GENERATED —
* never hand-edit it, re-run this instead. Hand-maintenance is why the
* predecessors (projects.json, dependencies.json, sync-submodules.sh) rotted
* into describing a layout that no longer exists.
*
* Deliberately records NO commit SHAs. Pins in this repo have never been
* maintained (pear-git drifted 108 commits while submodule tooling was
* broken), and the workspace consumes these modules by PATH via `file:..`
* deps, not by pin — so a SHA here would be maintenance burden buying
* nothing. Modules tracked as real submodules keep their pins in git, and
* are marked "submodule" below.
*
* node scripts/gen-modules-manifest.js # write manifest
* node scripts/gen-modules-manifest.js --check # exit 1 if stale (CI)
*/
const fs = require('fs')
const path = require('path')
const { execFileSync } = require('child_process')
const ROOT = path.resolve(__dirname, '..')
const SCAN = path.join(ROOT, 'hyper-fleeky')
const BARES = '/home/fleeky/git-repos'
const OUT = path.join(SCAN, 'modules.json')
function git (cwd, args) {
try {
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
} catch {
return null
}
}
function isRepo (dir) {
return fs.existsSync(path.join(dir, '.git'))
}
// Paths the parent repo records as gitlinks — these keep pins in git and are
// not the manifest's business, but we list them so the manifest is a complete
// map rather than a partial one (partial tracking is what created the mess).
function submodulePaths () {
const out = git(ROOT, ['ls-files', '-s', 'hyper-fleeky'])
if (!out) return new Set()
return new Set(
out.split('\n')
.filter(l => l.startsWith('160000'))
.map(l => l.split('\t')[1])
)
}
// A name-matched bare repo is NOT proof of the right target: packages/pgh
// pinned a commit absent from pgh.git but present in pgh-legacy.git, and
// hypernote is backed by hyper-note.git. Only claim a backing repo when the
// module's current HEAD actually resolves inside it.
function findBare (name, dir) {
const head = git(dir, ['rev-parse', 'HEAD'])
const origin = git(dir, ['remote', 'get-url', 'origin'])
const candidates = []
if (origin) candidates.push(origin)
candidates.push(path.join(BARES, name + '.git'))
candidates.push(path.join(BARES, name.replace(/^hyper-?/, 'hyper-') + '.git'))
// hypernote -> hyper-note.git style split
if (/^hyper[a-z]/.test(name)) candidates.push(path.join(BARES, name.replace(/^hyper/, 'hyper-') + '.git'))
const seen = new Set()
let fallback = null
for (const c of candidates) {
if (!c || seen.has(c)) continue
seen.add(c)
if (!fs.existsSync(c)) continue
if (!head) return { repo: c, verified: false }
const ok = git(c, ['cat-file', '-e', head + '^{commit}']) !== null
if (ok) return { repo: c, verified: true }
// Bare exists but does not hold our HEAD => unpushed work, not a missing
// repo. Keep it as the best-known target so the report can say so.
if (!fallback) fallback = c
}
return { repo: fallback, verified: false }
}
function version (dir) {
try {
return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version || null
} catch {
return null
}
}
function scan () {
const subs = submodulePaths()
const modules = {}
for (const name of fs.readdirSync(SCAN).sort()) {
const dir = path.join(SCAN, name)
let st
try { st = fs.lstatSync(dir) } catch { continue }
if (st.isSymbolicLink() || !st.isDirectory()) continue
if (!isRepo(dir)) continue
const rel = 'hyper-fleeky/' + name
const { repo, verified } = findBare(name, dir)
modules[name] = {
path: rel,
tracking: subs.has(rel) ? 'submodule' : 'manifest',
branch: git(dir, ['rev-parse', '--abbrev-ref', 'HEAD']),
version: version(dir),
repo,
// false => the backing repo does not contain this module's HEAD, i.e.
// there is unpushed local work. Treat as "not safely reproducible yet".
backed: verified,
lastCommit: git(dir, ['log', '-1', '--format=%ad', '--date=short'])
}
}
return modules
}
const modules = scan()
const manifest = {
_comment: 'GENERATED by scripts/gen-modules-manifest.js — do not hand-edit; re-run the generator.',
_supersedes: ['hyper-fleeky/projects.json', 'hyper-fleeky/dependencies.json', 'sync-submodules.sh'],
generatedFrom: 'hyper-fleeky/',
count: Object.keys(modules).length,
modules
}
const json = JSON.stringify(manifest, null, 2) + '\n'
if (process.argv.includes('--check')) {
const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : ''
if (current !== json) {
console.error('modules.json is STALE — run: node scripts/gen-modules-manifest.js')
process.exit(1)
}
console.log('modules.json up to date (' + manifest.count + ' modules)')
} else {
fs.writeFileSync(OUT, json)
const unbacked = Object.entries(modules).filter(([, m]) => !m.backed)
console.log('wrote ' + OUT + ' (' + manifest.count + ' modules)')
if (unbacked.length) {
console.log('\nWARNING — these modules\' HEAD is not in any backing repo:')
for (const [n, m] of unbacked) {
console.log(' ' + n + (m.repo ? ' UNPUSHED -> ' + m.repo : ' NO BARE REPO FOUND'))
}
console.log('\nUnpushed work exists only on this disk, and cannot be pinned or synced.')
}
}