import { SERVICE_NAME } from '../src/platform-identity.js'; import { execFileSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import { ZFS_PREFIX, } from '../src/config.js'; import type { FirstBootInstallMode } from './first-boot.js'; import { resolvePlatformRootDataset } from './install-identity.js'; import { commandExists } from './platform.js'; import { loadSystemEnv } from './system-env.js'; import { getZfsMetadata } from './zfs-metadata.js'; export interface ExistingInstallSignals { envFile: boolean; groupsDir: boolean; serviceFile: boolean; zfsDataset: boolean; zfsMetadata: boolean; runtimeUser: boolean; } export interface ExistingInstallDetection { signals: ExistingInstallSignals; strongSignals: string[]; softSignals: string[]; existing: boolean; rootDataset: string | null; installUuid: string | null; } export interface ResolvedInstallMode { requested: FirstBootInstallMode; effective: 'fresh' | 'upgrade' | 'rescue'; detection: ExistingInstallDetection; } interface DetectorDeps { existsSync: (filePath: string) => boolean; readdirSync: (filePath: string) => string[]; execFileSync: typeof execFileSync; commandExists: (name: string) => boolean; } const DEFAULT_DEPS: DetectorDeps = { existsSync: fs.existsSync, readdirSync: (filePath) => fs.readdirSync(filePath), execFileSync, commandExists, }; function hasProjectEnv(projectRoot: string, deps: DetectorDeps): boolean { const envFile = path.join(projectRoot, '.env'); if (!deps.existsSync(envFile)) return false; try { const content = fs.readFileSync(envFile, 'utf-8'); return content .split('\n') .map((line) => line.trim()) .some((line) => line && !line.startsWith('#')); } catch { return false; } } function hasNonEmptyGroupsDir(projectRoot: string, deps: DetectorDeps): boolean { const groupsDir = path.join(projectRoot, 'groups'); if (!deps.existsSync(groupsDir)) return false; try { return deps.readdirSync(groupsDir).some((entry) => !entry.startsWith('.')); } catch { return false; } } function hasInstalledService(deps: DetectorDeps): boolean { return deps.existsSync(`/usr/local/etc/rc.d/${SERVICE_NAME}`); } function hasRuntimeUser(deps: DetectorDeps): boolean { try { deps.execFileSync('id', ['-u', SERVICE_NAME], { stdio: ['ignore', 'ignore', 'ignore'], }); return true; } catch { return false; } } function hasZfsDataset(projectRoot: string, deps: DetectorDeps): boolean { if (!deps.commandExists('zfs')) return false; try { const systemEnv = loadSystemEnv(projectRoot, deps); const zfsPrefix = systemEnv.zfsPrefix || ZFS_PREFIX; const exactDataset = systemEnv.zfsPool && zfsPrefix ? `${systemEnv.zfsPool}/${zfsPrefix}` : null; const output = deps.execFileSync('zfs', ['list', '-H', '-o', 'name'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], }); const datasets = output .split('\n') .map((line) => line.trim()) .filter(Boolean); return datasets.some( (name) => (exactDataset ? name === exactDataset || name.startsWith(`${exactDataset}/`) || name.endsWith(`${exactDataset}@`) : false) || name.endsWith(`/${zfsPrefix}`) || name === zfsPrefix || name.includes(`/${zfsPrefix}/`) || name.endsWith(`/${zfsPrefix}/pgdata`), ); } catch { return false; } } function getZfsInstallUuid( projectRoot: string, deps: DetectorDeps, ): { rootDataset: string | null; installUuid: string | null } { if (!deps.commandExists('zfs')) { return { rootDataset: null, installUuid: null }; } try { const root = resolvePlatformRootDataset(projectRoot, deps); const metadata = getZfsMetadata(root.dataset, ['install-uuid'], deps); return { rootDataset: root.dataset, installUuid: metadata['install-uuid'], }; } catch { return { rootDataset: null, installUuid: null }; } } export function detectExistingInstall( projectRoot: string, deps: Partial = {}, ): ExistingInstallDetection { const resolvedDeps = { ...DEFAULT_DEPS, ...deps }; const signals: ExistingInstallSignals = { envFile: hasProjectEnv(projectRoot, resolvedDeps), groupsDir: hasNonEmptyGroupsDir(projectRoot, resolvedDeps), serviceFile: hasInstalledService(resolvedDeps), zfsDataset: hasZfsDataset(projectRoot, resolvedDeps), zfsMetadata: false, runtimeUser: hasRuntimeUser(resolvedDeps), }; const zfsIdentity = getZfsInstallUuid(projectRoot, resolvedDeps); signals.zfsMetadata = Boolean(zfsIdentity.installUuid); const strongSignals: string[] = []; const softSignals: string[] = []; if (signals.serviceFile) strongSignals.push('service'); if (signals.zfsDataset) strongSignals.push('zfs'); if (signals.zfsMetadata) strongSignals.push('zfs-metadata'); if (signals.runtimeUser) strongSignals.push('runtime-user'); if (signals.envFile) softSignals.push('env'); if (signals.groupsDir) softSignals.push('groups'); const existing = strongSignals.length > 0 || softSignals.length >= 2; return { signals, strongSignals, softSignals, existing, rootDataset: zfsIdentity.rootDataset, installUuid: zfsIdentity.installUuid, }; } export function resolveInstallMode( requested: FirstBootInstallMode, detection: ExistingInstallDetection, ): ResolvedInstallMode { if (requested === 'auto') { return { requested, effective: detection.existing ? 'upgrade' : 'fresh', detection, }; } if (requested === 'fresh' && detection.existing) { const found = [...detection.strongSignals, ...detection.softSignals].join(', '); throw new Error( `INSTALL_MODE=fresh refused: existing install detected (${found || 'signals present'}). Use INSTALL_MODE=upgrade or INSTALL_MODE=rescue, or remove the existing installation first.`, ); } if ((requested === 'upgrade' || requested === 'rescue') && !detection.existing) { throw new Error( `INSTALL_MODE=${requested} refused: no existing install detected. Use INSTALL_MODE=auto or INSTALL_MODE=fresh for a new machine.`, ); } return { requested, effective: requested, detection, }; }