/** * CliDeps factory — PLAN_RELAY_PHASE1 C2. Wires the abstract `CliDeps` seams (consumed by * `runCli`) to their real implementations: env-driven config, the on-disk keystore, Ed25519 * identity generation, §4.5 pairing redemption, the supervised tunnel, and OS service install. * All side effects live here so `cli.ts`/`runCli` stay pure and unit-testable. */ import { execFile } from 'node:child_process' import { mkdirSync, writeFileSync } from 'node:fs' import { homedir, userInfo } from 'node:os' import { dirname } from 'node:path' import { fileURLToPath } from 'node:url' import type { CliDeps } from '../cli.js' import type { AgentConfig } from '../config/agentConfig.js' import { loadAgentConfig } from '../config/agentConfig.js' import { openKeystore } from '../keys/keystore.js' import { generateIdentity } from '../keys/identity.js' import { redeemPairingCode } from '../enroll/pair.js' import { runTunnel } from '../transport/runTunnel.js' import { detectPlatform, installService as installServiceUnit, uninstallService as uninstallServiceUnit, type InstallDeps, type ServicePlatform, } from '../service/install.js' /** Resolve this process's own executable path (the bundled `dist/cli.js`) for the service unit. */ function selfBinPath(): string { return fileURLToPath(import.meta.url) } function runCommand(cmd: string, args: readonly string[]): Promise { return new Promise((resolve, reject) => { execFile(cmd, [...args], (err) => (err ? reject(err) : resolve())) }) } function realInstallDeps(): InstallDeps { return { writeFile: (path, content) => { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, content) }, runCommand, getuid: () => (typeof process.getuid === 'function' ? process.getuid() : 0), homedir, username: () => userInfo().username, binPath: selfBinPath, } } /** Map the current OS to its service manager, or fail fast with a clear message. */ function requirePlatform(): ServicePlatform { const platform = detectPlatform(process.platform) if (platform === null) { throw new Error(`service install/uninstall is unsupported on platform '${process.platform}'`) } return platform } /** Build the concrete CliDeps used by the real CLI entrypoint. */ export function createCliDeps(): CliDeps { return { loadConfig: () => loadAgentConfig(process.env), openKeystore: (stateDir) => openKeystore(stateDir), generateIdentity: () => generateIdentity(), redeem: (cfg: AgentConfig, code, id, ks) => redeemPairingCode(cfg.enrollUrl, code, id, ks), runTunnel: async (cfg, ks) => { const handle = await runTunnel(cfg, ks) const onSignal = (): void => { void handle.stop() } process.once('SIGTERM', onSignal) process.once('SIGINT', onSignal) return handle.done }, installService: (cfg) => installServiceUnit(cfg, requirePlatform(), realInstallDeps()), uninstallService: () => uninstallServiceUnit(requirePlatform(), { runCommand, homedir }), print: (line) => { process.stdout.write(`${line}\n`) }, } }