/** pgh init — one-time setup on a fresh machine */
import process from '#node:process'
import fs from '#node:fs'
import path from '#node:path'
import { homedir } from '#node:os'
import { TreeStore, DEFAULT_ROOT as DEFAULT_STORE } from '../store.js'
import { parseBootstrap, BOOTSTRAP_VERSION, BOOTSTRAP_TOPIC } from '../bootstrap.js'
import { loadOrCreateIdentity } from '../identity.js'
import { checkFilesystem, formatReport } from '../preflight.js'
const CONFIG_DIR = path.join(homedir(), '.config', 'pgh')
const HYPERIDENT_HOME = path.join(homedir(), '.hyperident')
const LOCAL_BIN = path.join(homedir(), '.local', 'bin')
const LOCAL_SHARE = path.join(homedir(), '.local', 'share')
export const usage = `pgh init — one-time setup on this machine
pgh init [--bootstrap <url>] [--offline] [--force] [--skip-preflight] [-q]
Creates the tree store, loads/creates a hyperident identity, and writes
~/.config/pgh/config.json. --offline skips network bootstrap; --force
re-runs even if already initialized. A filesystem preflight (unix sockets +
symlinks) runs first; --skip-preflight bypasses it (only if you know your
fs is capable).`
export async function run (client, args) {
const bootstrapIdx = args.indexOf('--bootstrap')
const bootstrapSpec = (bootstrapIdx !== -1 && args[bootstrapIdx + 1]) || null
const offline = args.includes('--offline')
const force = args.includes('--force')
const quiet = args.includes('--quiet') || args.includes('-q')
const skipPreflight = args.includes('--skip-preflight')
const log = quiet ? () => {} : (msg) => console.log(msg)
const configPath = path.join(CONFIG_DIR, 'config.json')
if (fs.existsSync(configPath) && !force) {
return {
alreadyInitialized: true,
configPath,
message: 'pgh is already initialized. Use --force to re-init.'
}
}
log('Initializing pgh...')
// 0. Filesystem preflight — pgh needs unix sockets + symlinks (see
// preflight.js). Run against the store root before creating anything, so
// a fs that can't host pgh (exfat/NTFS/FAT, or WSL's /mnt Windows-drive
// bridge) fails fast with a clear message instead of a mid-materialize /
// socket-bind crash later.
if (!skipPreflight) {
fs.mkdirSync(path.dirname(DEFAULT_STORE), { recursive: true })
const report = await checkFilesystem(DEFAULT_STORE)
if (!quiet) log(formatReport(report, DEFAULT_STORE))
if (!report.ok) {
process.stderr.write(
'\nERROR: this filesystem cannot host pgh: ' + report.errors.join('; ') + '.\n' +
'pgh requires a POSIX filesystem (ext4/btrfs/xfs) with unix domain sockets\n' +
'and symlinks. exfat/NTFS/FAT and the WSL Windows-drive bridge (/mnt/c via\n' +
'drvfs/9p) do NOT qualify.\n' +
(report.onWindowsDrive
? ' -> Detected a Windows drive (/mnt). On WSL2, keep pgh under $HOME on\n' +
' the Linux filesystem; do not put the store on /mnt/*.\n'
: ' -> Move HOME/PGH_STORE_ROOT onto a native Linux filesystem.\n') +
'Override with --skip-preflight only if you know your fs is capable.\n'
)
process.exit(1)
}
}
// 1. Create baseline dirs (NOT ~/.pgh/identity/ — that's hyperident's job)
for (const d of [CONFIG_DIR, LOCAL_BIN, LOCAL_SHARE]) {
fs.mkdirSync(d, { recursive: true })
}
// 2. Ensure tree store exists
const store = new TreeStore({ root: DEFAULT_STORE })
store.init()
log(' store: ' + DEFAULT_STORE)
// 3. Resolve the bootstrap spec
let bootstrap
try {
bootstrap = parseBootstrap(bootstrapSpec)
} catch (err) {
process.stderr.write('ERROR: bad --bootstrap value: ' + err.message + '\n')
process.exit(1)
}
log(' bootstrap: ' + bootstrap.label)
// 4. Load (or create) identity via hyperident. This is the SAME
// identity used by every hyperident-aware app on this machine —
// pgh doesn't get its own keypair; it shares ~/.hyperident/.
const identity = await loadOrCreateIdentity(HYPERIDENT_HOME)
const identityCreated = identity.source === 'raw' || !fs.existsSync(path.join(HYPERIDENT_HOME, 'identity.json'))
log(' identity: ' + identity.source + ' (' + identity.publicKeyHex.slice(0, 12) + '...)')
// 5. Write config
const config = {
initializedAt: new Date().toISOString(),
bootstrapVersion: BOOTSTRAP_VERSION,
bootstrapTopicHex: BOOTSTRAP_TOPIC.toString('hex'),
bootstrapSpec: bootstrapSpec || null,
store: DEFAULT_STORE,
offline
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
log(' config: ' + configPath)
// 6. Network bootstrap — defer until real fetcher exists (0.5k landing)
// For now, --offline and network-less init are equivalent: no
// content pulled. Once pgh-core itself is published via pgh
// publish + a network fetcher exists, `pgh init` can materialize
// pgh-core into the store from the bootstrap topic.
if (offline) {
log(' network: skipped (--offline)')
} else if (bootstrapSpec || !identityCreated) {
// Stub — log intent, skip actual network work for MVP
log(' network: stub — would join ' + bootstrap.label + ' to fetch pgh-core tree')
log(' (full network bootstrap lands with the fetcher in 0.5k+)')
}
return {
initialized: true,
config,
configPath,
identitySource: identity.source,
devicePubkey: identity.publicKeyHex,
store: DEFAULT_STORE,
identityCreated
}
}