|
1 | 1 | /** |
2 | 2 | * `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 |
5 | 5 | * Codex on a service-managed restart (the restarted instance re-injects); explicit stop/uninstall |
6 | 6 | * restore it via the command. |
7 | 7 | */ |
@@ -91,35 +91,113 @@ function stopWindows(): void { try { sh(`schtasks /end /tn ${TASK}`); } catch { |
91 | 91 | function statusWindows(): string { try { return sh(`schtasks /query /tn ${TASK}`); } catch { return ""; } } |
92 | 92 | function uninstallWindows(): void { try { sh(`schtasks /delete /tn ${TASK} /f`); } catch { /* absent */ } } |
93 | 93 |
|
| 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 | + |
94 | 172 | 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)."); |
99 | 176 | process.exit(1); |
100 | 177 | } |
101 | 178 | switch (sub) { |
102 | 179 | case "install": |
103 | | - mac ? installLaunchd() : installWindows(); |
| 180 | + ops.install(); |
104 | 181 | 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"); |
105 | 183 | break; |
106 | 184 | case "start": |
107 | | - mac ? startLaunchd() : startWindows(); |
| 185 | + ops.start(); |
108 | 186 | console.log("✅ service started."); |
109 | 187 | break; |
110 | 188 | case "stop": |
111 | | - mac ? stopLaunchd() : stopWindows(); |
| 189 | + ops.stop(); |
112 | 190 | restoreNativeCodex(); |
113 | 191 | console.log("✅ service stopped + native Codex restored."); |
114 | 192 | break; |
115 | 193 | case "status": { |
116 | | - const s = mac ? statusLaunchd() : statusWindows(); |
| 194 | + const s = ops.status(); |
117 | 195 | console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running."); |
118 | 196 | break; |
119 | 197 | } |
120 | 198 | case "uninstall": |
121 | 199 | case "remove": |
122 | | - mac ? uninstallLaunchd() : uninstallWindows(); |
| 200 | + ops.uninstall(); |
123 | 201 | restoreNativeCodex(); |
124 | 202 | console.log("✅ service uninstalled + native Codex restored."); |
125 | 203 | break; |
|
0 commit comments