refactor: make service defs the setup SSOT

This commit is contained in:
Eyejoker
2026-03-31 06:20:15 +09:00
parent 4bfd3f363f
commit 4003988ff4
4 changed files with 87 additions and 47 deletions

View File

@@ -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<string, string>;
}
export function getServiceDefs(projectRoot: string): ServiceDef[] {
const defs: ServiceDef[] = [
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',
},
];
const codexEnvPath = path.join(projectRoot, '.env.codex');
if (fs.existsSync(codexEnvPath)) {
defs.push({
{
kind: 'codex',
name: 'ejclaw-codex',
launchdLabel: 'com.ejclaw-codex',
description: 'EJClaw Codex Assistant',
logName: 'ejclaw-codex',
environmentFile: codexEnvPath,
extraEnv: {
ASSISTANT_NAME: 'codex',
envFileName: '.env.codex',
assistantName: 'codex',
},
});
}
const reviewEnvPath = path.join(projectRoot, '.env.codex-review');
if (fs.existsSync(reviewEnvPath)) {
defs.push({
{
kind: 'review',
name: 'ejclaw-review',
launchdLabel: 'com.ejclaw-review',
description: 'EJClaw Codex Review Assistant',
logName: 'ejclaw-review',
environmentFile: reviewEnvPath,
extraEnv: {
ASSISTANT_NAME: 'codex',
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 defs;
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[] {
return SERVICE_TEMPLATES.flatMap((template) => {
const def = materializeServiceDef(projectRoot, template);
return def ? [def] : [];
});
}
export function getConfiguredServiceNames(projectRoot: string): string[] {

View File

@@ -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', () => {

View File

@@ -59,14 +59,12 @@ export async function run(_args: string[]): Promise<void> {
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
const serviceDefs = getServiceDefs(projectRoot);
if (serviceDefs.some((def) => def.name === 'ejclaw-codex')) {
logger.info(
'Detected .env.codex — will also install ejclaw-codex service',
);
for (const def of serviceDefs) {
if (def.kind === 'primary' || !def.environmentFile) {
continue;
}
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`,
);
}

View File

@@ -184,10 +184,10 @@ export async function run(_args: string[]): Promise<void> {
(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 =