diff --git a/setup/service.test.ts b/setup/service.test.ts
index 8aa69f7..8827c2f 100644
--- a/setup/service.test.ts
+++ b/setup/service.test.ts
@@ -3,8 +3,13 @@ import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
-import { buildRuntimePathEnv, buildStackRestartSystemdUnit } from './service.js';
-import { getServiceDefs } from './service-defs.js';
+import {
+ buildLaunchdPlist,
+ buildRuntimePathEnv,
+ buildStackRestartSystemdUnit,
+ buildSystemdUnit,
+} from './service.js';
+import { getServiceDefs, type ServiceDef } from './service-defs.js';
/**
* Tests for service configuration generation.
@@ -13,105 +18,49 @@ import { getServiceDefs } from './service-defs.js';
* without actually loading services.
*/
-// Helper: generate a plist string the same way service.ts does
-function generatePlist(
- nodePath: string,
- projectRoot: string,
- homeDir: string,
-): string {
- const runtimePath = buildRuntimePathEnv(nodePath, homeDir);
-
- return `
-
-
-
- Label
- com.ejclaw
- ProgramArguments
-
- ${nodePath}
- ${projectRoot}/dist/index.js
-
- WorkingDirectory
- ${projectRoot}
- RunAtLoad
-
- KeepAlive
-
- EnvironmentVariables
-
- PATH
- ${runtimePath}
- HOME
- ${homeDir}
-
- StandardOutPath
- ${projectRoot}/logs/ejclaw.log
- StandardErrorPath
- ${projectRoot}/logs/ejclaw.error.log
-
-`;
-}
-
-function generateSystemdUnit(
- nodePath: string,
- projectRoot: string,
- homeDir: string,
- isSystem: boolean,
-): string {
- const runtimePath = buildRuntimePathEnv(nodePath, homeDir);
-
- return `[Unit]
-Description=EJClaw Personal Assistant
-After=network.target
-
-[Service]
-Type=simple
-ExecStart=${nodePath} ${projectRoot}/dist/index.js
-WorkingDirectory=${projectRoot}
-Restart=always
-RestartSec=5
-Environment=HOME=${homeDir}
-Environment=PATH=${runtimePath}
-StandardOutput=append:${projectRoot}/logs/ejclaw.log
-StandardError=append:${projectRoot}/logs/ejclaw.error.log
-
-[Install]
-WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`;
-}
+const baseServiceDef: ServiceDef = {
+ description: 'EJClaw Personal Assistant',
+ launchdLabel: 'com.ejclaw',
+ logName: 'ejclaw',
+ name: 'ejclaw',
+};
describe('plist generation', () => {
it('contains the correct label', () => {
- const plist = generatePlist(
- '/usr/local/bin/node',
+ const plist = buildLaunchdPlist(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/usr/local/bin/node',
'/home/user',
);
expect(plist).toContain('com.ejclaw');
});
it('uses the correct node path', () => {
- const plist = generatePlist(
- '/opt/node/bin/node',
+ const plist = buildLaunchdPlist(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/opt/node/bin/node',
'/home/user',
);
expect(plist).toContain('/opt/node/bin/node');
});
it('points to dist/index.js', () => {
- const plist = generatePlist(
- '/usr/local/bin/node',
+ const plist = buildLaunchdPlist(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/usr/local/bin/node',
'/home/user',
);
expect(plist).toContain('/home/user/ejclaw/dist/index.js');
});
it('sets log paths', () => {
- const plist = generatePlist(
- '/usr/local/bin/node',
+ const plist = buildLaunchdPlist(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/usr/local/bin/node',
'/home/user',
);
expect(plist).toContain('ejclaw.log');
@@ -127,9 +76,10 @@ describe('systemd unit generation', () => {
});
it('user unit uses default.target', () => {
- const unit = generateSystemdUnit(
- '/usr/bin/node',
+ const unit = buildSystemdUnit(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/usr/bin/node',
'/home/user',
false,
);
@@ -137,9 +87,10 @@ describe('systemd unit generation', () => {
});
it('system unit uses multi-user.target', () => {
- const unit = generateSystemdUnit(
- '/usr/bin/node',
+ const unit = buildSystemdUnit(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/usr/bin/node',
'/home/user',
true,
);
@@ -147,9 +98,10 @@ describe('systemd unit generation', () => {
});
it('contains restart policy', () => {
- const unit = generateSystemdUnit(
- '/usr/bin/node',
+ const unit = buildSystemdUnit(
+ baseServiceDef,
'/home/user/ejclaw',
+ '/usr/bin/node',
'/home/user',
false,
);
@@ -158,9 +110,10 @@ describe('systemd unit generation', () => {
});
it('sets correct ExecStart', () => {
- const unit = generateSystemdUnit(
- '/usr/bin/bun',
+ const unit = buildSystemdUnit(
+ baseServiceDef,
'/srv/ejclaw',
+ '/usr/bin/bun',
'/home/user',
false,
);
@@ -168,6 +121,25 @@ describe('systemd unit generation', () => {
'ExecStart=/usr/bin/bun /srv/ejclaw/dist/index.js',
);
});
+
+ it('preserves EnvironmentFile and extraEnv in the actual builder', () => {
+ const unit = buildSystemdUnit(
+ {
+ ...baseServiceDef,
+ environmentFile: '/srv/ejclaw/.env.codex',
+ extraEnv: { ASSISTANT_NAME: 'codex' },
+ logName: 'ejclaw-codex',
+ name: 'ejclaw-codex',
+ },
+ '/srv/ejclaw',
+ '/usr/bin/bun',
+ '/home/user',
+ false,
+ );
+
+ expect(unit).toContain('EnvironmentFile=/srv/ejclaw/.env.codex');
+ expect(unit).toContain('Environment=ASSISTANT_NAME=codex');
+ });
});
describe('WSL nohup fallback', () => {
diff --git a/setup/service.ts b/setup/service.ts
index 4ae9b0b..e3a3588 100644
--- a/setup/service.ts
+++ b/setup/service.ts
@@ -148,39 +148,7 @@ function setupLaunchd(
);
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
- const envEntries = buildLaunchdEnvironmentEntries(
- nodePath,
- homeDir,
- def.extraEnv,
- );
-
- const plist = `
-
-
-
- Label
- ${def.launchdLabel}
- ProgramArguments
-
- ${nodePath}
- ${projectRoot}/dist/index.js
-
- WorkingDirectory
- ${projectRoot}
- RunAtLoad
-
- KeepAlive
-
- EnvironmentVariables
-
-${envEntries.join('\n')}
-
- StandardOutPath
- ${projectRoot}/logs/${def.logName}.log
- StandardErrorPath
- ${projectRoot}/logs/${def.logName}.error.log
-
-`;
+ const plist = buildLaunchdPlist(def, projectRoot, nodePath, homeDir);
fs.writeFileSync(plistPath, plist);
logger.info({ plistPath, service: def.name }, 'Wrote launchd plist');
@@ -218,6 +186,47 @@ ${envEntries.join('\n')}
});
}
+export function buildLaunchdPlist(
+ def: ServiceDef,
+ projectRoot: string,
+ nodePath: string,
+ homeDir: string,
+): string {
+ const envEntries = buildLaunchdEnvironmentEntries(
+ nodePath,
+ homeDir,
+ def.extraEnv,
+ );
+
+ return `
+
+
+
+ Label
+ ${def.launchdLabel}
+ ProgramArguments
+
+ ${nodePath}
+ ${projectRoot}/dist/index.js
+
+ WorkingDirectory
+ ${projectRoot}
+ RunAtLoad
+
+ KeepAlive
+
+ EnvironmentVariables
+
+${envEntries.join('\n')}
+
+ StandardOutPath
+ ${projectRoot}/logs/${def.logName}.log
+ StandardErrorPath
+ ${projectRoot}/logs/${def.logName}.error.log
+
+`;
+}
+
/* ------------------------------------------------------------------ */
/* Linux */
/* ------------------------------------------------------------------ */
@@ -364,6 +373,25 @@ function setupSystemdUnit(
const unitPath = getUnitPath(def.name, homeDir, runningAsRoot);
fs.mkdirSync(path.dirname(unitPath), { recursive: true });
+ const unit = buildSystemdUnit(
+ def,
+ projectRoot,
+ nodePath,
+ homeDir,
+ runningAsRoot,
+ );
+
+ fs.writeFileSync(unitPath, unit);
+ logger.info({ unitPath, service: def.name }, 'Wrote systemd unit');
+}
+
+export function buildSystemdUnit(
+ def: ServiceDef,
+ projectRoot: string,
+ nodePath: string,
+ homeDir: string,
+ runningAsRoot: boolean,
+): string {
const envLines = buildSystemdEnvironmentLines(
nodePath,
homeDir,
@@ -375,7 +403,7 @@ function setupSystemdUnit(
? `EnvironmentFile=${def.environmentFile}\n`
: '';
- const unit = `[Unit]
+ return `[Unit]
Description=${def.description}
After=network.target
@@ -391,9 +419,6 @@ StandardError=append:${projectRoot}/logs/${def.logName}.error.log
[Install]
WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
-
- fs.writeFileSync(unitPath, unit);
- logger.info({ unitPath, service: def.name }, 'Wrote systemd unit');
}
export function buildStackRestartSystemdUnit(