diff --git a/setup/service-defs.ts b/setup/service-defs.ts index 2edef05..997e2f1 100644 --- a/setup/service-defs.ts +++ b/setup/service-defs.ts @@ -1,7 +1,11 @@ import fs from 'fs'; import path from 'path'; +export type ServiceKind = 'primary' | 'codex' | 'review'; + export interface ServiceDef { + /** Stable topology kind used by setup/verify logic */ + kind: ServiceKind; /** systemd unit name / nohup script name */ name: string; /** launchd label */ @@ -16,45 +20,76 @@ export interface ServiceDef { extraEnv?: Record; } +interface ServiceTemplate { + kind: ServiceKind; + name: string; + launchdLabel: string; + description: string; + logName: string; + envFileName?: string; + assistantName?: string; +} + +const SERVICE_TEMPLATES: ServiceTemplate[] = [ + { + kind: 'primary', + name: 'ejclaw', + launchdLabel: 'com.ejclaw', + description: 'EJClaw Personal Assistant (Claude Code)', + logName: 'ejclaw', + }, + { + kind: 'codex', + name: 'ejclaw-codex', + launchdLabel: 'com.ejclaw-codex', + description: 'EJClaw Codex Assistant', + logName: 'ejclaw-codex', + envFileName: '.env.codex', + assistantName: 'codex', + }, + { + kind: 'review', + name: 'ejclaw-review', + launchdLabel: 'com.ejclaw-review', + description: 'EJClaw Codex Review Assistant', + logName: 'ejclaw-review', + envFileName: '.env.codex-review', + assistantName: 'codex', + }, +]; + +function materializeServiceDef( + projectRoot: string, + template: ServiceTemplate, +): ServiceDef | null { + const environmentFile = template.envFileName + ? path.join(projectRoot, template.envFileName) + : undefined; + + if (environmentFile && !fs.existsSync(environmentFile)) { + return null; + } + + return { + kind: template.kind, + name: template.name, + launchdLabel: template.launchdLabel, + description: template.description, + logName: template.logName, + environmentFile, + extraEnv: template.assistantName + ? { + ASSISTANT_NAME: template.assistantName, + } + : undefined, + }; +} + export function getServiceDefs(projectRoot: string): ServiceDef[] { - const defs: ServiceDef[] = [ - { - name: 'ejclaw', - launchdLabel: 'com.ejclaw', - description: 'EJClaw Personal Assistant (Claude Code)', - logName: 'ejclaw', - }, - ]; - - const codexEnvPath = path.join(projectRoot, '.env.codex'); - if (fs.existsSync(codexEnvPath)) { - defs.push({ - name: 'ejclaw-codex', - launchdLabel: 'com.ejclaw-codex', - description: 'EJClaw Codex Assistant', - logName: 'ejclaw-codex', - environmentFile: codexEnvPath, - extraEnv: { - ASSISTANT_NAME: 'codex', - }, - }); - } - - const reviewEnvPath = path.join(projectRoot, '.env.codex-review'); - if (fs.existsSync(reviewEnvPath)) { - defs.push({ - name: 'ejclaw-review', - launchdLabel: 'com.ejclaw-review', - description: 'EJClaw Codex Review Assistant', - logName: 'ejclaw-review', - environmentFile: reviewEnvPath, - extraEnv: { - ASSISTANT_NAME: 'codex', - }, - }); - } - - return defs; + return SERVICE_TEMPLATES.flatMap((template) => { + const def = materializeServiceDef(projectRoot, template); + return def ? [def] : []; + }); } export function getConfiguredServiceNames(projectRoot: string): string[] { diff --git a/setup/service.test.ts b/setup/service.test.ts index 8827c2f..ae49e0f 100644 --- a/setup/service.test.ts +++ b/setup/service.test.ts @@ -19,6 +19,7 @@ import { getServiceDefs, type ServiceDef } from './service-defs.js'; */ const baseServiceDef: ServiceDef = { + kind: 'primary', description: 'EJClaw Personal Assistant', launchdLabel: 'com.ejclaw', logName: 'ejclaw', @@ -126,6 +127,7 @@ describe('systemd unit generation', () => { const unit = buildSystemdUnit( { ...baseServiceDef, + kind: 'codex', environmentFile: '/srv/ejclaw/.env.codex', extraEnv: { ASSISTANT_NAME: 'codex' }, logName: 'ejclaw-codex', @@ -184,6 +186,11 @@ describe('service definitions', () => { 'ejclaw-codex', 'ejclaw-review', ]); + expect(defs.map((def) => def.kind)).toEqual([ + 'primary', + 'codex', + 'review', + ]); }); it('generates a oneshot stack restart unit', () => { diff --git a/setup/service.ts b/setup/service.ts index e3a3588..07d1077 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -59,14 +59,12 @@ export async function run(_args: string[]): Promise { fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true }); const serviceDefs = getServiceDefs(projectRoot); - if (serviceDefs.some((def) => def.name === 'ejclaw-codex')) { + for (const def of serviceDefs) { + if (def.kind === 'primary' || !def.environmentFile) { + continue; + } logger.info( - 'Detected .env.codex — will also install ejclaw-codex service', - ); - } - if (serviceDefs.some((def) => def.name === 'ejclaw-review')) { - logger.info( - 'Detected .env.codex-review — will also install ejclaw-review service', + `Detected ${path.basename(def.environmentFile)} — will also install ${def.name} service`, ); } diff --git a/setup/verify.ts b/setup/verify.ts index cbcb0a5..f490457 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -184,10 +184,10 @@ export async function run(_args: string[]): Promise { (service) => service.status === 'running', ); const codexConfigured = serviceDefs.some( - (service) => service.name === 'ejclaw-codex', + (service) => service.kind === 'codex', ); const reviewConfigured = serviceDefs.some( - (service) => service.name === 'ejclaw-review', + (service) => service.kind === 'review', ); const status =