agent-anywhere-gateway 0.1.0 → 0.1.1
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 +27 -2
- package/package.json +1 -1
- package/src/gateway/main.js +8 -0
- package/src/gateway/service.js +568 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install -g agent-anywhere-gateway
|
|
|
14
14
|
agent-anywhere-gateway \
|
|
15
15
|
--server https://agent-anywhere.example.com \
|
|
16
16
|
--id office-mac \
|
|
17
|
-
--token
|
|
17
|
+
--token your-real-token \
|
|
18
18
|
--allowed-roots /Users/me/gitrepo \
|
|
19
19
|
--provider codex
|
|
20
20
|
```
|
|
@@ -30,6 +30,31 @@ agent-anywhere-gateway \
|
|
|
30
30
|
--providers codex,claude-code
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Service install
|
|
34
|
+
|
|
35
|
+
Install the gateway as a current-user service:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
agent-anywhere-gateway service install \
|
|
39
|
+
--server https://agent-anywhere.example.com \
|
|
40
|
+
--id office-mac \
|
|
41
|
+
--token replace-me \
|
|
42
|
+
--allowed-roots /Users/me/gitrepo \
|
|
43
|
+
--provider codex
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Manage it with:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
agent-anywhere-gateway service status
|
|
50
|
+
agent-anywhere-gateway service logs --follow
|
|
51
|
+
agent-anywhere-gateway service restart
|
|
52
|
+
agent-anywhere-gateway service uninstall
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The service command uses native user-level managers: macOS LaunchAgent,
|
|
56
|
+
Linux user systemd, and Windows Task Scheduler.
|
|
57
|
+
|
|
33
58
|
`--server` accepts the Control Server or Worker URL and expands it to
|
|
34
59
|
`/api/gateways/{gateway_id}/ws`. Use `--server-ws-url` when you need to pass
|
|
35
60
|
the exact WebSocket URL.
|
|
@@ -43,4 +68,4 @@ The same settings can be provided with environment variables:
|
|
|
43
68
|
- `AGENT_PROVIDER`
|
|
44
69
|
- `AGENT_PROVIDERS`
|
|
45
70
|
|
|
46
|
-
Version: 0.1.
|
|
71
|
+
Version: 0.1.1
|
package/package.json
CHANGED
package/src/gateway/main.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
serverWsUrl,
|
|
6
6
|
startGateway
|
|
7
7
|
} = require("./client");
|
|
8
|
+
const { runServiceCli } = require("./service");
|
|
8
9
|
|
|
9
10
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
10
11
|
const options = {};
|
|
@@ -159,6 +160,9 @@ function helpText() {
|
|
|
159
160
|
" --dry-run Print resolved config and exit",
|
|
160
161
|
" --help Show this help",
|
|
161
162
|
"",
|
|
163
|
+
"Commands:",
|
|
164
|
+
" service <command> Install or manage the gateway as an OS service",
|
|
165
|
+
"",
|
|
162
166
|
"Environment:",
|
|
163
167
|
" AGENT_ANYWHERE_SERVER_WS_URL Control Server WebSocket URL",
|
|
164
168
|
" AGENT_ANYWHERE_GATEWAY_ID Stable gateway machine id",
|
|
@@ -179,6 +183,9 @@ function printPlan(plan, log = console.log) {
|
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
function runCli(argv = process.argv.slice(2), env = process.env) {
|
|
186
|
+
if (argv[0] === "service") {
|
|
187
|
+
return runServiceCli(argv.slice(1), { env });
|
|
188
|
+
}
|
|
182
189
|
const options = parseArgs(argv);
|
|
183
190
|
if (options.help) {
|
|
184
191
|
console.log(helpText());
|
|
@@ -218,6 +225,7 @@ module.exports = {
|
|
|
218
225
|
registerUrl,
|
|
219
226
|
resolveServerWsUrl,
|
|
220
227
|
runCli,
|
|
228
|
+
runServiceCli,
|
|
221
229
|
serverWsUrl,
|
|
222
230
|
startGateway,
|
|
223
231
|
toGatewayWsUrl
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const { parseProviderList } = require("../shared/providers");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LABEL = "com.agent-anywhere.gateway";
|
|
8
|
+
const DEFAULT_LINUX_SERVICE_NAME = "agent-anywhere-gateway";
|
|
9
|
+
const DEFAULT_WINDOWS_TASK_NAME = "AgentAnywhereGateway";
|
|
10
|
+
const DEFAULT_GATEWAY_ID = "gateway";
|
|
11
|
+
|
|
12
|
+
function readValue(argv, index, name) {
|
|
13
|
+
const value = argv[index + 1];
|
|
14
|
+
if (!value || value.startsWith("--")) {
|
|
15
|
+
throw new Error(`${name} requires a value`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseServiceArgs(argv = []) {
|
|
21
|
+
const first = argv[0];
|
|
22
|
+
const command = first && !first.startsWith("-") ? first : "help";
|
|
23
|
+
const startIndex = command === "help" && first && first.startsWith("-") ? 0 : 1;
|
|
24
|
+
const options = { command };
|
|
25
|
+
for (let index = startIndex; index < argv.length; index += 1) {
|
|
26
|
+
const arg = argv[index];
|
|
27
|
+
const read = (name) => {
|
|
28
|
+
const value = readValue(argv, index, name);
|
|
29
|
+
index += 1;
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (arg === "--help" || arg === "-h") {
|
|
34
|
+
options.help = true;
|
|
35
|
+
} else if (arg === "--server" || arg === "--worker-url") {
|
|
36
|
+
options.server = read(arg);
|
|
37
|
+
} else if (arg === "--server-ws-url") {
|
|
38
|
+
options.serverWsUrl = read(arg);
|
|
39
|
+
} else if (arg === "--id") {
|
|
40
|
+
options.id = read(arg);
|
|
41
|
+
} else if (arg === "--name") {
|
|
42
|
+
options.name = read(arg);
|
|
43
|
+
} else if (arg === "--token") {
|
|
44
|
+
options.token = read(arg);
|
|
45
|
+
} else if (arg === "--allowed-roots") {
|
|
46
|
+
options.allowedRoots = read(arg);
|
|
47
|
+
} else if (arg === "--provider") {
|
|
48
|
+
options.provider = read(arg);
|
|
49
|
+
} else if (arg === "--providers") {
|
|
50
|
+
options.providers = read(arg);
|
|
51
|
+
} else if (arg === "--data-dir") {
|
|
52
|
+
options.dataDir = read(arg);
|
|
53
|
+
} else if (arg === "--replay-dir") {
|
|
54
|
+
options.replayDir = read(arg);
|
|
55
|
+
} else if (arg === "--reconnect-min-ms") {
|
|
56
|
+
options.reconnectMinMs = read(arg);
|
|
57
|
+
} else if (arg === "--reconnect-max-ms") {
|
|
58
|
+
options.reconnectMaxMs = read(arg);
|
|
59
|
+
} else if (arg === "--claude-auto-trust") {
|
|
60
|
+
options.claudeAutoTrust = true;
|
|
61
|
+
} else if (arg === "--label" || arg === "--service-name") {
|
|
62
|
+
options.label = read(arg);
|
|
63
|
+
} else if (arg === "--task-name") {
|
|
64
|
+
options.taskName = read(arg);
|
|
65
|
+
} else if (arg === "--cli-path") {
|
|
66
|
+
options.cliPath = read(arg);
|
|
67
|
+
} else if (arg === "--node-path") {
|
|
68
|
+
options.nodePath = read(arg);
|
|
69
|
+
} else if (arg === "--dry-run") {
|
|
70
|
+
options.dryRun = true;
|
|
71
|
+
} else if (arg === "--no-start") {
|
|
72
|
+
options.noStart = true;
|
|
73
|
+
} else if (arg === "--follow") {
|
|
74
|
+
options.follow = true;
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(`Unknown service option: ${arg}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeGatewayId(value) {
|
|
83
|
+
return String(value || os.hostname() || DEFAULT_GATEWAY_ID)
|
|
84
|
+
.trim()
|
|
85
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-")
|
|
86
|
+
.replace(/^-+|-+$/g, "") || DEFAULT_GATEWAY_ID;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeProviderList(value) {
|
|
90
|
+
return parseProviderList(value, ["codex"]).join(",");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function primaryProvider(value) {
|
|
94
|
+
return parseProviderList(value, ["codex"])[0] || "codex";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toGatewayWsUrl(serverUrl, id) {
|
|
98
|
+
const url = new URL(serverUrl);
|
|
99
|
+
if (url.protocol === "http:") {
|
|
100
|
+
url.protocol = "ws:";
|
|
101
|
+
} else if (url.protocol === "https:") {
|
|
102
|
+
url.protocol = "wss:";
|
|
103
|
+
}
|
|
104
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
105
|
+
throw new Error("--server must be an http(s) or ws(s) URL");
|
|
106
|
+
}
|
|
107
|
+
if (!url.pathname || url.pathname === "/") {
|
|
108
|
+
url.pathname = `/api/gateways/${encodeURIComponent(id)}/ws`;
|
|
109
|
+
}
|
|
110
|
+
return url.toString();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function windowsLocalAppData(env = process.env, homeDir = os.homedir()) {
|
|
114
|
+
return env.LOCALAPPDATA || path.join(homeDir, "AppData", "Local");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function defaultBaseDir(platform = process.platform, homeDir = os.homedir(), env = process.env) {
|
|
118
|
+
if (platform === "darwin") {
|
|
119
|
+
return path.join(homeDir, "Library", "Application Support", "agent-anywhere-gateway");
|
|
120
|
+
}
|
|
121
|
+
if (platform === "win32") {
|
|
122
|
+
return path.join(windowsLocalAppData(env, homeDir), "AgentAnywhereGateway");
|
|
123
|
+
}
|
|
124
|
+
return path.join(homeDir, ".local", "share", "agent-anywhere-gateway");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function defaultConfigDir(platform = process.platform, homeDir = os.homedir(), env = process.env) {
|
|
128
|
+
if (platform === "darwin") {
|
|
129
|
+
return path.join(homeDir, "Library", "Application Support", "agent-anywhere-gateway");
|
|
130
|
+
}
|
|
131
|
+
if (platform === "win32") {
|
|
132
|
+
return path.join(windowsLocalAppData(env, homeDir), "AgentAnywhereGateway");
|
|
133
|
+
}
|
|
134
|
+
return path.join(homeDir, ".config", "agent-anywhere-gateway");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defaultLogDir(platform = process.platform, homeDir = os.homedir(), env = process.env) {
|
|
138
|
+
if (platform === "darwin") {
|
|
139
|
+
return path.join(homeDir, "Library", "Logs");
|
|
140
|
+
}
|
|
141
|
+
if (platform === "win32") {
|
|
142
|
+
return path.join(windowsLocalAppData(env, homeDir), "AgentAnywhereGateway", "Logs");
|
|
143
|
+
}
|
|
144
|
+
return path.join(homeDir, ".local", "state", "agent-anywhere-gateway");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function fallbackCliPath() {
|
|
148
|
+
return path.resolve(__dirname, "..", "..", "bin", "agent-anywhere-gateway.js");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveCliPath(value = process.argv[1]) {
|
|
152
|
+
return path.resolve(value || fallbackCliPath());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveServiceSettings(options = {}, {
|
|
156
|
+
env = process.env,
|
|
157
|
+
platform = process.platform,
|
|
158
|
+
homeDir = os.homedir(),
|
|
159
|
+
nodePath = process.execPath,
|
|
160
|
+
cliPath = resolveCliPath()
|
|
161
|
+
} = {}) {
|
|
162
|
+
const id = normalizeGatewayId(options.id || env.AGENT_ANYWHERE_GATEWAY_ID);
|
|
163
|
+
const rawServerWsUrl = options.serverWsUrl || env.AGENT_ANYWHERE_SERVER_WS_URL ||
|
|
164
|
+
(options.server ? toGatewayWsUrl(options.server, id) : `ws://localhost:8787/api/gateways/${encodeURIComponent(id)}/ws`);
|
|
165
|
+
const serverWsUrl = String(rawServerWsUrl).replace("{gateway_id}", encodeURIComponent(id));
|
|
166
|
+
const providers = normalizeProviderList(options.providers || options.provider || env.AGENT_PROVIDERS || env.AGENT_PROVIDER || "codex");
|
|
167
|
+
const token = options.token || env.AGENT_ANYWHERE_GATEWAY_TOKEN || "";
|
|
168
|
+
const baseDir = defaultBaseDir(platform, homeDir, env);
|
|
169
|
+
const logDir = defaultLogDir(platform, homeDir, env);
|
|
170
|
+
const configDir = defaultConfigDir(platform, homeDir, env);
|
|
171
|
+
return {
|
|
172
|
+
platform,
|
|
173
|
+
homeDir,
|
|
174
|
+
id,
|
|
175
|
+
name: options.name || env.AGENT_ANYWHERE_MACHINE_NAME || id,
|
|
176
|
+
token,
|
|
177
|
+
serverWsUrl,
|
|
178
|
+
allowedRoots: options.allowedRoots || env.AGENT_ANYWHERE_ALLOWED_ROOTS || "",
|
|
179
|
+
providers,
|
|
180
|
+
primaryProvider: primaryProvider(providers),
|
|
181
|
+
claudeAutoTrust: options.claudeAutoTrust ? "1" : env.AGENT_ANYWHERE_CLAUDE_AUTO_TRUST || "",
|
|
182
|
+
dataDir: options.dataDir || env.AGENT_ANYWHERE_DATA_DIR || path.join(baseDir, "data"),
|
|
183
|
+
replayDir: options.replayDir || env.AGENT_ANYWHERE_GATEWAY_REPLAY_DIR || "",
|
|
184
|
+
reconnectMinMs: options.reconnectMinMs || env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MIN_MS || "",
|
|
185
|
+
reconnectMaxMs: options.reconnectMaxMs || env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MAX_MS || "",
|
|
186
|
+
label: options.label || DEFAULT_LABEL,
|
|
187
|
+
linuxServiceName: options.label || DEFAULT_LINUX_SERVICE_NAME,
|
|
188
|
+
windowsTaskName: options.taskName || options.label || DEFAULT_WINDOWS_TASK_NAME,
|
|
189
|
+
nodePath: path.resolve(options.nodePath || nodePath),
|
|
190
|
+
cliPath: resolveCliPath(options.cliPath || cliPath),
|
|
191
|
+
configDir,
|
|
192
|
+
logDir,
|
|
193
|
+
stdoutPath: path.join(logDir, "agent-anywhere-gateway.log"),
|
|
194
|
+
stderrPath: path.join(logDir, "agent-anywhere-gateway.err.log"),
|
|
195
|
+
noStart: Boolean(options.noStart),
|
|
196
|
+
dryRun: Boolean(options.dryRun)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertInstallSettings(settings) {
|
|
201
|
+
if (!settings.token || /^please-change-me$/i.test(settings.token) || /^replace-/i.test(settings.token)) {
|
|
202
|
+
throw new Error("service install requires a real --token or AGENT_ANYWHERE_GATEWAY_TOKEN");
|
|
203
|
+
}
|
|
204
|
+
if (!settings.allowedRoots) {
|
|
205
|
+
throw new Error("service install requires --allowed-roots or AGENT_ANYWHERE_ALLOWED_ROOTS");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function gatewayEnv(settings) {
|
|
210
|
+
const env = {
|
|
211
|
+
AGENT_ANYWHERE_GATEWAY_ID: settings.id,
|
|
212
|
+
AGENT_ANYWHERE_MACHINE_NAME: settings.name,
|
|
213
|
+
AGENT_ANYWHERE_GATEWAY_TOKEN: settings.token,
|
|
214
|
+
AGENT_ANYWHERE_SERVER_WS_URL: settings.serverWsUrl,
|
|
215
|
+
AGENT_ANYWHERE_ALLOWED_ROOTS: settings.allowedRoots,
|
|
216
|
+
AGENT_ANYWHERE_DATA_DIR: settings.dataDir,
|
|
217
|
+
AGENT_PROVIDERS: settings.providers,
|
|
218
|
+
AGENT_PROVIDER: settings.primaryProvider
|
|
219
|
+
};
|
|
220
|
+
if (settings.claudeAutoTrust) env.AGENT_ANYWHERE_CLAUDE_AUTO_TRUST = settings.claudeAutoTrust;
|
|
221
|
+
if (settings.replayDir) env.AGENT_ANYWHERE_GATEWAY_REPLAY_DIR = settings.replayDir;
|
|
222
|
+
if (settings.reconnectMinMs) env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MIN_MS = settings.reconnectMinMs;
|
|
223
|
+
if (settings.reconnectMaxMs) env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MAX_MS = settings.reconnectMaxMs;
|
|
224
|
+
return env;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function xmlEscape(value) {
|
|
228
|
+
return String(value)
|
|
229
|
+
.replace(/&/g, "&")
|
|
230
|
+
.replace(/</g, "<")
|
|
231
|
+
.replace(/>/g, ">")
|
|
232
|
+
.replace(/"/g, """)
|
|
233
|
+
.replace(/'/g, "'");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function plistForSettings(settings) {
|
|
237
|
+
const env = gatewayEnv(settings);
|
|
238
|
+
const envXml = Object.keys(env).sort().map((key) => (
|
|
239
|
+
` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(env[key])}</string>`
|
|
240
|
+
)).join("\n");
|
|
241
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
242
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
243
|
+
<plist version="1.0">
|
|
244
|
+
<dict>
|
|
245
|
+
<key>Label</key>
|
|
246
|
+
<string>${xmlEscape(settings.label)}</string>
|
|
247
|
+
<key>ProgramArguments</key>
|
|
248
|
+
<array>
|
|
249
|
+
<string>${xmlEscape(settings.nodePath)}</string>
|
|
250
|
+
<string>${xmlEscape(settings.cliPath)}</string>
|
|
251
|
+
</array>
|
|
252
|
+
<key>WorkingDirectory</key>
|
|
253
|
+
<string>${xmlEscape(settings.homeDir || os.homedir())}</string>
|
|
254
|
+
<key>EnvironmentVariables</key>
|
|
255
|
+
<dict>
|
|
256
|
+
${envXml}
|
|
257
|
+
</dict>
|
|
258
|
+
<key>RunAtLoad</key>
|
|
259
|
+
<true/>
|
|
260
|
+
<key>KeepAlive</key>
|
|
261
|
+
<true/>
|
|
262
|
+
<key>ThrottleInterval</key>
|
|
263
|
+
<integer>10</integer>
|
|
264
|
+
<key>StandardOutPath</key>
|
|
265
|
+
<string>${xmlEscape(settings.stdoutPath)}</string>
|
|
266
|
+
<key>StandardErrorPath</key>
|
|
267
|
+
<string>${xmlEscape(settings.stderrPath)}</string>
|
|
268
|
+
</dict>
|
|
269
|
+
</plist>
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function shellSingleQuote(value) {
|
|
274
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function systemdQuote(value) {
|
|
278
|
+
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function envFileContent(settings) {
|
|
282
|
+
return Object.keys(gatewayEnv(settings)).sort()
|
|
283
|
+
.map((key) => `${key}=${shellSingleQuote(gatewayEnv(settings)[key])}`)
|
|
284
|
+
.join("\n") + "\n";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function systemdUnitForSettings(settings) {
|
|
288
|
+
const envFile = path.join(settings.configDir, "gateway.env");
|
|
289
|
+
return `[Unit]
|
|
290
|
+
Description=Agent Anywhere Gateway
|
|
291
|
+
After=network-online.target
|
|
292
|
+
Wants=network-online.target
|
|
293
|
+
|
|
294
|
+
[Service]
|
|
295
|
+
Type=simple
|
|
296
|
+
EnvironmentFile=${systemdQuote(envFile)}
|
|
297
|
+
ExecStart=${systemdQuote(settings.nodePath)} ${systemdQuote(settings.cliPath)}
|
|
298
|
+
Restart=always
|
|
299
|
+
RestartSec=5
|
|
300
|
+
WorkingDirectory=${systemdQuote(settings.homeDir || os.homedir())}
|
|
301
|
+
|
|
302
|
+
[Install]
|
|
303
|
+
WantedBy=default.target
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function windowsCmdEscape(value) {
|
|
308
|
+
return String(value).replace(/%/g, "%%");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function windowsRunnerForSettings(settings) {
|
|
312
|
+
const lines = ["@echo off"];
|
|
313
|
+
for (const key of Object.keys(gatewayEnv(settings)).sort()) {
|
|
314
|
+
lines.push(`set "${key}=${windowsCmdEscape(gatewayEnv(settings)[key])}"`);
|
|
315
|
+
}
|
|
316
|
+
lines.push(`if not exist "${windowsCmdEscape(settings.logDir)}" mkdir "${windowsCmdEscape(settings.logDir)}"`);
|
|
317
|
+
lines.push(`"${windowsCmdEscape(settings.nodePath)}" "${windowsCmdEscape(settings.cliPath)}" >> "${windowsCmdEscape(settings.stdoutPath)}" 2>> "${windowsCmdEscape(settings.stderrPath)}"`);
|
|
318
|
+
return `${lines.join("\r\n")}\r\n`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function chmodPrivate(filePath) {
|
|
322
|
+
if (process.platform !== "win32") {
|
|
323
|
+
fs.chmodSync(filePath, 0o600);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function runCommand(command, args, { dryRun = false, executor = spawnSync, allowFailure = false } = {}) {
|
|
328
|
+
if (dryRun) {
|
|
329
|
+
console.log([command, ...args].join(" "));
|
|
330
|
+
return { status: 0 };
|
|
331
|
+
}
|
|
332
|
+
const result = executor(command, args, { stdio: "inherit" });
|
|
333
|
+
if (result.error && !allowFailure) {
|
|
334
|
+
throw result.error;
|
|
335
|
+
}
|
|
336
|
+
if (result.status !== 0 && !allowFailure) {
|
|
337
|
+
throw new Error(`${command} ${args.join(" ")} failed with status ${result.status}`);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function writeFile(filePath, content, { dryRun = false } = {}) {
|
|
343
|
+
if (dryRun) {
|
|
344
|
+
console.log(`Would write ${filePath}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
348
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function macPaths(settings, homeDir = os.homedir()) {
|
|
352
|
+
return {
|
|
353
|
+
plistPath: path.join(homeDir, "Library", "LaunchAgents", `${settings.label}.plist`)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function linuxPaths(settings, homeDir = os.homedir()) {
|
|
358
|
+
return {
|
|
359
|
+
envPath: path.join(settings.configDir, "gateway.env"),
|
|
360
|
+
servicePath: path.join(homeDir, ".config", "systemd", "user", `${settings.linuxServiceName}.service`)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function windowsPaths(settings) {
|
|
365
|
+
return {
|
|
366
|
+
runnerPath: path.join(settings.configDir, "run-gateway.cmd")
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function installMacService(settings, options = {}) {
|
|
371
|
+
const { plistPath } = macPaths(settings, options.homeDir || settings.homeDir || os.homedir());
|
|
372
|
+
writeFile(plistPath, plistForSettings(settings), settings);
|
|
373
|
+
if (!settings.dryRun) {
|
|
374
|
+
fs.mkdirSync(settings.logDir, { recursive: true });
|
|
375
|
+
fs.mkdirSync(settings.dataDir, { recursive: true });
|
|
376
|
+
chmodPrivate(plistPath);
|
|
377
|
+
}
|
|
378
|
+
const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : ""}`;
|
|
379
|
+
runCommand("launchctl", ["bootout", domain, plistPath], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync, allowFailure: true });
|
|
380
|
+
runCommand("launchctl", ["bootstrap", domain, plistPath], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync });
|
|
381
|
+
runCommand("launchctl", ["enable", `${domain}/${settings.label}`], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync });
|
|
382
|
+
if (!settings.noStart) {
|
|
383
|
+
runCommand("launchctl", ["kickstart", "-k", `${domain}/${settings.label}`], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync });
|
|
384
|
+
}
|
|
385
|
+
return { path: plistPath };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function installLinuxService(settings, options = {}) {
|
|
389
|
+
const { envPath, servicePath } = linuxPaths(settings, options.homeDir || settings.homeDir || os.homedir());
|
|
390
|
+
writeFile(envPath, envFileContent(settings), settings);
|
|
391
|
+
writeFile(servicePath, systemdUnitForSettings(settings), settings);
|
|
392
|
+
if (!settings.dryRun) {
|
|
393
|
+
fs.mkdirSync(settings.dataDir, { recursive: true });
|
|
394
|
+
fs.mkdirSync(settings.logDir, { recursive: true });
|
|
395
|
+
chmodPrivate(envPath);
|
|
396
|
+
}
|
|
397
|
+
const executor = options.executor || spawnSync;
|
|
398
|
+
runCommand("systemctl", ["--user", "daemon-reload"], { dryRun: settings.dryRun, executor });
|
|
399
|
+
runCommand("systemctl", ["--user", "enable", `${settings.linuxServiceName}.service`], { dryRun: settings.dryRun, executor });
|
|
400
|
+
if (!settings.noStart) {
|
|
401
|
+
runCommand("systemctl", ["--user", "restart", `${settings.linuxServiceName}.service`], { dryRun: settings.dryRun, executor });
|
|
402
|
+
}
|
|
403
|
+
return { path: servicePath, envPath };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function installWindowsService(settings, options = {}) {
|
|
407
|
+
const { runnerPath } = windowsPaths(settings);
|
|
408
|
+
writeFile(runnerPath, windowsRunnerForSettings(settings), settings);
|
|
409
|
+
const executor = options.executor || spawnSync;
|
|
410
|
+
runCommand("schtasks", ["/Create", "/TN", settings.windowsTaskName, "/TR", `"${runnerPath}"`, "/SC", "ONLOGON", "/F"], {
|
|
411
|
+
dryRun: settings.dryRun,
|
|
412
|
+
executor
|
|
413
|
+
});
|
|
414
|
+
if (!settings.noStart) {
|
|
415
|
+
runCommand("schtasks", ["/Run", "/TN", settings.windowsTaskName], { dryRun: settings.dryRun, executor });
|
|
416
|
+
}
|
|
417
|
+
return { path: runnerPath };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function installService(settings, options = {}) {
|
|
421
|
+
assertInstallSettings(settings);
|
|
422
|
+
if (settings.platform === "darwin") return installMacService(settings, options);
|
|
423
|
+
if (settings.platform === "win32") return installWindowsService(settings, options);
|
|
424
|
+
return installLinuxService(settings, options);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function controlService(command, settings, options = {}) {
|
|
428
|
+
const executor = options.executor || spawnSync;
|
|
429
|
+
if (settings.platform === "darwin") {
|
|
430
|
+
const { plistPath } = macPaths(settings, options.homeDir || settings.homeDir || os.homedir());
|
|
431
|
+
const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : ""}`;
|
|
432
|
+
if (command === "status") return runCommand("launchctl", ["print", `${domain}/${settings.label}`], { dryRun: settings.dryRun, executor });
|
|
433
|
+
if (command === "start") {
|
|
434
|
+
runCommand("launchctl", ["bootstrap", domain, plistPath], { dryRun: settings.dryRun, executor, allowFailure: true });
|
|
435
|
+
return runCommand("launchctl", ["kickstart", "-k", `${domain}/${settings.label}`], { dryRun: settings.dryRun, executor });
|
|
436
|
+
}
|
|
437
|
+
if (command === "restart") return runCommand("launchctl", ["kickstart", "-k", `${domain}/${settings.label}`], { dryRun: settings.dryRun, executor });
|
|
438
|
+
if (command === "stop") return runCommand("launchctl", ["bootout", domain, plistPath], { dryRun: settings.dryRun, executor });
|
|
439
|
+
if (command === "uninstall") {
|
|
440
|
+
runCommand("launchctl", ["bootout", domain, plistPath], { dryRun: settings.dryRun, executor, allowFailure: true });
|
|
441
|
+
if (!settings.dryRun && fs.existsSync(plistPath)) fs.unlinkSync(plistPath);
|
|
442
|
+
return { status: 0 };
|
|
443
|
+
}
|
|
444
|
+
if (command === "logs") {
|
|
445
|
+
const args = settings.follow ?
|
|
446
|
+
["-f", settings.stdoutPath, settings.stderrPath] :
|
|
447
|
+
["-n", "100", settings.stdoutPath, settings.stderrPath];
|
|
448
|
+
return runCommand("tail", args, { dryRun: settings.dryRun, executor });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (settings.platform === "win32") {
|
|
452
|
+
if (command === "status") return runCommand("schtasks", ["/Query", "/TN", settings.windowsTaskName, "/V", "/FO", "LIST"], { dryRun: settings.dryRun, executor });
|
|
453
|
+
if (command === "start" || command === "restart") return runCommand("schtasks", ["/Run", "/TN", settings.windowsTaskName], { dryRun: settings.dryRun, executor });
|
|
454
|
+
if (command === "stop") return runCommand("schtasks", ["/End", "/TN", settings.windowsTaskName], { dryRun: settings.dryRun, executor });
|
|
455
|
+
if (command === "uninstall") return runCommand("schtasks", ["/Delete", "/TN", settings.windowsTaskName, "/F"], { dryRun: settings.dryRun, executor, allowFailure: true });
|
|
456
|
+
if (command === "logs") {
|
|
457
|
+
if (settings.follow) {
|
|
458
|
+
return runCommand("powershell.exe", [
|
|
459
|
+
"-NoProfile",
|
|
460
|
+
"-Command",
|
|
461
|
+
`Get-Content -LiteralPath '${settings.stdoutPath.replace(/'/g, "''")}', '${settings.stderrPath.replace(/'/g, "''")}' -Tail 100 -Wait`
|
|
462
|
+
], { dryRun: settings.dryRun, executor });
|
|
463
|
+
}
|
|
464
|
+
return runCommand("cmd.exe", ["/d", "/c", "type", settings.stdoutPath, settings.stderrPath], { dryRun: settings.dryRun, executor });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const serviceName = `${settings.linuxServiceName}.service`;
|
|
468
|
+
if (command === "status") return runCommand("systemctl", ["--user", "status", serviceName, "--no-pager"], { dryRun: settings.dryRun, executor });
|
|
469
|
+
if (command === "start") return runCommand("systemctl", ["--user", "start", serviceName], { dryRun: settings.dryRun, executor });
|
|
470
|
+
if (command === "restart") return runCommand("systemctl", ["--user", "restart", serviceName], { dryRun: settings.dryRun, executor });
|
|
471
|
+
if (command === "stop") return runCommand("systemctl", ["--user", "stop", serviceName], { dryRun: settings.dryRun, executor });
|
|
472
|
+
if (command === "uninstall") {
|
|
473
|
+
runCommand("systemctl", ["--user", "disable", "--now", serviceName], { dryRun: settings.dryRun, executor, allowFailure: true });
|
|
474
|
+
const { envPath, servicePath } = linuxPaths(settings, options.homeDir || settings.homeDir || os.homedir());
|
|
475
|
+
if (!settings.dryRun) {
|
|
476
|
+
if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
|
|
477
|
+
if (fs.existsSync(envPath)) fs.unlinkSync(envPath);
|
|
478
|
+
}
|
|
479
|
+
runCommand("systemctl", ["--user", "daemon-reload"], { dryRun: settings.dryRun, executor });
|
|
480
|
+
return { status: 0 };
|
|
481
|
+
}
|
|
482
|
+
if (command === "logs") {
|
|
483
|
+
const args = ["--user", "-u", serviceName, "-n", "100", "--no-pager"];
|
|
484
|
+
if (settings.follow) args.push("-f");
|
|
485
|
+
return runCommand("journalctl", args, { dryRun: settings.dryRun, executor });
|
|
486
|
+
}
|
|
487
|
+
throw new Error(`Unknown service command: ${command}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function redacted(value) {
|
|
491
|
+
if (!value) return "";
|
|
492
|
+
const text = String(value);
|
|
493
|
+
return `${text.slice(0, 4)}...${text.slice(-4)}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function printServicePlan(settings, log = console.log) {
|
|
497
|
+
log(`Service platform: ${settings.platform}`);
|
|
498
|
+
log(`Gateway id: ${settings.id}`);
|
|
499
|
+
log(`Machine name: ${settings.name}`);
|
|
500
|
+
log(`Server WS URL: ${settings.serverWsUrl}`);
|
|
501
|
+
log(`Allowed roots: ${settings.allowedRoots}`);
|
|
502
|
+
log(`Providers: ${settings.providers}`);
|
|
503
|
+
log(`Gateway token: ${settings.token ? redacted(settings.token) : "(missing)"}`);
|
|
504
|
+
log(`Data dir: ${settings.dataDir}`);
|
|
505
|
+
log(`Log dir: ${settings.logDir}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function serviceHelpText() {
|
|
509
|
+
return `Usage:
|
|
510
|
+
agent-anywhere-gateway service install --server <url> --id <id> --token <token> --allowed-roots <paths> [--provider <provider>]
|
|
511
|
+
agent-anywhere-gateway service status
|
|
512
|
+
agent-anywhere-gateway service start
|
|
513
|
+
agent-anywhere-gateway service stop
|
|
514
|
+
agent-anywhere-gateway service restart
|
|
515
|
+
agent-anywhere-gateway service logs [--follow]
|
|
516
|
+
agent-anywhere-gateway service uninstall
|
|
517
|
+
|
|
518
|
+
Install options:
|
|
519
|
+
--server <url> Control Server or Worker URL, http(s) or ws(s).
|
|
520
|
+
--server-ws-url <url> Explicit gateway WebSocket URL.
|
|
521
|
+
--id <id> Stable gateway machine id.
|
|
522
|
+
--name <name> Human-readable machine name.
|
|
523
|
+
--token <token> Shared gateway token.
|
|
524
|
+
--allowed-roots <paths> Comma-separated allowed project roots.
|
|
525
|
+
--provider <provider> Local provider. Default: codex.
|
|
526
|
+
--providers <providers> Comma-separated local providers, e.g. codex,claude-code.
|
|
527
|
+
--data-dir <path> Gateway data directory.
|
|
528
|
+
--claude-auto-trust Trust allowed project paths for Claude Remote Control.
|
|
529
|
+
--label <name> Service label/name. macOS default: ${DEFAULT_LABEL}; Linux default: ${DEFAULT_LINUX_SERVICE_NAME}.
|
|
530
|
+
--task-name <name> Windows scheduled task name. Default: ${DEFAULT_WINDOWS_TASK_NAME}.
|
|
531
|
+
--no-start Install and enable without starting immediately.
|
|
532
|
+
--dry-run Print service actions without writing or running commands.
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function runServiceCli(argv = process.argv.slice(2), options = {}) {
|
|
537
|
+
const parsed = parseServiceArgs(argv);
|
|
538
|
+
if (parsed.help || parsed.command === "help") {
|
|
539
|
+
console.log(serviceHelpText());
|
|
540
|
+
return Promise.resolve();
|
|
541
|
+
}
|
|
542
|
+
const settings = resolveServiceSettings(parsed, options);
|
|
543
|
+
if (parsed.command === "install") {
|
|
544
|
+
printServicePlan(settings);
|
|
545
|
+
installService(settings, options);
|
|
546
|
+
return Promise.resolve();
|
|
547
|
+
}
|
|
548
|
+
controlService(parsed.command, settings, options);
|
|
549
|
+
return Promise.resolve();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = {
|
|
553
|
+
DEFAULT_LABEL,
|
|
554
|
+
DEFAULT_LINUX_SERVICE_NAME,
|
|
555
|
+
DEFAULT_WINDOWS_TASK_NAME,
|
|
556
|
+
controlService,
|
|
557
|
+
envFileContent,
|
|
558
|
+
gatewayEnv,
|
|
559
|
+
installService,
|
|
560
|
+
parseServiceArgs,
|
|
561
|
+
plistForSettings,
|
|
562
|
+
resolveServiceSettings,
|
|
563
|
+
runServiceCli,
|
|
564
|
+
serviceHelpText,
|
|
565
|
+
systemdUnitForSettings,
|
|
566
|
+
toGatewayWsUrl,
|
|
567
|
+
windowsRunnerForSettings
|
|
568
|
+
};
|