@khanglvm/llm-router 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,369 @@
1
+ /**
2
+ * OS startup integration for llm-router.
3
+ * Supports macOS LaunchAgent and Linux systemd --user service.
4
+ */
5
+
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { promises as fs, existsSync } from "node:fs";
9
+ import { spawnSync } from "node:child_process";
10
+
11
+ const SERVICE_NAME = "llm-router";
12
+ const LAUNCH_AGENT_ID = "dev.llm-router";
13
+
14
+ function resolveDarwinDomain() {
15
+ const uid = process.getuid?.();
16
+ return uid !== undefined ? `gui/${uid}` : "gui/$(id -u)";
17
+ }
18
+
19
+ function quoteArg(value) {
20
+ const escaped = String(value).replace(/"/g, '\\"');
21
+ return `"${escaped}"`;
22
+ }
23
+
24
+ function runCommand(command, args, { cwd } = {}) {
25
+ const result = spawnSync(command, args, {
26
+ cwd,
27
+ encoding: "utf8",
28
+ env: {
29
+ ...process.env,
30
+ FORCE_COLOR: "0"
31
+ }
32
+ });
33
+
34
+ return {
35
+ ok: result.status === 0,
36
+ status: result.status ?? 1,
37
+ stdout: result.stdout || "",
38
+ stderr: result.stderr || "",
39
+ error: result.error
40
+ };
41
+ }
42
+
43
+ function resolveCliEntryPath() {
44
+ if (process.env.LLM_ROUTER_CLI_PATH) return process.env.LLM_ROUTER_CLI_PATH;
45
+ const nodeBinDir = path.dirname(process.execPath);
46
+ for (const binName of ["llm-router", "llm-router-route"]) {
47
+ const candidate = path.join(nodeBinDir, binName);
48
+ if (existsSync(candidate)) return candidate;
49
+ }
50
+ if (process.argv[1]) return path.resolve(process.argv[1]);
51
+ throw new Error("Unable to resolve llm-router CLI entry path.");
52
+ }
53
+
54
+ function makeExecArgs({ configPath, host, port, watchConfig, watchBinary, requireAuth }) {
55
+ return [
56
+ "start",
57
+ `--config=${configPath}`,
58
+ `--host=${host}`,
59
+ `--port=${port}`,
60
+ `--watch-config=${watchConfig ? "true" : "false"}`,
61
+ `--watch-binary=${watchBinary ? "true" : "false"}`,
62
+ `--require-auth=${requireAuth ? "true" : "false"}`
63
+ ];
64
+ }
65
+
66
+ function buildLaunchAgentPlist({ nodePath, cliPath, configPath, host, port, watchConfig, watchBinary, requireAuth }) {
67
+ const logDir = path.join(os.homedir(), "Library", "Logs");
68
+ const stdoutPath = path.join(logDir, "llm-router.out.log");
69
+ const stderrPath = path.join(logDir, "llm-router.err.log");
70
+ const args = [nodePath, cliPath, ...makeExecArgs({ configPath, host, port, watchConfig, watchBinary, requireAuth })];
71
+
72
+ const xmlArgs = args.map((arg) => ` <string>${arg}</string>`).join("\n");
73
+
74
+ return `<?xml version="1.0" encoding="UTF-8"?>
75
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
76
+ <plist version="1.0">
77
+ <dict>
78
+ <key>Label</key>
79
+ <string>${LAUNCH_AGENT_ID}</string>
80
+ <key>ProgramArguments</key>
81
+ <array>
82
+ ${xmlArgs}
83
+ </array>
84
+ <key>RunAtLoad</key>
85
+ <true/>
86
+ <key>KeepAlive</key>
87
+ <true/>
88
+ <key>EnvironmentVariables</key>
89
+ <dict>
90
+ <key>LLM_ROUTER_MANAGED_BY_STARTUP</key>
91
+ <string>1</string>
92
+ <key>LLM_ROUTER_CLI_PATH</key>
93
+ <string>${cliPath}</string>
94
+ </dict>
95
+ <key>StandardOutPath</key>
96
+ <string>${stdoutPath}</string>
97
+ <key>StandardErrorPath</key>
98
+ <string>${stderrPath}</string>
99
+ <key>WorkingDirectory</key>
100
+ <string>${process.cwd()}</string>
101
+ </dict>
102
+ </plist>
103
+ `;
104
+ }
105
+
106
+ function buildSystemdService({ nodePath, cliPath, configPath, host, port, watchConfig, watchBinary, requireAuth }) {
107
+ const execArgs = makeExecArgs({ configPath, host, port, watchConfig, watchBinary, requireAuth }).map(quoteArg).join(" ");
108
+ const execStart = `${quoteArg(nodePath)} ${quoteArg(cliPath)} ${execArgs}`;
109
+
110
+ return `[Unit]
111
+ Description=LLM Router local route
112
+ After=network.target
113
+
114
+ [Service]
115
+ Type=simple
116
+ ExecStart=${execStart}
117
+ Restart=always
118
+ RestartSec=2
119
+ Environment=NODE_ENV=production
120
+ Environment=LLM_ROUTER_MANAGED_BY_STARTUP=1
121
+ Environment=LLM_ROUTER_CLI_PATH=${cliPath}
122
+ WorkingDirectory=${process.cwd()}
123
+
124
+ [Install]
125
+ WantedBy=default.target
126
+ `;
127
+ }
128
+
129
+ async function installDarwin({ configPath, host, port, watchConfig, watchBinary, requireAuth }) {
130
+ const launchAgentsDir = path.join(os.homedir(), "Library", "LaunchAgents");
131
+ const plistPath = path.join(launchAgentsDir, `${LAUNCH_AGENT_ID}.plist`);
132
+ const nodePath = process.execPath;
133
+ const cliPath = resolveCliEntryPath();
134
+
135
+ await fs.mkdir(launchAgentsDir, { recursive: true });
136
+ await fs.mkdir(path.join(os.homedir(), "Library", "Logs"), { recursive: true });
137
+
138
+ const content = buildLaunchAgentPlist({
139
+ nodePath,
140
+ cliPath,
141
+ configPath,
142
+ host,
143
+ port,
144
+ watchConfig,
145
+ watchBinary,
146
+ requireAuth
147
+ });
148
+
149
+ await fs.writeFile(plistPath, content, "utf8");
150
+
151
+ const domain = resolveDarwinDomain();
152
+
153
+ // Best effort reload sequence.
154
+ runCommand("launchctl", ["bootout", domain, plistPath]);
155
+ const bootstrap = runCommand("launchctl", ["bootstrap", domain, plistPath]);
156
+ if (!bootstrap.ok) {
157
+ throw new Error(bootstrap.stderr || bootstrap.stdout || "launchctl bootstrap failed.");
158
+ }
159
+
160
+ runCommand("launchctl", ["enable", `${domain}/${LAUNCH_AGENT_ID}`]);
161
+
162
+ return {
163
+ manager: "launchd",
164
+ serviceId: LAUNCH_AGENT_ID,
165
+ filePath: plistPath
166
+ };
167
+ }
168
+
169
+ async function uninstallDarwin() {
170
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_ID}.plist`);
171
+ const domain = resolveDarwinDomain();
172
+
173
+ runCommand("launchctl", ["bootout", domain, plistPath]);
174
+
175
+ try {
176
+ await fs.unlink(plistPath);
177
+ } catch (error) {
178
+ if (!(error && typeof error === "object" && error.code === "ENOENT")) {
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ return {
184
+ manager: "launchd",
185
+ serviceId: LAUNCH_AGENT_ID,
186
+ filePath: plistPath
187
+ };
188
+ }
189
+
190
+ async function statusDarwin() {
191
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_ID}.plist`);
192
+ let installed = true;
193
+ try {
194
+ await fs.access(plistPath);
195
+ } catch {
196
+ installed = false;
197
+ }
198
+
199
+ const domain = resolveDarwinDomain();
200
+ const listResult = runCommand("launchctl", ["print", `${domain}/${LAUNCH_AGENT_ID}`]);
201
+
202
+ return {
203
+ manager: "launchd",
204
+ serviceId: LAUNCH_AGENT_ID,
205
+ installed,
206
+ running: listResult.ok,
207
+ filePath: plistPath,
208
+ detail: listResult.ok ? listResult.stdout : (listResult.stderr || listResult.stdout)
209
+ };
210
+ }
211
+
212
+ async function stopDarwin() {
213
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_ID}.plist`);
214
+ const domain = resolveDarwinDomain();
215
+ runCommand("launchctl", ["bootout", domain, plistPath]);
216
+ return statusDarwin();
217
+ }
218
+
219
+ async function restartDarwin() {
220
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_ID}.plist`);
221
+ await fs.access(plistPath);
222
+ const domain = resolveDarwinDomain();
223
+ runCommand("launchctl", ["bootout", domain, plistPath]);
224
+ const bootstrap = runCommand("launchctl", ["bootstrap", domain, plistPath]);
225
+ if (!bootstrap.ok) {
226
+ throw new Error(bootstrap.stderr || bootstrap.stdout || "launchctl bootstrap failed.");
227
+ }
228
+ runCommand("launchctl", ["enable", `${domain}/${LAUNCH_AGENT_ID}`]);
229
+ return statusDarwin();
230
+ }
231
+
232
+ async function installLinux({ configPath, host, port, watchConfig, watchBinary, requireAuth }) {
233
+ const systemdDir = path.join(os.homedir(), ".config", "systemd", "user");
234
+ const servicePath = path.join(systemdDir, `${SERVICE_NAME}.service`);
235
+ const nodePath = process.execPath;
236
+ const cliPath = resolveCliEntryPath();
237
+
238
+ await fs.mkdir(systemdDir, { recursive: true });
239
+ const content = buildSystemdService({
240
+ nodePath,
241
+ cliPath,
242
+ configPath,
243
+ host,
244
+ port,
245
+ watchConfig,
246
+ watchBinary,
247
+ requireAuth
248
+ });
249
+ await fs.writeFile(servicePath, content, "utf8");
250
+
251
+ const daemonReload = runCommand("systemctl", ["--user", "daemon-reload"]);
252
+ if (!daemonReload.ok) {
253
+ throw new Error(daemonReload.stderr || daemonReload.stdout || "systemctl daemon-reload failed.");
254
+ }
255
+ const enableNow = runCommand("systemctl", ["--user", "enable", "--now", `${SERVICE_NAME}.service`]);
256
+ if (!enableNow.ok) {
257
+ throw new Error(enableNow.stderr || enableNow.stdout || "systemctl enable --now failed.");
258
+ }
259
+
260
+ return {
261
+ manager: "systemd-user",
262
+ serviceId: `${SERVICE_NAME}.service`,
263
+ filePath: servicePath
264
+ };
265
+ }
266
+
267
+ async function uninstallLinux() {
268
+ const servicePath = path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
269
+ runCommand("systemctl", ["--user", "disable", "--now", `${SERVICE_NAME}.service`]);
270
+ runCommand("systemctl", ["--user", "daemon-reload"]);
271
+
272
+ try {
273
+ await fs.unlink(servicePath);
274
+ } catch (error) {
275
+ if (!(error && typeof error === "object" && error.code === "ENOENT")) {
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ return {
281
+ manager: "systemd-user",
282
+ serviceId: `${SERVICE_NAME}.service`,
283
+ filePath: servicePath
284
+ };
285
+ }
286
+
287
+ async function statusLinux() {
288
+ const servicePath = path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
289
+ let installed = true;
290
+ try {
291
+ await fs.access(servicePath);
292
+ } catch {
293
+ installed = false;
294
+ }
295
+
296
+ const isActive = runCommand("systemctl", ["--user", "is-active", `${SERVICE_NAME}.service`]);
297
+ return {
298
+ manager: "systemd-user",
299
+ serviceId: `${SERVICE_NAME}.service`,
300
+ installed,
301
+ running: isActive.ok && isActive.stdout.trim() === "active",
302
+ filePath: servicePath,
303
+ detail: isActive.stdout || isActive.stderr
304
+ };
305
+ }
306
+
307
+ async function stopLinux() {
308
+ runCommand("systemctl", ["--user", "stop", `${SERVICE_NAME}.service`]);
309
+ return statusLinux();
310
+ }
311
+
312
+ async function restartLinux() {
313
+ const servicePath = path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
314
+ await fs.access(servicePath);
315
+ const restart = runCommand("systemctl", ["--user", "restart", `${SERVICE_NAME}.service`]);
316
+ if (!restart.ok) {
317
+ const start = runCommand("systemctl", ["--user", "start", `${SERVICE_NAME}.service`]);
318
+ if (!start.ok) {
319
+ throw new Error(start.stderr || start.stdout || restart.stderr || restart.stdout || "systemctl restart failed.");
320
+ }
321
+ }
322
+ return statusLinux();
323
+ }
324
+
325
+ export async function installStartup(options) {
326
+ const payload = {
327
+ configPath: options.configPath,
328
+ host: options.host || "127.0.0.1",
329
+ port: options.port || 8787,
330
+ watchConfig: options.watchConfig !== false,
331
+ watchBinary: options.watchBinary !== false,
332
+ requireAuth: options.requireAuth === true
333
+ };
334
+
335
+ if (process.platform === "darwin") return installDarwin(payload);
336
+ if (process.platform === "linux") return installLinux(payload);
337
+
338
+ throw new Error(`OS startup is not supported on platform '${process.platform}' yet.`);
339
+ }
340
+
341
+ export async function uninstallStartup() {
342
+ if (process.platform === "darwin") return uninstallDarwin();
343
+ if (process.platform === "linux") return uninstallLinux();
344
+ throw new Error(`OS startup is not supported on platform '${process.platform}' yet.`);
345
+ }
346
+
347
+ export async function startupStatus() {
348
+ if (process.platform === "darwin") return statusDarwin();
349
+ if (process.platform === "linux") return statusLinux();
350
+ return {
351
+ manager: "unsupported",
352
+ serviceId: SERVICE_NAME,
353
+ installed: false,
354
+ running: false,
355
+ detail: `Platform '${process.platform}' is not supported yet.`
356
+ };
357
+ }
358
+
359
+ export async function stopStartup() {
360
+ if (process.platform === "darwin") return stopDarwin();
361
+ if (process.platform === "linux") return stopLinux();
362
+ throw new Error(`OS startup is not supported on platform '${process.platform}' yet.`);
363
+ }
364
+
365
+ export async function restartStartup() {
366
+ if (process.platform === "darwin") return restartDarwin();
367
+ if (process.platform === "linux") return restartLinux();
368
+ throw new Error(`OS startup is not supported on platform '${process.platform}' yet.`);
369
+ }