From 4fcec1ef474a380dc4b632b382f66af9fb066c94 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Tue, 31 Mar 2026 05:52:19 +0900 Subject: [PATCH] refactor: extract runtime PATH env builder for SSOT --- setup/service.test.ts | 16 +++++++-- setup/service.ts | 79 ++++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/setup/service.test.ts b/setup/service.test.ts index 6676acc..8aa69f7 100644 --- a/setup/service.test.ts +++ b/setup/service.test.ts @@ -3,7 +3,7 @@ import os from 'os'; import path from 'path'; import { afterEach, describe, expect, it } from 'vitest'; -import { buildStackRestartSystemdUnit } from './service.js'; +import { buildRuntimePathEnv, buildStackRestartSystemdUnit } from './service.js'; import { getServiceDefs } from './service-defs.js'; /** @@ -19,6 +19,8 @@ function generatePlist( projectRoot: string, homeDir: string, ): string { + const runtimePath = buildRuntimePathEnv(nodePath, homeDir); + return ` @@ -39,7 +41,7 @@ function generatePlist( EnvironmentVariables PATH - /usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin + ${runtimePath} HOME ${homeDir} @@ -57,6 +59,8 @@ function generateSystemdUnit( homeDir: string, isSystem: boolean, ): string { + const runtimePath = buildRuntimePathEnv(nodePath, homeDir); + return `[Unit] Description=EJClaw Personal Assistant After=network.target @@ -68,7 +72,7 @@ WorkingDirectory=${projectRoot} Restart=always RestartSec=5 Environment=HOME=${homeDir} -Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin +Environment=PATH=${runtimePath} StandardOutput=append:${projectRoot}/logs/ejclaw.log StandardError=append:${projectRoot}/logs/ejclaw.error.log @@ -116,6 +120,12 @@ describe('plist generation', () => { }); describe('systemd unit generation', () => { + it('shares the runtime PATH builder across service formats', () => { + expect(buildRuntimePathEnv('/usr/bin/bun', '/home/user')).toBe( + '/usr/bin:/usr/local/bin:/usr/bin:/bin:/home/user/.local/bin:/home/user/.npm-global/bin', + ); + }); + it('user unit uses default.target', () => { const unit = generateSystemdUnit( '/usr/bin/node', diff --git a/setup/service.ts b/setup/service.ts index 735c63c..4ae9b0b 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -89,6 +89,47 @@ export async function run(_args: string[]): Promise { } } +export function buildRuntimePathEnv(nodePath: string, homeDir: string): string { + return `${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin`; +} + +function buildLaunchdEnvironmentEntries( + nodePath: string, + homeDir: string, + extraEnv?: Record, +): string[] { + const envEntries = [ + ` PATH`, + ` ${buildRuntimePathEnv(nodePath, homeDir)}`, + ` HOME`, + ` ${homeDir}`, + ]; + if (extraEnv) { + for (const [k, v] of Object.entries(extraEnv)) { + envEntries.push(` ${k}`); + envEntries.push(` ${v}`); + } + } + return envEntries; +} + +function buildSystemdEnvironmentLines( + nodePath: string, + homeDir: string, + extraEnv?: Record, +): string[] { + const envLines = [ + `Environment=HOME=${homeDir}`, + `Environment=PATH=${buildRuntimePathEnv(nodePath, homeDir)}`, + ]; + if (extraEnv) { + for (const [k, v] of Object.entries(extraEnv)) { + envLines.push(`Environment=${k}=${v}`); + } + } + return envLines; +} + /* ------------------------------------------------------------------ */ /* macOS (launchd) */ /* ------------------------------------------------------------------ */ @@ -107,19 +148,11 @@ function setupLaunchd( ); fs.mkdirSync(path.dirname(plistPath), { recursive: true }); - // Build extra env dict entries - const envEntries = [ - ` PATH`, - ` ${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin`, - ` HOME`, - ` ${homeDir}`, - ]; - if (def.extraEnv) { - for (const [k, v] of Object.entries(def.extraEnv)) { - envEntries.push(` ${k}`); - envEntries.push(` ${v}`); - } - } + const envEntries = buildLaunchdEnvironmentEntries( + nodePath, + homeDir, + def.extraEnv, + ); const plist = ` @@ -331,16 +364,11 @@ function setupSystemdUnit( const unitPath = getUnitPath(def.name, homeDir, runningAsRoot); fs.mkdirSync(path.dirname(unitPath), { recursive: true }); - // Build Environment= lines - const envLines = [ - `Environment=HOME=${homeDir}`, - `Environment=PATH=${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin`, - ]; - if (def.extraEnv) { - for (const [k, v] of Object.entries(def.extraEnv)) { - envLines.push(`Environment=${k}=${v}`); - } - } + const envLines = buildSystemdEnvironmentLines( + nodePath, + homeDir, + def.extraEnv, + ); // EnvironmentFile line (optional — for codex service loading .env.codex) const envFileLine = def.environmentFile @@ -373,10 +401,7 @@ export function buildStackRestartSystemdUnit( nodePath: string, homeDir: string, ): string { - const envLines = [ - `Environment=HOME=${homeDir}`, - `Environment=PATH=${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin:${homeDir}/.npm-global/bin`, - ]; + const envLines = buildSystemdEnvironmentLines(nodePath, homeDir); return `[Unit] Description=EJClaw Stack Restart Orchestrator