import { parseArgs } from 'node:util'; import { join } from 'node:path'; import { homedir } from 'node:os'; import { chmodSync, mkdirSync } from 'node:fs'; export interface Config { port: number; bind: string; dbPath: string; dataDir: string; /** origins supplémentaires autorisées (ex. https://machine.tailnet.ts.net) */ allowedOrigins: string[]; printToken: boolean; /** racine effective de l'install Claude (~/.claude par défaut, --claude-home, ou réglage claude_home). */ claudeHome: string; /** true si --claude-home a été passé explicitement → a priorité sur le réglage claude_home. */ claudeHomeFromFlag: boolean; /** ~/.claude/projects (transcripts JSONL) : surchargeable via --claude-home (tests). */ claudeProjectsDir: string; /** ~/.claude/sessions (registre des sessions CLI vivantes). */ claudeSessionsDir: string; /** sujet VAPID des notifications Web Push (mailto: ou URL). */ vapidContact: string; /** découverte auto des repos au démarrage + périodique (désactivable via --no-discover). */ autoDiscover: boolean; } export function loadConfig(argv = process.argv.slice(2)): Config { const { values } = parseArgs({ args: argv, options: { port: { type: 'string', default: '7317' }, bind: { type: 'string', default: '127.0.0.1' }, db: { type: 'string' }, 'allow-origin': { type: 'string', multiple: true }, 'print-token': { type: 'boolean', default: false }, 'i-know-this-exposes-a-terminal': { type: 'boolean', default: false }, // racine de l'install Claude (~/.claude par défaut) : surchargée par les tests d'acceptation. 'claude-home': { type: 'string' }, // sujet VAPID des notifications push (contact requis par la spec Web Push). 'vapid-contact': { type: 'string' }, // désactive la découverte auto des repos (boot + périodique) : utilisé par les tests d'acceptation. 'no-discover': { type: 'boolean', default: false }, }, strict: true, }); const bind = values.bind ?? '127.0.0.1'; const loopback = bind === '127.0.0.1' || bind === '::1' || bind === 'localhost'; if (!loopback && !values['i-know-this-exposes-a-terminal']) { throw new Error( `Refusing to bind to ${bind}: an Arboretum server is remote code execution by design.\n` + `Use Tailscale Serve against the default 127.0.0.1 bind (recommended), or pass\n` + `--i-know-this-exposes-a-terminal if you really know what you are doing.`, ); } const dataDir = join(process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), 'arboretum'); mkdirSync(dataDir, { recursive: true }); // La DB contient des secrets (server_secret, clé privée VAPID, hashs de tokens) : le dossier de // données ne doit jamais être lisible par d'autres utilisateurs du système. chmod best-effort // (peut échouer sur certains FS Windows/montés ; le démarrage avertit alors sans bloquer). try { chmodSync(dataDir, 0o700); } catch { /* FS sans permissions POSIX : ignoré, cf. avertissement dans db.openDb */ } const claudeHomeFlag = values['claude-home']; const claudeHome = claudeHomeFlag ?? join(homedir(), '.claude'); return { port: Number(values.port), bind, dbPath: values.db ?? join(dataDir, 'arboretum.db'), dataDir, allowedOrigins: values['allow-origin'] ?? [], printToken: values['print-token'] ?? false, claudeHome, claudeHomeFromFlag: claudeHomeFlag !== undefined, claudeProjectsDir: join(claudeHome, 'projects'), claudeSessionsDir: join(claudeHome, 'sessions'), vapidContact: values['vapid-contact'] ?? 'mailto:arboretum@localhost', autoDiscover: !(values['no-discover'] ?? false), }; }