refactor: make service defs the setup SSOT
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
export type ServiceKind = 'primary' | 'codex' | 'review';
|
||||||
|
|
||||||
export interface ServiceDef {
|
export interface ServiceDef {
|
||||||
|
/** Stable topology kind used by setup/verify logic */
|
||||||
|
kind: ServiceKind;
|
||||||
/** systemd unit name / nohup script name */
|
/** systemd unit name / nohup script name */
|
||||||
name: string;
|
name: string;
|
||||||
/** launchd label */
|
/** launchd label */
|
||||||
@@ -16,45 +20,76 @@ export interface ServiceDef {
|
|||||||
extraEnv?: Record<string, string>;
|
extraEnv?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[] {
|
export function getServiceDefs(projectRoot: string): ServiceDef[] {
|
||||||
const defs: ServiceDef[] = [
|
return SERVICE_TEMPLATES.flatMap((template) => {
|
||||||
{
|
const def = materializeServiceDef(projectRoot, template);
|
||||||
name: 'ejclaw',
|
return def ? [def] : [];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfiguredServiceNames(projectRoot: string): string[] {
|
export function getConfiguredServiceNames(projectRoot: string): string[] {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { getServiceDefs, type ServiceDef } from './service-defs.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const baseServiceDef: ServiceDef = {
|
const baseServiceDef: ServiceDef = {
|
||||||
|
kind: 'primary',
|
||||||
description: 'EJClaw Personal Assistant',
|
description: 'EJClaw Personal Assistant',
|
||||||
launchdLabel: 'com.ejclaw',
|
launchdLabel: 'com.ejclaw',
|
||||||
logName: 'ejclaw',
|
logName: 'ejclaw',
|
||||||
@@ -126,6 +127,7 @@ describe('systemd unit generation', () => {
|
|||||||
const unit = buildSystemdUnit(
|
const unit = buildSystemdUnit(
|
||||||
{
|
{
|
||||||
...baseServiceDef,
|
...baseServiceDef,
|
||||||
|
kind: 'codex',
|
||||||
environmentFile: '/srv/ejclaw/.env.codex',
|
environmentFile: '/srv/ejclaw/.env.codex',
|
||||||
extraEnv: { ASSISTANT_NAME: 'codex' },
|
extraEnv: { ASSISTANT_NAME: 'codex' },
|
||||||
logName: 'ejclaw-codex',
|
logName: 'ejclaw-codex',
|
||||||
@@ -184,6 +186,11 @@ describe('service definitions', () => {
|
|||||||
'ejclaw-codex',
|
'ejclaw-codex',
|
||||||
'ejclaw-review',
|
'ejclaw-review',
|
||||||
]);
|
]);
|
||||||
|
expect(defs.map((def) => def.kind)).toEqual([
|
||||||
|
'primary',
|
||||||
|
'codex',
|
||||||
|
'review',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates a oneshot stack restart unit', () => {
|
it('generates a oneshot stack restart unit', () => {
|
||||||
|
|||||||
@@ -59,14 +59,12 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
||||||
|
|
||||||
const serviceDefs = getServiceDefs(projectRoot);
|
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(
|
logger.info(
|
||||||
'Detected .env.codex — will also install ejclaw-codex service',
|
`Detected ${path.basename(def.environmentFile)} — will also install ${def.name} service`,
|
||||||
);
|
|
||||||
}
|
|
||||||
if (serviceDefs.some((def) => def.name === 'ejclaw-review')) {
|
|
||||||
logger.info(
|
|
||||||
'Detected .env.codex-review — will also install ejclaw-review service',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,10 +184,10 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
(service) => service.status === 'running',
|
(service) => service.status === 'running',
|
||||||
);
|
);
|
||||||
const codexConfigured = serviceDefs.some(
|
const codexConfigured = serviceDefs.some(
|
||||||
(service) => service.name === 'ejclaw-codex',
|
(service) => service.kind === 'codex',
|
||||||
);
|
);
|
||||||
const reviewConfigured = serviceDefs.some(
|
const reviewConfigured = serviceDefs.some(
|
||||||
(service) => service.name === 'ejclaw-review',
|
(service) => service.kind === 'review',
|
||||||
);
|
);
|
||||||
|
|
||||||
const status =
|
const status =
|
||||||
|
|||||||
Reference in New Issue
Block a user