#!/usr/bin/env node import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadConfig, type Config } from './config.js'; import { openDb, type Db } from './db/index.js'; import { buildApp } from './app.js'; import { readClaudeHome } from './core/claude-settings.js'; import { runInstall, runUninstall, runStatus, printUsage, printTokenBanner } from './cli/install.js'; const pkg = JSON.parse( readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'), ) as { version: string }; /** * Applique l'override `claude_home` (réglage UI) sur la config runtime, sauf si --claude-home a été * passé explicitement (le flag gagne). Lu une seule fois au démarrage → un changement via l'UI prend * effet au redémarrage du daemon (comme l'intervalle de scan). Mute `config`. */ function applyClaudeHomeOverride(config: Config, db: Db): void { if (config.claudeHomeFromFlag) return; const override = readClaudeHome(db); if (!override) return; config.claudeHome = override; config.claudeProjectsDir = join(override, 'projects'); config.claudeSessionsDir = join(override, 'sessions'); } /** Démarre le daemon : écoute HTTP, scan des sessions, drain propre au SIGTERM/SIGINT. */ export async function runDaemon(config: Config): Promise { const db = openDb(config.dbPath); applyClaudeHomeOverride(config, db); // override claude_home (réglage UI) avant de câbler les services const { app, auth, manager, discovery, repoDiscovery } = buildApp(config, db, pkg.version); const bootstrapToken = auth.ensureBootstrapToken(); await app.listen({ port: config.port, host: config.bind }); discovery.start(); // scan initial + rafraîchissement périodique des sessions découvertes if (config.autoDiscover) repoDiscovery.start(); // découverte auto des repos : scan au boot + re-scan périodique const url = `http://${config.bind === '0.0.0.0' ? '127.0.0.1' : config.bind}:${config.port}`; app.log.info(`Arboretum v${pkg.version} — ${url}`); if (bootstrapToken) { // Affiché une seule fois : le hash seul est stocké. printTokenBanner(bootstrapToken, url); } else if (config.printToken) { console.log('Tokens are stored hashed and cannot be re-printed. Create a new one from Settings (or reset the db).'); } let shuttingDown = false; const shutdown = (signal: string): void => { if (shuttingDown) return; shuttingDown = true; app.log.info(`${signal} received — draining sessions then exiting`); discovery.stop(); repoDiscovery.stop(); manager.shutdown(); setTimeout(() => { void app.close().then(() => process.exit(0)); }, 1000).unref(); }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); } // Routeur de sous-commandes. On inspecte argv[0] AVANT loadConfig (parseArgs strict throw sur // un positionnel inconnu). Aucune sous-commande (ou un flag en tête) → daemon : rétrocompat stricte // de `arboretum`, `arboretum --port 8080`, `npx @johanleroy/git-arboretum --allow-origin …`. async function main(): Promise { const argv = process.argv.slice(2); const cmd = argv[0]; switch (cmd) { case 'install': return runInstall(argv.slice(1)); case 'uninstall': return runUninstall(argv.slice(1)); case 'status': return runStatus(argv.slice(1)); case 'serve': return runDaemon(loadConfig(argv.slice(1))); case 'help': case '--help': case '-h': printUsage(pkg.version); return; default: if (cmd && !cmd.startsWith('-')) { console.error(`Unknown command: ${cmd}\n`); printUsage(pkg.version); process.exit(1); } return runDaemon(loadConfig(argv)); } } main().catch((err) => { console.error(err instanceof Error ? err.message : err); process.exit(1); });