Files
arboretum/packages/server/src/index.ts
Johan LEROY de066abb54
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
feat(server): handshake token pour un shell embarquant le daemon (app de bureau)
Dans runDaemon, si ARBORETUM_EMIT_TOKEN_FD est fourni, mint un token 'desktop' et l'écrit en JSON {token,url} sur ce descripteur (pipe stdio privé parent-enfant). Additif et gardé : sans la variable, comportement strictement inchangé. Permettra à l'app Electron d'ouvrir la SPA déjà authentifiée sans écran de login. Vérifié (handshake OK), 431 tests.
2026-07-17 17:44:36 +02:00

117 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync, writeSync } 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<void> {
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, sessionArchive, repoDiscovery, worktrees, fsWatcher } = 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
sessionArchive.start(); // archivage auto des sessions terminées (rétention configurable)
worktrees.armMainCheckoutWatchers(); // P11 : temps réel du checkout principal de chaque repo
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).');
}
// Handshake pour un shell embarquant le daemon (app de bureau) : si ARBORETUM_EMIT_TOKEN_FD est
// fourni, on mint un token frais et on l'écrit en JSON sur ce descripteur (pipe stdio privé
// parent<->enfant). Additif : sans cette variable, comportement strictement inchangé. Le token ne
// transite jamais par le renderer et n'est pas affiché.
const emitFd = process.env.ARBORETUM_EMIT_TOKEN_FD;
if (emitFd) {
try {
const token = auth.createToken('desktop');
writeSync(Number(emitFd), `${JSON.stringify({ token, url })}\n`);
} catch (err) {
app.log.warn(`token handshake failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
let shuttingDown = false;
const shutdown = (signal: string): void => {
if (shuttingDown) return;
shuttingDown = true;
app.log.info(`${signal} received: draining sessions then exiting`);
discovery.stop();
sessionArchive.stop();
repoDiscovery.stop();
void fsWatcher.closeAll(); // libère les descripteurs des watchers FS
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<void> {
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);
});