agent-relay-server 0.3.11 → 0.4.0
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.
- package/README.md +237 -22
- package/bin/agent-relay-codex.ts +79 -6
- package/codex/README.md +18 -3
- package/codex/hooks/session-start.ts +2 -2
- package/codex/live-sidecar.ts +2 -0
- package/codex/plugin/.codex-plugin/plugin.json +1 -1
- package/codex/plugin/skills/agent-relay/SKILL.md +1 -0
- package/codex/relay.ts +8 -3
- package/examples/integrations/github-issue.ts +54 -0
- package/examples/integrations/ops-alert.sh +27 -0
- package/examples/integrations/prometheus-alertmanager.ts +61 -0
- package/examples/integrations/support-ticket.sh +28 -0
- package/package.json +5 -4
- package/public/dashboard.js +701 -0
- package/public/index.html +143 -504
- package/src/cli.ts +217 -0
- package/src/config.ts +38 -0
- package/src/daemon.ts +453 -0
- package/src/db.ts +442 -16
- package/src/index.ts +96 -70
- package/src/routes.ts +334 -17
- package/src/security.ts +103 -0
- package/src/setup.ts +187 -0
- package/src/sse.ts +18 -2
- package/src/types.ts +67 -1
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { DEFAULT_HOST, DEFAULT_PORT, defaultEnvFile, defaultLogDir, type SetupEnvironment } from "./setup";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const MANAGED_MARKER = "agent-relay-managed-daemon";
|
|
11
|
+
const DEFAULT_DAEMON_NAME = "agent-relay";
|
|
12
|
+
|
|
13
|
+
export type DaemonAction =
|
|
14
|
+
| "install"
|
|
15
|
+
| "uninstall"
|
|
16
|
+
| "start"
|
|
17
|
+
| "stop"
|
|
18
|
+
| "restart"
|
|
19
|
+
| "enable"
|
|
20
|
+
| "disable"
|
|
21
|
+
| "status"
|
|
22
|
+
| "logs";
|
|
23
|
+
|
|
24
|
+
export type DaemonScope = "user" | "system";
|
|
25
|
+
export type DaemonKind = "systemd" | "launchd" | "unsupported";
|
|
26
|
+
|
|
27
|
+
export type DaemonOptions = {
|
|
28
|
+
action: DaemonAction;
|
|
29
|
+
name?: string;
|
|
30
|
+
envFile?: string;
|
|
31
|
+
port?: number;
|
|
32
|
+
host?: string;
|
|
33
|
+
scope?: DaemonScope;
|
|
34
|
+
binaryPath?: string;
|
|
35
|
+
start?: boolean;
|
|
36
|
+
enable?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type DaemonEnvironment = SetupEnvironment & {
|
|
40
|
+
uid?: number;
|
|
41
|
+
hasSystemctl?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type DaemonFilePlan = {
|
|
45
|
+
path: string;
|
|
46
|
+
content: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type DaemonPlan = {
|
|
50
|
+
action: DaemonAction;
|
|
51
|
+
kind: DaemonKind;
|
|
52
|
+
scope: DaemonScope;
|
|
53
|
+
supported: boolean;
|
|
54
|
+
name: string;
|
|
55
|
+
label: string;
|
|
56
|
+
uid?: number;
|
|
57
|
+
serviceFilePath?: string;
|
|
58
|
+
envFile: string;
|
|
59
|
+
port: number;
|
|
60
|
+
host: string;
|
|
61
|
+
binaryPath: string;
|
|
62
|
+
logDir: string;
|
|
63
|
+
file?: DaemonFilePlan;
|
|
64
|
+
commands: string[][];
|
|
65
|
+
warnings: string[];
|
|
66
|
+
manualCommand: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type DaemonExecutionResult = {
|
|
70
|
+
plan: DaemonPlan;
|
|
71
|
+
output: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export async function detectDaemonEnvironment(): Promise<DaemonEnvironment> {
|
|
75
|
+
let hasSystemctl: boolean | undefined;
|
|
76
|
+
if (process.platform === "linux") {
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync("systemctl", ["--version"]);
|
|
79
|
+
hasSystemctl = true;
|
|
80
|
+
} catch {
|
|
81
|
+
hasSystemctl = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
platform: process.platform,
|
|
86
|
+
homeDir: homedir(),
|
|
87
|
+
uid: process.getuid?.(),
|
|
88
|
+
...(hasSystemctl !== undefined ? { hasSystemctl } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createDaemonPlan(
|
|
93
|
+
options: DaemonOptions,
|
|
94
|
+
env: DaemonEnvironment = {},
|
|
95
|
+
): DaemonPlan {
|
|
96
|
+
const platform = env.platform ?? process.platform;
|
|
97
|
+
const home = env.homeDir ?? homedir();
|
|
98
|
+
const uid = env.uid ?? process.getuid?.();
|
|
99
|
+
const name = normalizeServiceName(options.name);
|
|
100
|
+
const port = normalizePort(options.port);
|
|
101
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
102
|
+
const scope = normalizeScope(platform, options.scope, uid);
|
|
103
|
+
const envFile = resolve(options.envFile ?? defaultEnvFile(env));
|
|
104
|
+
const logDir = defaultLogDir(env);
|
|
105
|
+
const rawBinaryPath = options.binaryPath ?? defaultBinaryPath();
|
|
106
|
+
const binaryPath = rawBinaryPath.startsWith("/") ? resolve(rawBinaryPath) : rawBinaryPath;
|
|
107
|
+
const warnings: string[] = [];
|
|
108
|
+
const manualCommand = `set -a; . ${shellQuote(envFile)}; set +a; exec ${daemonProgramArgs(binaryPath).map(shellQuote).join(" ")}`;
|
|
109
|
+
|
|
110
|
+
let kind: DaemonKind = "unsupported";
|
|
111
|
+
if (platform === "linux" && env.hasSystemctl !== false) {
|
|
112
|
+
kind = "systemd";
|
|
113
|
+
} else if (platform === "darwin" && scope === "user") {
|
|
114
|
+
kind = "launchd";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (platform === "darwin" && scope === "system") {
|
|
118
|
+
warnings.push("System LaunchDaemons are not managed yet; use a user LaunchAgent or install manually.");
|
|
119
|
+
kind = "unsupported";
|
|
120
|
+
}
|
|
121
|
+
if (platform === "linux" && env.hasSystemctl === false) {
|
|
122
|
+
warnings.push("systemctl was not detected; install manually with the rendered command.");
|
|
123
|
+
}
|
|
124
|
+
if (host !== DEFAULT_HOST) {
|
|
125
|
+
warnings.push("Remote daemon binds require AGENT_RELAY_TOKEN in the env file; setup generates one by default.");
|
|
126
|
+
}
|
|
127
|
+
if (!options.binaryPath && binaryPath === "agent-relay") {
|
|
128
|
+
warnings.push("Service will resolve `agent-relay` from its PATH; pass --binary with an absolute path for the most predictable daemon.");
|
|
129
|
+
}
|
|
130
|
+
if (binaryPath.includes("bunx-")) {
|
|
131
|
+
warnings.push("The detected bunx temp path is not stable across restarts; pass --binary with a durable agent-relay path.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const label = kind === "launchd" ? `dev.agent-relay.${name}` : name;
|
|
135
|
+
const path = serviceFilePath(kind, scope, name, home);
|
|
136
|
+
const basePlan = {
|
|
137
|
+
action: options.action,
|
|
138
|
+
kind,
|
|
139
|
+
scope,
|
|
140
|
+
supported: kind !== "unsupported",
|
|
141
|
+
name,
|
|
142
|
+
label,
|
|
143
|
+
...(uid !== undefined ? { uid } : {}),
|
|
144
|
+
...(path ? { serviceFilePath: path } : {}),
|
|
145
|
+
envFile,
|
|
146
|
+
port,
|
|
147
|
+
host,
|
|
148
|
+
binaryPath,
|
|
149
|
+
logDir,
|
|
150
|
+
warnings,
|
|
151
|
+
manualCommand,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let file: DaemonFilePlan | undefined;
|
|
155
|
+
if (options.action === "install" && kind === "systemd") {
|
|
156
|
+
file = {
|
|
157
|
+
path: path!,
|
|
158
|
+
content: renderSystemdUnit({ name, scope, envFile, binaryPath }),
|
|
159
|
+
};
|
|
160
|
+
} else if (options.action === "install" && kind === "launchd") {
|
|
161
|
+
file = {
|
|
162
|
+
path: path!,
|
|
163
|
+
content: renderLaunchAgent({ label, envFile, logDir, binaryPath }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const commands =
|
|
168
|
+
options.action === "install" && kind === "systemd"
|
|
169
|
+
? [
|
|
170
|
+
["systemctl", ...systemdArgs(scope, ["daemon-reload"])],
|
|
171
|
+
...(options.enable ? [["systemctl", ...systemdArgs(scope, ["enable", `${name}.service`])]] : []),
|
|
172
|
+
...(options.start ? [["systemctl", ...systemdArgs(scope, ["start", `${name}.service`])]] : []),
|
|
173
|
+
]
|
|
174
|
+
: options.action === "install" && kind === "launchd"
|
|
175
|
+
? [
|
|
176
|
+
["launchctl", "bootstrap", `gui/${uid ?? 0}`, path!],
|
|
177
|
+
...(options.enable ? [["launchctl", "enable", `gui/${uid ?? 0}/${label}`]] : []),
|
|
178
|
+
...(options.start ? [["launchctl", "kickstart", "-k", `gui/${uid ?? 0}/${label}`]] : []),
|
|
179
|
+
]
|
|
180
|
+
: commandForAction(basePlan, options.action);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...basePlan,
|
|
184
|
+
...(file ? { file } : {}),
|
|
185
|
+
commands,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function executeDaemonPlan(
|
|
190
|
+
plan: DaemonPlan,
|
|
191
|
+
options: { dryRun?: boolean; force?: boolean } = {},
|
|
192
|
+
): Promise<DaemonExecutionResult> {
|
|
193
|
+
if (!plan.supported || options.dryRun) {
|
|
194
|
+
return { plan, output: formatDaemonPlan(plan) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const output: string[] = [];
|
|
198
|
+
if (plan.action === "install" && plan.file) {
|
|
199
|
+
const existing = await readIfExists(plan.file.path);
|
|
200
|
+
if (existing && !existing.includes(MANAGED_MARKER) && !options.force) {
|
|
201
|
+
throw new Error(`Refusing to overwrite unmanaged daemon file: ${plan.file.path}`);
|
|
202
|
+
}
|
|
203
|
+
await mkdir(dirname(plan.file.path), { recursive: true });
|
|
204
|
+
await mkdir(plan.logDir, { recursive: true });
|
|
205
|
+
await writeFile(plan.file.path, plan.file.content, "utf-8");
|
|
206
|
+
output.push(`Wrote ${plan.file.path}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (plan.action === "uninstall" && plan.serviceFilePath) {
|
|
210
|
+
const existing = await readIfExists(plan.serviceFilePath);
|
|
211
|
+
if (existing && !existing.includes(MANAGED_MARKER) && !options.force) {
|
|
212
|
+
throw new Error(`Refusing to remove unmanaged daemon file: ${plan.serviceFilePath}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const command of plan.commands) {
|
|
217
|
+
const result = await runCommand(command);
|
|
218
|
+
output.push(...result);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (plan.action === "uninstall" && plan.serviceFilePath) {
|
|
222
|
+
const existing = await readIfExists(plan.serviceFilePath);
|
|
223
|
+
if (existing?.includes(MANAGED_MARKER)) {
|
|
224
|
+
await rm(plan.serviceFilePath, { force: true });
|
|
225
|
+
output.push(`Removed ${plan.serviceFilePath}`);
|
|
226
|
+
if (plan.kind === "systemd") {
|
|
227
|
+
const command = ["systemctl", ...systemdArgs(plan.scope, ["daemon-reload"])];
|
|
228
|
+
output.push(...await runCommand(command));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { plan, output: output.join("\n") || formatDaemonPlan(plan) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function formatDaemonPlan(plan: DaemonPlan): string {
|
|
237
|
+
const lines: string[] = [];
|
|
238
|
+
lines.push(`Daemon backend: ${plan.supported ? `${plan.kind} (${plan.scope})` : "unsupported"}`);
|
|
239
|
+
lines.push(`Name: ${plan.name}`);
|
|
240
|
+
lines.push(`Env file: ${plan.envFile}`);
|
|
241
|
+
lines.push(`Listen: ${plan.host}:${plan.port}`);
|
|
242
|
+
lines.push(`Logs: ${plan.logDir}`);
|
|
243
|
+
if (plan.serviceFilePath) lines.push(`Service file: ${plan.serviceFilePath}`);
|
|
244
|
+
if (plan.warnings.length > 0) {
|
|
245
|
+
lines.push("", "Warnings:");
|
|
246
|
+
for (const warning of plan.warnings) lines.push(`- ${warning}`);
|
|
247
|
+
}
|
|
248
|
+
if (plan.file) {
|
|
249
|
+
lines.push("", `Would write ${plan.file.path}:`, plan.file.content.trimEnd());
|
|
250
|
+
}
|
|
251
|
+
if (plan.commands.length > 0) {
|
|
252
|
+
lines.push("", "Commands:");
|
|
253
|
+
for (const command of plan.commands) lines.push(` ${command.join(" ")}`);
|
|
254
|
+
}
|
|
255
|
+
if (!plan.supported) {
|
|
256
|
+
lines.push("", "Manual command:", ` ${plan.manualCommand}`);
|
|
257
|
+
}
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderSystemdUnit(options: {
|
|
262
|
+
name: string;
|
|
263
|
+
scope: DaemonScope;
|
|
264
|
+
envFile: string;
|
|
265
|
+
binaryPath: string;
|
|
266
|
+
}): string {
|
|
267
|
+
const execArgs = daemonProgramArgs(options.binaryPath).map(quoteSystemdArg);
|
|
268
|
+
return [
|
|
269
|
+
`# ${MANAGED_MARKER}`,
|
|
270
|
+
"[Unit]",
|
|
271
|
+
"Description=Agent Relay server",
|
|
272
|
+
"After=network.target",
|
|
273
|
+
"",
|
|
274
|
+
"[Service]",
|
|
275
|
+
"Type=simple",
|
|
276
|
+
`EnvironmentFile=${quoteSystemdArg(options.envFile)}`,
|
|
277
|
+
`ExecStart=${execArgs.join(" ")}`,
|
|
278
|
+
"Restart=on-failure",
|
|
279
|
+
"RestartSec=2",
|
|
280
|
+
"",
|
|
281
|
+
"[Install]",
|
|
282
|
+
`WantedBy=${options.scope === "user" ? "default.target" : "multi-user.target"}`,
|
|
283
|
+
"",
|
|
284
|
+
].join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderLaunchAgent(options: {
|
|
288
|
+
label: string;
|
|
289
|
+
envFile: string;
|
|
290
|
+
logDir: string;
|
|
291
|
+
binaryPath: string;
|
|
292
|
+
}): string {
|
|
293
|
+
const shellCommand = [
|
|
294
|
+
"set -a",
|
|
295
|
+
`. ${shellQuote(options.envFile)}`,
|
|
296
|
+
"set +a",
|
|
297
|
+
`exec ${daemonProgramArgs(options.binaryPath).map(shellQuote).join(" ")}`,
|
|
298
|
+
].join("; ");
|
|
299
|
+
const args = ["/bin/sh", "-lc", shellCommand];
|
|
300
|
+
const programArguments = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
301
|
+
|
|
302
|
+
return [
|
|
303
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
304
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
305
|
+
"<plist version=\"1.0\">",
|
|
306
|
+
`<dict><!-- ${MANAGED_MARKER} -->`,
|
|
307
|
+
" <key>Label</key>",
|
|
308
|
+
` <string>${xmlEscape(options.label)}</string>`,
|
|
309
|
+
" <key>ProgramArguments</key>",
|
|
310
|
+
" <array>",
|
|
311
|
+
programArguments,
|
|
312
|
+
" </array>",
|
|
313
|
+
" <key>RunAtLoad</key>",
|
|
314
|
+
" <true/>",
|
|
315
|
+
" <key>KeepAlive</key>",
|
|
316
|
+
" <true/>",
|
|
317
|
+
" <key>StandardOutPath</key>",
|
|
318
|
+
` <string>${xmlEscape(join(options.logDir, "agent-relay.log"))}</string>`,
|
|
319
|
+
" <key>StandardErrorPath</key>",
|
|
320
|
+
` <string>${xmlEscape(join(options.logDir, "agent-relay.err.log"))}</string>`,
|
|
321
|
+
"</dict>",
|
|
322
|
+
"</plist>",
|
|
323
|
+
"",
|
|
324
|
+
].join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function commandForAction(plan: Omit<DaemonPlan, "commands">, action: DaemonAction): string[][] {
|
|
328
|
+
if (plan.kind === "systemd") {
|
|
329
|
+
const unit = `${plan.name}.service`;
|
|
330
|
+
switch (action) {
|
|
331
|
+
case "start": return [["systemctl", ...systemdArgs(plan.scope, ["start", unit])]];
|
|
332
|
+
case "stop": return [["systemctl", ...systemdArgs(plan.scope, ["stop", unit])]];
|
|
333
|
+
case "restart": return [["systemctl", ...systemdArgs(plan.scope, ["restart", unit])]];
|
|
334
|
+
case "enable": return [["systemctl", ...systemdArgs(plan.scope, ["enable", unit])]];
|
|
335
|
+
case "disable": return [["systemctl", ...systemdArgs(plan.scope, ["disable", unit])]];
|
|
336
|
+
case "status": return [["systemctl", ...systemdArgs(plan.scope, ["status", unit, "--no-pager", "-l"])]];
|
|
337
|
+
case "logs": return [["journalctl", ...systemdArgs(plan.scope, ["-u", unit, "-n", "100", "--no-pager"])]];
|
|
338
|
+
case "uninstall": return [["systemctl", ...systemdArgs(plan.scope, ["disable", "--now", unit])]];
|
|
339
|
+
case "install": return [];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (plan.kind === "launchd") {
|
|
344
|
+
const domain = `gui/${plan.uid ?? 0}`;
|
|
345
|
+
const target = `${domain}/${plan.label}`;
|
|
346
|
+
switch (action) {
|
|
347
|
+
case "start": return [["launchctl", "kickstart", "-k", target]];
|
|
348
|
+
case "stop": return [["launchctl", "kill", "TERM", target]];
|
|
349
|
+
case "restart": return [["launchctl", "kill", "TERM", target], ["launchctl", "kickstart", "-k", target]];
|
|
350
|
+
case "enable": return [["launchctl", "enable", target]];
|
|
351
|
+
case "disable": return [["launchctl", "disable", target]];
|
|
352
|
+
case "status": return [["launchctl", "print", target]];
|
|
353
|
+
case "logs": return [["tail", "-n", "100", join(plan.logDir, "agent-relay.err.log")]];
|
|
354
|
+
case "uninstall": return [["launchctl", "bootout", domain, plan.serviceFilePath ?? ""]];
|
|
355
|
+
case "install": return [];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function daemonProgramArgs(binaryPath: string): string[] {
|
|
363
|
+
return binaryPath.endsWith(".ts") || binaryPath.endsWith(".js")
|
|
364
|
+
? [process.execPath, binaryPath]
|
|
365
|
+
: [binaryPath];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function defaultBinaryPath(): string {
|
|
369
|
+
const current = process.argv[1];
|
|
370
|
+
if (!current || current.includes("bunx-")) return "agent-relay";
|
|
371
|
+
return current;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeServiceName(name: string | undefined): string {
|
|
375
|
+
const normalized = (name ?? DEFAULT_DAEMON_NAME).trim();
|
|
376
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
377
|
+
throw new Error("--name must contain only letters, numbers, dots, underscores, and hyphens");
|
|
378
|
+
}
|
|
379
|
+
return normalized;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizePort(port: number | undefined): number {
|
|
383
|
+
const value = port ?? DEFAULT_PORT;
|
|
384
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
385
|
+
throw new Error("--port must be an integer from 1 to 65535");
|
|
386
|
+
}
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeScope(platform: NodeJS.Platform, requested: DaemonScope | undefined, uid: number | undefined): DaemonScope {
|
|
391
|
+
if (requested) return requested;
|
|
392
|
+
void platform;
|
|
393
|
+
void uid;
|
|
394
|
+
// System services are higher blast radius and must be selected explicitly.
|
|
395
|
+
return "user";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function serviceFilePath(kind: DaemonKind, scope: DaemonScope, name: string, home: string): string | undefined {
|
|
399
|
+
if (kind === "systemd") {
|
|
400
|
+
return scope === "user"
|
|
401
|
+
? join(home, ".config", "systemd", "user", `${name}.service`)
|
|
402
|
+
: join("/etc", "systemd", "system", `${name}.service`);
|
|
403
|
+
}
|
|
404
|
+
if (kind === "launchd") {
|
|
405
|
+
return join(home, "Library", "LaunchAgents", `dev.agent-relay.${name}.plist`);
|
|
406
|
+
}
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function systemdArgs(scope: DaemonScope, args: string[]): string[] {
|
|
411
|
+
return scope === "user" ? ["--user", ...args] : args;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function readIfExists(path: string): Promise<string | undefined> {
|
|
415
|
+
try {
|
|
416
|
+
await access(path, constants.F_OK);
|
|
417
|
+
return await readFile(path, "utf-8");
|
|
418
|
+
} catch {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function runCommand(command: string[]): Promise<string[]> {
|
|
424
|
+
const output = [`$ ${command.join(" ")}`];
|
|
425
|
+
try {
|
|
426
|
+
const { stdout, stderr } = await execFileAsync(command[0]!, command.slice(1));
|
|
427
|
+
if (stdout.trim()) output.push(stdout.trim());
|
|
428
|
+
if (stderr.trim()) output.push(stderr.trim());
|
|
429
|
+
return output;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const err = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string };
|
|
432
|
+
if (err.stdout?.trim()) output.push(err.stdout.trim());
|
|
433
|
+
if (err.stderr?.trim()) output.push(err.stderr.trim());
|
|
434
|
+
throw new Error(output.join("\n") || err.message);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function quoteSystemdArg(value: string): string {
|
|
439
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function shellQuote(value: string): string {
|
|
443
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function xmlEscape(value: string): string {
|
|
447
|
+
return value
|
|
448
|
+
.replace(/&/g, "&")
|
|
449
|
+
.replace(/</g, "<")
|
|
450
|
+
.replace(/>/g, ">")
|
|
451
|
+
.replace(/"/g, """)
|
|
452
|
+
.replace(/'/g, "'");
|
|
453
|
+
}
|