Skip to content

Commit be4397b

Browse files
lidge-junclaude
andcommitted
feat(70): add Linux systemd user unit service support
Add systemd --user service management alongside existing launchd (macOS) and Task Scheduler (Windows). Includes Docker detection, linger hint, OCX_SERVICE=1 parity, and log routing to ~/.opencodex/service.log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 91f0875 commit be4397b

1 file changed

Lines changed: 89 additions & 11 deletions

File tree

src/service.ts

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* `ocx service` — run the proxy as a background service that auto-starts on login and
3-
* auto-restarts on crash. macOS → launchd LaunchAgent; Windows → Task Scheduler.
4-
* The plist/task sets OCX_SERVICE=1 so the proxy's shutdown handler does NOT restore native
3+
* auto-restarts on crash. macOS → launchd; Windows → Task Scheduler; Linux → systemd user unit.
4+
* The service sets OCX_SERVICE=1 so the proxy's shutdown handler does NOT restore native
55
* Codex on a service-managed restart (the restarted instance re-injects); explicit stop/uninstall
66
* restore it via the command.
77
*/
@@ -91,35 +91,113 @@ function stopWindows(): void { try { sh(`schtasks /end /tn ${TASK}`); } catch {
9191
function statusWindows(): string { try { return sh(`schtasks /query /tn ${TASK}`); } catch { return ""; } }
9292
function uninstallWindows(): void { try { sh(`schtasks /delete /tn ${TASK} /f`); } catch { /* absent */ } }
9393

94+
// ── Linux (systemd user unit) ──
95+
function unitDir(): string {
96+
return join(homedir(), ".config", "systemd", "user");
97+
}
98+
99+
function unitPath(): string {
100+
return join(unitDir(), `${TASK}.service`);
101+
}
102+
103+
export function buildUnit(): string {
104+
const { bun, cli } = cliEntry();
105+
const log = logPath();
106+
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
107+
return `[Unit]
108+
Description=OpenCodex Proxy Server
109+
After=network-online.target
110+
Wants=network-online.target
111+
112+
[Service]
113+
Type=simple
114+
ExecStart=${bun} ${cli} start
115+
Restart=on-failure
116+
RestartSec=5
117+
Environment=OCX_SERVICE=1
118+
Environment=PATH=${path}
119+
StandardOutput=append:${log}
120+
StandardError=append:${log}
121+
122+
[Install]
123+
WantedBy=default.target
124+
`;
125+
}
126+
127+
function isSystemd(): boolean {
128+
try { execSync("systemctl --version", { stdio: "pipe" }); return true; } catch { return false; }
129+
}
130+
131+
function installSystemd(): void {
132+
const dir = unitDir();
133+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
134+
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
135+
writeFileSync(unitPath(), buildUnit(), "utf8");
136+
sh("systemctl --user daemon-reload");
137+
sh(`systemctl --user enable --now ${TASK}`);
138+
}
139+
function startSystemd(): void { sh(`systemctl --user start ${TASK}`); }
140+
function stopSystemd(): void { try { sh(`systemctl --user stop ${TASK}`); } catch { /* not running */ } }
141+
function statusSystemd(): string { try { return sh(`systemctl --user status ${TASK}`); } catch { return ""; } }
142+
function uninstallSystemd(): void {
143+
try { sh(`systemctl --user disable --now ${TASK}`); } catch { /* absent */ }
144+
if (existsSync(unitPath())) unlinkSync(unitPath());
145+
try { sh("systemctl --user daemon-reload"); } catch { /* best-effort */ }
146+
}
147+
148+
type ServiceOps = {
149+
install: () => void; start: () => void; stop: () => void;
150+
status: () => string; uninstall: () => void;
151+
};
152+
153+
function platformOps(): ServiceOps | null {
154+
if (process.platform === "darwin")
155+
return { install: installLaunchd, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
156+
if (process.platform === "win32")
157+
return { install: installWindows, start: startWindows, stop: stopWindows, status: statusWindows, uninstall: uninstallWindows };
158+
if (process.platform === "linux") {
159+
if (existsSync("/.dockerenv")) {
160+
console.error("Docker detected. Run 'ocx start' directly instead of using the service manager.");
161+
process.exit(1);
162+
}
163+
if (!isSystemd()) {
164+
console.error("systemd not found. Run 'ocx start' under your process supervisor.");
165+
process.exit(1);
166+
}
167+
return { install: installSystemd, start: startSystemd, stop: stopSystemd, status: statusSystemd, uninstall: uninstallSystemd };
168+
}
169+
return null;
170+
}
171+
94172
export function serviceCommand(sub?: string): void {
95-
const mac = process.platform === "darwin";
96-
const win = process.platform === "win32";
97-
if (!mac && !win) {
98-
console.error("ocx service supports macOS (launchd) and Windows (Task Scheduler). On Linux, run 'ocx start' under systemd or your process supervisor.");
173+
const ops = platformOps();
174+
if (!ops) {
175+
console.error("ocx service supports macOS (launchd), Windows (Task Scheduler), and Linux (systemd).");
99176
process.exit(1);
100177
}
101178
switch (sub) {
102179
case "install":
103-
mac ? installLaunchd() : installWindows();
180+
ops.install();
104181
console.log("✅ opencodex service installed + started (auto-starts on login, auto-restarts on crash).");
182+
if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
105183
break;
106184
case "start":
107-
mac ? startLaunchd() : startWindows();
185+
ops.start();
108186
console.log("✅ service started.");
109187
break;
110188
case "stop":
111-
mac ? stopLaunchd() : stopWindows();
189+
ops.stop();
112190
restoreNativeCodex();
113191
console.log("✅ service stopped + native Codex restored.");
114192
break;
115193
case "status": {
116-
const s = mac ? statusLaunchd() : statusWindows();
194+
const s = ops.status();
117195
console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running.");
118196
break;
119197
}
120198
case "uninstall":
121199
case "remove":
122-
mac ? uninstallLaunchd() : uninstallWindows();
200+
ops.uninstall();
123201
restoreNativeCodex();
124202
console.log("✅ service uninstalled + native Codex restored.");
125203
break;

0 commit comments

Comments
 (0)