clawdie-ai/setup/system-env.ts
Operator & Codex 975f37f895 feat(install): add versioned setup and system contracts
---
Build: pass | Tests: pass — Tests  2000 passed (2000)
2026-04-27 10:06:44 +02:00

245 lines
7.3 KiB
TypeScript

import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import type { FirstBootZfsLayout } from './first-boot.js';
import { commandExists } from './platform.js';
import { normalizeResourceId } from '../src/platform-layout.js';
export interface SystemEnvConfig {
systemSchemaVersion: number;
networkExternalIf: string | null;
networkInternalIf: string | null;
tailscaleIf: string | null;
zfsPool: string | null;
zfsLayout: FirstBootZfsLayout | null;
zfsDataDisks: number | null;
zfsHotSpares: number | null;
zfsDisks: string[];
zfsSpareDisks: string[];
zfsPrefix: string | null;
gpuDevice: string | null;
sndDevice: string | null;
}
export interface ResolvedSystemEnv extends SystemEnvConfig {
detected: {
networkExternalIf: boolean;
networkInternalIf: boolean;
tailscaleIf: boolean;
gpuDevice: boolean;
sndDevice: boolean;
};
}
interface SystemEnvDeps {
commandExists: (name: string) => boolean;
execFileSync: typeof execFileSync;
existsSync: (filePath: string) => boolean;
readFileSync: typeof fs.readFileSync;
}
const DEFAULT_DEPS: SystemEnvDeps = {
commandExists,
execFileSync,
existsSync: fs.existsSync,
readFileSync: fs.readFileSync,
};
export const SYSTEM_ENV_SCHEMA_VERSION = 1;
function parseScalar(content: string, key: string): string | null {
const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
if (!match) return null;
const raw = match[1].trim().replace(/^['"]|['"]$/gu, '');
return raw || null;
}
function parseInteger(content: string, key: string): number | null {
const value = parseScalar(content, key);
if (!value) return null;
if (!/^\d+$/u.test(value)) return null;
return Number.parseInt(value, 10);
}
function parseCsv(content: string, key: string): string[] {
const value = parseScalar(content, key);
if (!value) return [];
return value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean);
}
function parseLayout(content: string): FirstBootZfsLayout | null {
const value = (parseScalar(content, 'ZFS_LAYOUT') || '').toLowerCase();
if (!value) return null;
if (
value === 'single' ||
value === 'mirror' ||
value === 'raidz1' ||
value === 'raidz2'
) {
return value;
}
return null;
}
export function loadSystemEnv(
projectRoot: string = process.cwd(),
deps: Partial<SystemEnvDeps> = {},
): SystemEnvConfig {
const resolvedDeps = { ...DEFAULT_DEPS, ...deps };
const systemEnvFile = path.join(projectRoot, 'system.env');
if (!resolvedDeps.existsSync(systemEnvFile)) {
return {
systemSchemaVersion: SYSTEM_ENV_SCHEMA_VERSION,
networkExternalIf: null,
networkInternalIf: null,
tailscaleIf: null,
zfsPool: null,
zfsLayout: null,
zfsDataDisks: null,
zfsHotSpares: null,
zfsDisks: [],
zfsSpareDisks: [],
zfsPrefix: null,
gpuDevice: null,
sndDevice: null,
};
}
const content = resolvedDeps.readFileSync(systemEnvFile, 'utf-8');
const parsedSchema = parseInteger(content, 'SYSTEM_SCHEMA_VERSION');
if (parsedSchema !== null && parsedSchema !== SYSTEM_ENV_SCHEMA_VERSION) {
throw new Error(
`system.env SYSTEM_SCHEMA_VERSION=${parsedSchema} is not supported. Expected ${SYSTEM_ENV_SCHEMA_VERSION}.`,
);
}
return {
systemSchemaVersion: parsedSchema ?? SYSTEM_ENV_SCHEMA_VERSION,
networkExternalIf: parseScalar(content, 'NETWORK_EXTERNAL_IF'),
networkInternalIf: parseScalar(content, 'NETWORK_INTERNAL_IF'),
tailscaleIf: parseScalar(content, 'TAILSCALE_IF'),
zfsPool: parseScalar(content, 'ZFS_POOL'),
zfsLayout: parseLayout(content),
zfsDataDisks: parseInteger(content, 'ZFS_DATA_DISKS'),
zfsHotSpares: parseInteger(content, 'ZFS_HOT_SPARES'),
zfsDisks: parseCsv(content, 'ZFS_DISKS'),
zfsSpareDisks: parseCsv(content, 'ZFS_SPARE_DISKS'),
zfsPrefix: (() => {
const raw = parseScalar(content, 'ZFS_PREFIX');
return raw ? normalizeResourceId(raw) || null : null;
})(),
gpuDevice: parseScalar(content, 'GPU_DEVICE'),
sndDevice: parseScalar(content, 'SND_DEVICE'),
};
}
function detectDefaultRouteInterface(deps: SystemEnvDeps): string | null {
if (!deps.commandExists('route')) return null;
try {
const out = deps.execFileSync('route', ['-n', 'get', 'default'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
return out.match(/^\s*interface:\s*(\S+)/mu)?.[1]?.trim() || null;
} catch {
return null;
}
}
function detectInterfaces(deps: SystemEnvDeps): string[] {
if (!deps.commandExists('ifconfig')) return [];
try {
const out = deps.execFileSync('ifconfig', ['-l'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
return out
.trim()
.split(/\s+/u)
.map((entry) => entry.trim())
.filter(Boolean);
} catch {
return [];
}
}
function detectInternalIf(deps: SystemEnvDeps): string | null {
const interfaces = detectInterfaces(deps);
if (interfaces.includes('warden0')) return 'warden0';
const bridge = interfaces.find((name) => /^bridge\d+$/u.test(name));
return bridge || null;
}
function detectTailscaleIf(deps: SystemEnvDeps): string | null {
const interfaces = detectInterfaces(deps);
return interfaces.includes('tailscale0') ? 'tailscale0' : null;
}
function detectGpuDevice(deps: SystemEnvDeps): string | null {
if (deps.existsSync('/dev/drm0')) return 'drm0';
if (deps.existsSync('/dev/dri/card0')) return 'drm0';
if (!deps.commandExists('pciconf')) return null;
try {
const out = deps.execFileSync('pciconf', ['-lv'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
if (/(VGA|display)/iu.test(out)) {
return 'auto-gpu';
}
} catch {
// ignore
}
return null;
}
function detectSndDevice(deps: SystemEnvDeps): string | null {
if (deps.existsSync('/dev/pcm0')) return 'pcm0';
if (deps.commandExists('cat') && deps.existsSync('/dev/sndstat')) {
try {
const out = deps.execFileSync('cat', ['/dev/sndstat'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
const match = out.match(/\b(pcm\d+)\b/u);
return match?.[1] || null;
} catch {
return null;
}
}
return null;
}
export function resolveSystemEnv(
projectRoot: string = process.cwd(),
deps: Partial<SystemEnvDeps> = {},
): ResolvedSystemEnv {
const resolvedDeps = { ...DEFAULT_DEPS, ...deps };
const loaded = loadSystemEnv(projectRoot, resolvedDeps);
const detectedExternal = !loaded.networkExternalIf;
const detectedInternal = !loaded.networkInternalIf;
const detectedTailscale = !loaded.tailscaleIf;
const detectedGpu = !loaded.gpuDevice;
const detectedSnd = !loaded.sndDevice;
return {
...loaded,
networkExternalIf:
loaded.networkExternalIf || detectDefaultRouteInterface(resolvedDeps),
networkInternalIf: loaded.networkInternalIf || detectInternalIf(resolvedDeps),
tailscaleIf: loaded.tailscaleIf || detectTailscaleIf(resolvedDeps),
gpuDevice: loaded.gpuDevice || detectGpuDevice(resolvedDeps),
sndDevice: loaded.sndDevice || detectSndDevice(resolvedDeps),
detected: {
networkExternalIf: detectedExternal,
networkInternalIf: detectedInternal,
tailscaleIf: detectedTailscale,
gpuDevice: detectedGpu,
sndDevice: detectedSnd,
},
};
}