feat(settings): réglages Claude CLI (binaire + ~/.claude) & notif « disponible »

- Réglages → Claude CLI : override du chemin du binaire `claude` (effet à la
  prochaine session, fallback `which claude`), diagnostic de détection, et
  override de la racine ~/.claude (effet au redémarrage). Validateurs stricts.
- Push : notifie aussi sur le front busy→idle (session redevenue disponible).
- Renomme l'état affiché idle → « disponible » / « available » (web EN/FR + VS Code) ;
  l'enum SessionActivity du protocole reste inchangé.
This commit is contained in:
2026-06-24 10:15:29 +02:00
parent 2506dfb1f3
commit b5236b41c8
16 changed files with 519 additions and 27 deletions

View File

@@ -1,4 +1,5 @@
import { execFileSync } from 'node:child_process';
import { accessSync, constants } from 'node:fs';
export interface SpawnSpec {
file: string;
@@ -12,22 +13,74 @@ export interface SpawnOptions {
resume?: { claudeSessionId: string; fork?: boolean };
/** répertoires supplémentaires à relier dans une seule session (P6) : `--add-dir <path>` répété. */
addDirs?: string[];
/** chemin explicite du binaire `claude` (réglage UI) ; sinon résolution via PATH (`which`). */
claudeBinPath?: string | null;
}
/** Diagnostic de résolution du binaire `claude` (exposé en lecture dans Réglages). */
export interface ClaudeBinDiagnostic {
/** chemin résolu du binaire, ou null si introuvable. */
path: string | null;
/** 'configured' = réglage explicite ; 'path' = trouvé via PATH ; null = introuvable. */
source: 'configured' | 'path' | null;
/** true si le binaire est présent et exécutable. */
ok: boolean;
}
let cachedClaudeBin: string | null = null;
export function resolveClaudeBin(): string {
if (cachedClaudeBin) return cachedClaudeBin;
/** Recherche `claude` dans le PATH (sans throw). null si absent. */
function findClaudeOnPath(): string | null {
try {
cachedClaudeBin = execFileSync('which', ['claude'], { encoding: 'utf8' }).trim();
return execFileSync('which', ['claude'], { encoding: 'utf8' }).trim() || null;
} catch {
return null;
}
}
function isExecutable(path: string): boolean {
try {
accessSync(path, constants.X_OK);
return true;
} catch {
return false;
}
}
/**
* Résout le binaire `claude`. Si `configuredPath` est fourni (réglage UI), il est utilisé tel quel
* — validé exécutable, message clair sinon — et JAMAIS mis en cache (modifiable à chaud). Sinon :
* `which claude`, mis en cache. Un service systemd/launchd démarre avec un PATH minimal sans
* ~/.local/bin → `which claude` y échoue ; d'où le réglage de chemin explicite (et le PATH figé par
* `arboretum install`).
*/
export function resolveClaudeBin(configuredPath?: string | null): string {
if (configuredPath) {
if (!isExecutable(configuredPath)) {
throw new Error(`Configured Claude CLI path is not executable: ${configuredPath}`);
}
return configuredPath;
}
if (cachedClaudeBin) return cachedClaudeBin;
const found = findClaudeOnPath();
if (!found) {
throw new Error(
'Claude Code CLI not found in PATH. Install it first: https://code.claude.com/docs/en/quickstart',
);
}
cachedClaudeBin = found;
return cachedClaudeBin;
}
/** Diagnostic non-throwing pour l'UI (Réglages) : recalculé à chaque appel, jamais caché. */
export function diagnoseClaudeBin(configuredPath?: string | null): ClaudeBinDiagnostic {
if (configuredPath) {
return { path: configuredPath, source: 'configured', ok: isExecutable(configuredPath) };
}
const found = findClaudeOnPath();
return found ? { path: found, source: 'path', ok: true } : { path: null, source: null, ok: false };
}
/** Module volontairement abstrait : le plan B « BYO API key / Agent SDK » se brancherait ici. */
export function buildSpawnSpec(opts: SpawnOptions): SpawnSpec {
const env: NodeJS.ProcessEnv = {
@@ -46,5 +99,5 @@ export function buildSpawnSpec(opts: SpawnOptions): SpawnSpec {
}
// Session de groupe : relie plusieurs repos/worktrees dans une seule session (P6).
for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
return { file: resolveClaudeBin(), args, env };
return { file: resolveClaudeBin(opts.claudeBinPath), args, env };
}