OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / commands / init.js

137 lines · javascript ref:
/** 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
  }
}