@jiayunxie/aerial 0.2.1 → 0.2.2

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/docs/usage.md +2 -2
  3. package/package.json +5 -5
  4. package/src/cli/args.js +28 -0
  5. package/src/cli/config-command.js +39 -0
  6. package/src/cli/disable-command.js +28 -0
  7. package/src/{doctor.js → cli/doctor.js} +3 -3
  8. package/src/cli/help.js +23 -0
  9. package/src/cli/index.js +92 -0
  10. package/src/cli/key-command.js +20 -0
  11. package/src/cli/login-command.js +28 -0
  12. package/src/{model-selection.js → cli/model-selection.js} +5 -5
  13. package/src/cli/output.js +75 -0
  14. package/src/{probe.js → cli/probe.js} +3 -3
  15. package/src/cli/proxy-command.js +120 -0
  16. package/src/cli/runtime-auth.js +21 -0
  17. package/src/cli/service-command.js +120 -0
  18. package/src/cli/setup-command.js +122 -0
  19. package/src/{setup-selection.js → cli/setup-selection.js} +3 -20
  20. package/src/cli/start-command.js +11 -0
  21. package/src/cli/status-command.js +26 -0
  22. package/src/{version.js → cli/version.js} +1 -1
  23. package/src/proxy/cache-policy.js +89 -0
  24. package/src/proxy/cache-telemetry.js +172 -0
  25. package/src/proxy/effort.js +120 -0
  26. package/src/proxy/headers.js +33 -0
  27. package/src/proxy/index.js +85 -0
  28. package/src/proxy/models.js +27 -0
  29. package/src/{responses-websocket.js → proxy/responses-websocket.js} +2 -2
  30. package/src/{server.js → proxy/server.js} +5 -5
  31. package/src/proxy/transport.js +55 -0
  32. package/src/service/health.js +54 -0
  33. package/src/service/index.js +38 -0
  34. package/src/service/lifecycle.js +301 -0
  35. package/src/service/platform.js +227 -0
  36. package/src/service/runner.js +45 -0
  37. package/src/service/status.js +296 -0
  38. package/src/service/wrapper-render.js +228 -0
  39. package/src/setup/backup.js +38 -0
  40. package/src/{setup.js → setup/clients.js} +11 -198
  41. package/src/setup/index.js +4 -0
  42. package/src/setup/restore.js +90 -0
  43. package/src/setup/status.js +24 -0
  44. package/src/setup/toml.js +41 -0
  45. package/src/{auth.js → shared/auth.js} +1 -1
  46. package/src/{config.js → shared/config.js} +2 -2
  47. package/src/shared/effort.js +19 -0
  48. package/src/shared/file-utils.js +31 -0
  49. package/src/{paths.js → shared/paths.js} +2 -3
  50. package/src/{upstream-fetch.js → upstream/fetch.js} +1 -1
  51. package/src/cli.js +0 -684
  52. package/src/copilot.js +0 -572
  53. package/src/service.js +0 -1182
  54. /package/src/{app-status.js → cli/app-status.js} +0 -0
  55. /package/src/{model-catalog.js → proxy/model-catalog.js} +0 -0
  56. /package/src/{model-utils.js → proxy/model-utils.js} +0 -0
  57. /package/src/{constants.js → shared/constants.js} +0 -0
  58. /package/src/{crypto.js → shared/crypto.js} +0 -0
  59. /package/src/{http-utils.js → shared/http-utils.js} +0 -0
  60. /package/src/{log.js → shared/log.js} +0 -0
  61. /package/src/{prompt-utils.js → shared/prompt-utils.js} +0 -0
  62. /package/src/{proxy-config.js → upstream/proxy-config.js} +0 -0
  63. /package/src/{socks5-bridge.js → upstream/socks5-bridge.js} +0 -0
package/src/service.js DELETED
@@ -1,1182 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { configDir, apiKeyPath, githubTokenPath } from "./paths.js";
7
- import { loadConfig } from "./config.js";
8
- import { logEvent } from "./log.js";
9
-
10
- const SERVICE_LABEL = "com.jiayunxie.aerial";
11
- const WIN_TASK_NAME = "AerialLocalProxy";
12
- const PLIST_HEADER = "<!-- Generated by aerial; do not edit. Run `aerial service install` to regenerate. -->";
13
- const POSIX_HEADER = "# Generated by aerial; do not edit. Run `aerial service install` to regenerate.";
14
- const DEFAULT_WRAPPER_LOG_MAX_BYTES = 5 * 1024 * 1024;
15
- const DEFAULT_WRAPPER_LOG_BACKUPS = 3;
16
- const HEALTH_TIMEOUT_MS = 1500;
17
- const HEALTH_START_TIMEOUT_MS = 5000;
18
- const HEALTH_POLL_INTERVAL_MS = 250;
19
- const MIN_SERVICE_NODE_MAJOR = 24;
20
- const DARWIN_CODEX_NODE = "/Applications/Codex.app/Contents/Resources/node";
21
-
22
- function wrapperLogConfig() {
23
- const out = { maxBytes: DEFAULT_WRAPPER_LOG_MAX_BYTES, backups: DEFAULT_WRAPPER_LOG_BACKUPS };
24
- const rawMax = process.env.AERIAL_LOG_MAX_BYTES;
25
- if (rawMax && /^\d+$/.test(String(rawMax).trim())) {
26
- const n = Number(String(rawMax).trim());
27
- if (n > 0) out.maxBytes = n;
28
- }
29
- const rawBackups = process.env.AERIAL_LOG_BACKUPS;
30
- if (rawBackups && /^\d+$/.test(String(rawBackups).trim())) {
31
- const n = Number(String(rawBackups).trim());
32
- if (n >= 1) out.backups = n;
33
- }
34
- return out;
35
- }
36
-
37
- function defaultRunCommand(file, args, opts = {}) {
38
- if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
39
- const installed = process.env.AERIAL_SERVICE_DRYRUN_INSTALLED === "1";
40
- const fail = process.env.AERIAL_SERVICE_DRYRUN_FAIL || "";
41
- if (file === "schtasks.exe" && Array.isArray(args)) {
42
- if (args.includes("/Query")) {
43
- if (installed) {
44
- return { status: 0, signal: undefined, stdout: "TaskName: \\AerialLocalProxy\r\nStatus: Running", stderr: "", error: undefined, dryRun: true };
45
- }
46
- return { status: 1, signal: undefined, stdout: "", stderr: "(dryrun) task not registered", error: undefined, dryRun: true };
47
- }
48
- if (args.includes("/Delete") && fail === "delete") {
49
- return { status: 9, signal: undefined, stdout: "", stderr: "ERROR: Access is denied.", error: undefined, dryRun: true };
50
- }
51
- }
52
- if (file === "launchctl" && Array.isArray(args)) {
53
- if (args[0] === "list") {
54
- if (installed) {
55
- return { status: 0, signal: undefined, stdout: '{\n\t"PID" = 1234;\n\t"LastExitStatus" = 0;\n};', stderr: "", error: undefined, dryRun: true };
56
- }
57
- return { status: 1, signal: undefined, stdout: "", stderr: "(dryrun) service not loaded", error: undefined, dryRun: true };
58
- }
59
- if (args[0] === "bootout" && fail === "bootout") {
60
- return { status: 9216, signal: undefined, stdout: "", stderr: "Boot-out failed: 5: Input/output error", error: undefined, dryRun: true };
61
- }
62
- }
63
- return { status: 0, signal: undefined, stdout: "", stderr: "", error: undefined, dryRun: true };
64
- }
65
- const res = spawnSync(file, args, {
66
- stdio: opts.stdio || "pipe",
67
- encoding: "utf8",
68
- timeout: opts.timeout || 15000,
69
- env: opts.env || process.env,
70
- windowsHide: true
71
- });
72
- return {
73
- status: res.status,
74
- signal: res.signal,
75
- stdout: res.stdout || "",
76
- stderr: res.stderr || "",
77
- error: res.error
78
- };
79
- }
80
-
81
- function parseNodeMajor(versionText) {
82
- const match = /^v?(\d+)\./.exec(String(versionText || "").trim());
83
- return match ? Number(match[1]) : undefined;
84
- }
85
-
86
- function nodeMajorOfBinary(file) {
87
- if (!file || !fs.existsSync(file)) return undefined;
88
- const result = spawnSync(file, ["--version"], {
89
- stdio: "pipe",
90
- encoding: "utf8",
91
- timeout: 3000,
92
- windowsHide: true
93
- });
94
- if (result.status !== 0) return undefined;
95
- return parseNodeMajor(result.stdout || result.stderr);
96
- }
97
-
98
- function selectServiceNodeBinary({ requested, current, candidates = [], versionOf = nodeMajorOfBinary } = {}) {
99
- if (requested) return requested;
100
- if (versionOf(current) >= MIN_SERVICE_NODE_MAJOR) return current;
101
- for (const candidate of candidates) {
102
- if (versionOf(candidate) >= MIN_SERVICE_NODE_MAJOR) return candidate;
103
- }
104
- return current;
105
- }
106
-
107
- function serviceNodeCandidates() {
108
- if (process.platform === "darwin") return [DARWIN_CODEX_NODE];
109
- return [];
110
- }
111
-
112
- function nodeBinary() {
113
- return selectServiceNodeBinary({
114
- requested: process.env.AERIAL_SERVICE_NODE,
115
- current: process.execPath,
116
- candidates: serviceNodeCandidates()
117
- });
118
- }
119
-
120
- function cliEntry() {
121
- if (process.env.AERIAL_SERVICE_CLI) return process.env.AERIAL_SERVICE_CLI;
122
- return path.join(path.dirname(fileURLToPath(import.meta.url)), "cli.js");
123
- }
124
-
125
- function logsDir() {
126
- return process.env.AERIAL_LOG_DIR || path.join(configDir(), "logs");
127
- }
128
-
129
- function aerialLogPath() {
130
- return path.join(logsDir(), "aerial.log");
131
- }
132
-
133
- function stdioLogPath() {
134
- return path.join(logsDir(), "aerial-stdio.log");
135
- }
136
-
137
- function plistPath() {
138
- return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
139
- }
140
-
141
- function darwinWrapperPath() {
142
- return path.join(configDir(), "bin", "aerial-service.sh");
143
- }
144
-
145
- function winWrapperPath() {
146
- return path.join(configDir(), "bin", "aerial-service.ps1");
147
- }
148
-
149
- function ensureDir(file) {
150
- fs.mkdirSync(path.dirname(file), { recursive: true });
151
- }
152
-
153
- function atomicWriteText(file, content, { mode } = {}) {
154
- ensureDir(file);
155
- const tmp = `${file}.aerial-svc-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
156
- const opts = mode !== undefined ? { mode } : undefined;
157
- fs.writeFileSync(tmp, content, opts);
158
- try {
159
- if (process.platform === "win32" && fs.existsSync(file)) fs.unlinkSync(file);
160
- fs.renameSync(tmp, file);
161
- } catch (err) {
162
- try { fs.unlinkSync(tmp); } catch {}
163
- throw err;
164
- }
165
- if (process.platform !== "win32" && mode !== undefined) {
166
- try { fs.chmodSync(file, mode); } catch {}
167
- }
168
- }
169
-
170
- function xmlEscape(value) {
171
- return String(value)
172
- .replace(/&/g, "&amp;")
173
- .replace(/</g, "&lt;")
174
- .replace(/>/g, "&gt;")
175
- .replace(/"/g, "&quot;")
176
- .replace(/'/g, "&apos;");
177
- }
178
-
179
- function shEscape(value) {
180
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
181
- }
182
-
183
- function psEscape(value) {
184
- return `'${String(value).replace(/'/g, "''")}'`;
185
- }
186
-
187
- function uidString() {
188
- if (process.platform !== "darwin") return "";
189
- const uid = process.getuid?.();
190
- return uid === undefined || uid === null ? "" : String(uid);
191
- }
192
-
193
- function explicitConfigDir() {
194
- const v = process.env.AERIAL_CONFIG_DIR;
195
- return v && v.trim() ? v : undefined;
196
- }
197
-
198
- export function renderPlist({ label = SERVICE_LABEL, wrapperPath: wrapper }) {
199
- return [
200
- '<?xml version="1.0" encoding="UTF-8"?>',
201
- PLIST_HEADER,
202
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
203
- '<plist version="1.0">',
204
- "<dict>",
205
- " <key>Label</key>",
206
- ` <string>${xmlEscape(label)}</string>`,
207
- " <key>ProgramArguments</key>",
208
- " <array>",
209
- " <string>/bin/sh</string>",
210
- ` <string>${xmlEscape(wrapper)}</string>`,
211
- " </array>",
212
- " <key>RunAtLoad</key>",
213
- " <true/>",
214
- " <key>KeepAlive</key>",
215
- " <dict>",
216
- " <key>SuccessfulExit</key>",
217
- " <false/>",
218
- " <key>Crashed</key>",
219
- " <true/>",
220
- " </dict>",
221
- " <key>ThrottleInterval</key>",
222
- " <integer>10</integer>",
223
- "</dict>",
224
- "</plist>",
225
- ""
226
- ].join("\n");
227
- }
228
-
229
- export function renderDarwinWrapper({ nodePath, cliPath, host, port, stdioLog, aerialLog, configDir: cfg, maxBytes, backups }) {
230
- const max = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULT_WRAPPER_LOG_MAX_BYTES;
231
- const keep = Number.isInteger(backups) && backups >= 1 ? backups : DEFAULT_WRAPPER_LOG_BACKUPS;
232
- const lines = [
233
- "#!/bin/sh",
234
- POSIX_HEADER,
235
- "set -e",
236
- `STDIO_LOG=${shEscape(stdioLog)}`,
237
- `AERIAL_LOG_FILE=${shEscape(aerialLog)}`,
238
- `MAX_BYTES=${max}`,
239
- `BACKUPS=${keep}`,
240
- `NODE_BIN=${shEscape(nodePath)}`,
241
- `CLI_ENTRY=${shEscape(cliPath)}`,
242
- `HOST=${shEscape(host)}`,
243
- `PORT=${Number(port)}`,
244
- "STDIO_DIR=$(dirname \"$STDIO_LOG\")",
245
- "AERIAL_DIR=$(dirname \"$AERIAL_LOG_FILE\")",
246
- "mkdir -p \"$STDIO_DIR\" \"$AERIAL_DIR\"",
247
- "if [ -f \"$STDIO_LOG\" ]; then",
248
- " SIZE=$(wc -c < \"$STDIO_LOG\" | tr -d ' ')",
249
- " if [ \"$SIZE\" -gt \"$MAX_BYTES\" ]; then",
250
- " i=$((BACKUPS - 1))",
251
- " while [ \"$i\" -ge 1 ]; do",
252
- " SRC=\"$STDIO_LOG.$i\"",
253
- " DST=\"$STDIO_LOG.$((i + 1))\"",
254
- " if [ -f \"$SRC\" ]; then mv \"$SRC\" \"$DST\"; fi",
255
- " i=$((i - 1))",
256
- " done",
257
- " mv \"$STDIO_LOG\" \"$STDIO_LOG.1\"",
258
- " fi",
259
- "fi",
260
- "export AERIAL_LOG_FILE",
261
- `export AERIAL_LOG_MAX_BYTES=${max}`,
262
- `export AERIAL_LOG_BACKUPS=${keep}`
263
- ];
264
- if (cfg) lines.push(`export AERIAL_CONFIG_DIR=${shEscape(cfg)}`);
265
- lines.push("exec \"$NODE_BIN\" \"$CLI_ENTRY\" start --host \"$HOST\" --port \"$PORT\" >> \"$STDIO_LOG\" 2>&1");
266
- lines.push("");
267
- return lines.join("\n");
268
- }
269
-
270
- export function renderWindowsWrapper({ nodePath, cliPath, host, port, stdioLog, aerialLog, configDir: cfg, maxBytes, backups }) {
271
- const max = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULT_WRAPPER_LOG_MAX_BYTES;
272
- const keep = Number.isInteger(backups) && backups >= 1 ? backups : DEFAULT_WRAPPER_LOG_BACKUPS;
273
- const lines = [
274
- POSIX_HEADER,
275
- "$ErrorActionPreference = 'Stop'",
276
- `$node = ${psEscape(nodePath)}`,
277
- `$cli = ${psEscape(cliPath)}`,
278
- `$serviceHost = ${psEscape(host)}`,
279
- `$servicePort = ${Number(port)}`,
280
- `$stdioLog = ${psEscape(stdioLog)}`,
281
- `$aerialLog = ${psEscape(aerialLog)}`,
282
- `$maxBytes = ${max}`,
283
- `$backups = ${keep}`,
284
- "$stdioDir = Split-Path -Parent $stdioLog",
285
- "$aerialDir = Split-Path -Parent $aerialLog",
286
- "if (-not (Test-Path -LiteralPath $stdioDir)) { New-Item -ItemType Directory -Path $stdioDir | Out-Null }",
287
- "if (-not (Test-Path -LiteralPath $aerialDir)) { New-Item -ItemType Directory -Path $aerialDir | Out-Null }",
288
- "if (Test-Path -LiteralPath $stdioLog) {",
289
- " $size = (Get-Item -LiteralPath $stdioLog).Length",
290
- " if ($size -gt $maxBytes) {",
291
- " for ($i = $backups - 1; $i -ge 1; $i--) {",
292
- " $src = \"$stdioLog.$i\"",
293
- " $dst = \"$stdioLog.$($i + 1)\"",
294
- " if (Test-Path -LiteralPath $src) {",
295
- " if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
296
- " Move-Item -LiteralPath $src -Destination $dst",
297
- " }",
298
- " }",
299
- " if (Test-Path -LiteralPath \"$stdioLog.1\") { Remove-Item -LiteralPath \"$stdioLog.1\" -Force }",
300
- " Move-Item -LiteralPath $stdioLog -Destination \"$stdioLog.1\"",
301
- " }",
302
- "}",
303
- "$env:AERIAL_LOG_FILE = $aerialLog",
304
- "$env:AERIAL_LOG_MAX_BYTES = \"$maxBytes\"",
305
- "$env:AERIAL_LOG_BACKUPS = \"$backups\""
306
- ];
307
- if (cfg) lines.push(`$env:AERIAL_CONFIG_DIR = ${psEscape(cfg)}`);
308
- lines.push("& $node $cli start --host $serviceHost --port $servicePort *>> $stdioLog");
309
- lines.push("");
310
- return lines.join("\r\n");
311
- }
312
-
313
- function quoteSchtasksTR(command) {
314
- const wrapped = command.replace(/"/g, '\\"');
315
- return `\\"${wrapped}\\"`;
316
- }
317
-
318
- export function buildSchtasksCreateArgs({ taskName = WIN_TASK_NAME, wrapperPath: wrapper }) {
319
- const cmd = `powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${wrapper}"`;
320
- return [
321
- "/Create",
322
- "/TN", taskName,
323
- "/SC", "ONLOGON",
324
- "/RL", "LIMITED",
325
- "/F",
326
- "/TR", quoteSchtasksTR(cmd)
327
- ];
328
- }
329
-
330
- function buildSchtasksArgs(action, taskName = WIN_TASK_NAME) {
331
- if (action === "delete") return ["/Delete", "/TN", taskName, "/F"];
332
- if (action === "run") return ["/Run", "/TN", taskName];
333
- if (action === "end") return ["/End", "/TN", taskName];
334
- if (action === "query") return ["/Query", "/TN", taskName, "/FO", "LIST"];
335
- throw new Error(`Unsupported schtasks action: ${action}`);
336
- }
337
-
338
- function isUnsupportedPlatform() {
339
- return process.platform !== "darwin" && process.platform !== "win32";
340
- }
341
-
342
- function unsupportedError(action) {
343
- const platform = process.platform;
344
- return new Error(`aerial service ${action}: unsupported platform (${platform}). Service management is implemented for macOS (launchd) and Windows (Task Scheduler). On ${platform}, run \`aerial start\` directly or wrap it in your own init system.`);
345
- }
346
-
347
- async function defaultHealthFetch(host, port) {
348
- if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
349
- return { ok: false, error: "dryrun" };
350
- }
351
- const controller = new AbortController();
352
- const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
353
- try {
354
- const res = await fetch(`http://${host}:${port}/health`, { signal: controller.signal });
355
- let body;
356
- let parseFailed = false;
357
- try {
358
- body = await res.json();
359
- } catch {
360
- parseFailed = true;
361
- }
362
- return { ok: res.ok, status: res.status, body, parseFailed };
363
- } catch (err) {
364
- return { ok: false, error: err.message };
365
- } finally {
366
- clearTimeout(timer);
367
- }
368
- }
369
-
370
- function classifyHealth(probe) {
371
- if (!probe || probe.error) return { mode: "absent" };
372
- if (probe.status !== 200) return { mode: "absent", httpStatus: probe.status };
373
- if (probe.parseFailed) return { mode: "port_conflict", reason: "health body is not JSON" };
374
- if (!probe.body || probe.body.service !== "aerial") {
375
- return { mode: "port_conflict", reason: "200 response but not Aerial" };
376
- }
377
- return { mode: "aerial_running", body: probe.body };
378
- }
379
-
380
- async function pollForAerialUp(host, port, healthFetch, deadlineMs = HEALTH_START_TIMEOUT_MS) {
381
- const fetcher = healthFetch || defaultHealthFetch;
382
- const start = Date.now();
383
- let lastProbe;
384
- let lastCls;
385
- let attempts = 0;
386
- while (Date.now() - start < deadlineMs) {
387
- attempts += 1;
388
- lastProbe = await fetcher(host, port);
389
- lastCls = classifyHealth(lastProbe);
390
- if (lastCls.mode === "aerial_running" || lastCls.mode === "port_conflict") {
391
- return { cls: lastCls, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
392
- }
393
- await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));
394
- }
395
- return { cls: lastCls || { mode: "absent" }, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
396
- }
397
-
398
- function unescapeShSingleQuoted(line, prefix) {
399
- if (!line.startsWith(prefix)) return undefined;
400
- const rest = line.slice(prefix.length);
401
- if (!rest.startsWith("'")) return undefined;
402
- let i = 1;
403
- let out = "";
404
- while (i < rest.length) {
405
- const ch = rest[i];
406
- if (ch === "'") {
407
- if (rest.slice(i, i + 4) === "'\\''") {
408
- out += "'";
409
- i += 4;
410
- continue;
411
- }
412
- return out;
413
- }
414
- out += ch;
415
- i += 1;
416
- }
417
- return undefined;
418
- }
419
-
420
- function unescapePsSingleQuoted(line, prefix) {
421
- if (!line.startsWith(prefix)) return undefined;
422
- const rest = line.slice(prefix.length);
423
- if (!rest.startsWith("'")) return undefined;
424
- let i = 1;
425
- let out = "";
426
- while (i < rest.length) {
427
- const ch = rest[i];
428
- if (ch === "'") {
429
- if (rest[i + 1] === "'") {
430
- out += "'";
431
- i += 2;
432
- continue;
433
- }
434
- return out;
435
- }
436
- out += ch;
437
- i += 1;
438
- }
439
- return undefined;
440
- }
441
-
442
- function parseWrapperPaths(wrapperFile) {
443
- if (!wrapperFile) return { node: undefined, cli: undefined };
444
- try {
445
- if (!fs.existsSync(wrapperFile)) return { node: undefined, cli: undefined };
446
- const data = fs.readFileSync(wrapperFile, "utf8");
447
- const lines = data.split(/\r?\n/);
448
- if (wrapperFile.endsWith(".sh")) {
449
- let node;
450
- let cli;
451
- for (const line of lines) {
452
- if (node === undefined) {
453
- const candidate = unescapeShSingleQuoted(line, "NODE_BIN=");
454
- if (candidate !== undefined) node = candidate;
455
- }
456
- if (cli === undefined) {
457
- const candidate = unescapeShSingleQuoted(line, "CLI_ENTRY=");
458
- if (candidate !== undefined) cli = candidate;
459
- }
460
- if (node !== undefined && cli !== undefined) break;
461
- }
462
- return { node, cli };
463
- }
464
- if (wrapperFile.endsWith(".ps1")) {
465
- let node;
466
- let cli;
467
- for (const line of lines) {
468
- if (node === undefined) {
469
- const m = line.match(/^\$node\s*=\s*(.*)$/);
470
- if (m) {
471
- const candidate = unescapePsSingleQuoted(m[1].trim(), "");
472
- if (candidate !== undefined) node = candidate;
473
- }
474
- }
475
- if (cli === undefined) {
476
- const m = line.match(/^\$cli\s*=\s*(.*)$/);
477
- if (m) {
478
- const candidate = unescapePsSingleQuoted(m[1].trim(), "");
479
- if (candidate !== undefined) cli = candidate;
480
- }
481
- }
482
- if (node !== undefined && cli !== undefined) break;
483
- }
484
- return { node, cli };
485
- }
486
- } catch {}
487
- return { node: undefined, cli: undefined };
488
- }
489
-
490
- function readWrapperNodePath(wrapperFile) {
491
- return parseWrapperPaths(wrapperFile).node;
492
- }
493
-
494
- export const STALE_REASONS = Object.freeze({
495
- WRAPPER_MISSING: "wrapper_missing",
496
- WRAPPER_NODE_MISSING: "wrapper_node_missing",
497
- WRAPPER_CLI_MISSING: "wrapper_cli_missing",
498
- WRAPPER_LOG_CONFIG_UNPARSEABLE: "wrapper_log_config_unparseable"
499
- });
500
-
501
- function wrapperBlock(state) {
502
- if (!state || state.installed !== true) {
503
- return { stale: false, staleReasons: [] };
504
- }
505
- let wrapperPath;
506
- if (process.platform === "darwin") wrapperPath = darwinWrapperPath();
507
- else if (process.platform === "win32") wrapperPath = winWrapperPath();
508
- const wrapperFileExists = wrapperPath ? fs.existsSync(wrapperPath) : false;
509
- const staleReasons = [];
510
- if (!wrapperFileExists) {
511
- return {
512
- path: wrapperPath,
513
- nodePath: undefined,
514
- nodeExists: undefined,
515
- cliPath: undefined,
516
- cliExists: undefined,
517
- logConfigParseable: undefined,
518
- stale: true,
519
- staleReasons: [STALE_REASONS.WRAPPER_MISSING]
520
- };
521
- }
522
- const { node: nodePath, cli: cliPath } = parseWrapperPaths(wrapperPath);
523
- const nodeExists = nodePath ? fs.existsSync(nodePath) : false;
524
- const cliExists = cliPath ? fs.existsSync(cliPath) : false;
525
- const logConfigParseable = parseWrapperLogValues(wrapperPath) !== null;
526
- if (!nodeExists) staleReasons.push(STALE_REASONS.WRAPPER_NODE_MISSING);
527
- if (!cliExists) staleReasons.push(STALE_REASONS.WRAPPER_CLI_MISSING);
528
- if (!logConfigParseable) staleReasons.push(STALE_REASONS.WRAPPER_LOG_CONFIG_UNPARSEABLE);
529
- return {
530
- path: wrapperPath,
531
- nodePath,
532
- nodeExists,
533
- cliPath,
534
- cliExists,
535
- logConfigParseable,
536
- stale: staleReasons.length > 0,
537
- staleReasons
538
- };
539
- }
540
-
541
- function healthFailedDiagnostics({ wrapper, probe, attempts, elapsedMs }) {
542
- const out = {
543
- stdioLog: stdioLogPath(),
544
- aerialLog: aerialLogPath(),
545
- wrapperNode: readWrapperNodePath(wrapper),
546
- health: { attempts, elapsedMs }
547
- };
548
- if (probe && probe.error) out.health.lastError = probe.error;
549
- if (probe && probe.status !== undefined) out.health.lastStatus = probe.status;
550
- return out;
551
- }
552
-
553
- function authFileStatus(file) {
554
- if (!fs.existsSync(file)) return { file, state: "missing" };
555
- try {
556
- const data = fs.readFileSync(file, "utf8");
557
- if (!data || !data.trim()) return { file, state: "invalid", reason: "empty" };
558
- return { file, state: "present" };
559
- } catch (err) {
560
- return { file, state: "invalid", reason: err.message };
561
- }
562
- }
563
-
564
- function authBlock() {
565
- return {
566
- api_key: authFileStatus(apiKeyPath()),
567
- github_token: authFileStatus(githubTokenPath())
568
- };
569
- }
570
-
571
- function statFile(file) {
572
- if (!file) return { exists: false };
573
- try {
574
- if (!fs.existsSync(file)) return { exists: false };
575
- return { exists: true, size: fs.statSync(file).size };
576
- } catch {
577
- return { exists: false };
578
- }
579
- }
580
-
581
- function logsBlock() {
582
- const dir = logsDir();
583
- const primary = aerialLogPath();
584
- const stdio = stdioLogPath();
585
- let wrapperFile;
586
- if (process.platform === "darwin") wrapperFile = darwinWrapperPath();
587
- else if (process.platform === "win32") wrapperFile = winWrapperPath();
588
- const parsed = wrapperFile ? parseWrapperLogValues(wrapperFile) : null;
589
- let maxBytes;
590
- let rotateKeep;
591
- let source;
592
- if (parsed) {
593
- maxBytes = parsed.maxBytes;
594
- rotateKeep = parsed.backups;
595
- source = "installed-wrapper";
596
- } else {
597
- const cfg = wrapperLogConfig();
598
- maxBytes = cfg.maxBytes;
599
- rotateKeep = cfg.backups;
600
- source = "next-install-default";
601
- }
602
- return {
603
- dir,
604
- primary: { file: primary, ...statFile(primary) },
605
- stdio: { file: stdio, ...statFile(stdio) },
606
- maxFileBytes: maxBytes,
607
- rotateKeep,
608
- source
609
- };
610
- }
611
-
612
- function parseWrapperLogValues(file) {
613
- if (!file) return null;
614
- try {
615
- if (!fs.existsSync(file)) return null;
616
- const data = fs.readFileSync(file, "utf8");
617
- let maxBytes;
618
- let backups;
619
- if (file.endsWith(".sh")) {
620
- const m = data.match(/^MAX_BYTES=(\d+)\s*$/m);
621
- const b = data.match(/^BACKUPS=(\d+)\s*$/m);
622
- if (m) maxBytes = Number(m[1]);
623
- if (b) backups = Number(b[1]);
624
- } else if (file.endsWith(".ps1")) {
625
- const m = data.match(/^\$maxBytes\s*=\s*(\d+)\s*$/m);
626
- const b = data.match(/^\$backups\s*=\s*(\d+)\s*$/m);
627
- if (m) maxBytes = Number(m[1]);
628
- if (b) backups = Number(b[1]);
629
- }
630
- if (!Number.isInteger(maxBytes) || maxBytes <= 0) return null;
631
- if (!Number.isInteger(backups) || backups < 1) return null;
632
- return { maxBytes, backups };
633
- } catch {
634
- return null;
635
- }
636
- }
637
-
638
- function darwinServiceState(ctx) {
639
- const r = ctx.run("launchctl", ["list", SERVICE_LABEL]);
640
- const installed = fs.existsSync(plistPath());
641
- if (!installed) return { installed: false, loaded: false };
642
- if (r.status !== 0) return { installed: true, loaded: false };
643
- const pidMatch = /"PID"\s*=\s*(\d+)/.exec(r.stdout);
644
- const lastExitMatch = /"LastExitStatus"\s*=\s*(-?\d+)/.exec(r.stdout);
645
- return {
646
- installed: true,
647
- loaded: true,
648
- pid: pidMatch ? Number(pidMatch[1]) : undefined,
649
- lastExitStatus: lastExitMatch ? Number(lastExitMatch[1]) : undefined
650
- };
651
- }
652
-
653
- function windowsServiceState(ctx) {
654
- const r = ctx.run("schtasks.exe", buildSchtasksArgs("query"));
655
- if (r.status !== 0) return { installed: false, loaded: false };
656
- const statusMatch = /Status:\s*(\S+)/i.exec(r.stdout);
657
- const status = statusMatch ? statusMatch[1].trim() : undefined;
658
- return { installed: true, loaded: status === "Running", status };
659
- }
660
-
661
- function serviceState(ctx) {
662
- const adapter = serviceAdapter(ctx);
663
- if (adapter) return adapter.state();
664
- return { installed: false, loaded: false, reason: "unsupported_platform" };
665
- }
666
-
667
- function tokenWarning() {
668
- const tokenFile = githubTokenPath();
669
- if (fs.existsSync(tokenFile)) {
670
- try {
671
- const data = fs.readFileSync(tokenFile, "utf8");
672
- if (data && data.trim()) return undefined;
673
- } catch {}
674
- }
675
- const envToken = process.env.AERIAL_GITHUB_TOKEN;
676
- const envOnly = envToken && envToken.trim();
677
- const message = envOnly
678
- ? "AERIAL_GITHUB_TOKEN is set in this shell only; the background service does not inherit it. Run `aerial login` to persist a service-readable GitHub token, otherwise proxy requests return 503."
679
- : "GitHub token is not configured. Service will start, but proxy requests return 503 until you run `aerial login`.";
680
- return { level: "warning", code: "github_token_missing", message };
681
- }
682
-
683
- function darwinWriteDefinition() {
684
- const wrapper = darwinWrapperPath();
685
- const config = loadConfig();
686
- const logCfg = wrapperLogConfig();
687
- const wrapperContent = renderDarwinWrapper({
688
- nodePath: nodeBinary(),
689
- cliPath: cliEntry(),
690
- host: config.host,
691
- port: config.port,
692
- stdioLog: stdioLogPath(),
693
- aerialLog: aerialLogPath(),
694
- configDir: explicitConfigDir(),
695
- maxBytes: logCfg.maxBytes,
696
- backups: logCfg.backups
697
- });
698
- atomicWriteText(wrapper, wrapperContent, { mode: 0o755 });
699
- const file = plistPath();
700
- const plistContent = renderPlist({ wrapperPath: wrapper });
701
- atomicWriteText(file, plistContent, { mode: 0o644 });
702
- return { file, wrapper };
703
- }
704
-
705
- function darwinBootstrap(ctx) {
706
- const file = plistPath();
707
- const existing = ctx.run("launchctl", ["list", SERVICE_LABEL]);
708
- if (existing.status === 0) {
709
- ctx.run("launchctl", ["bootout", `gui/${uidString()}`, file], { stdio: "ignore" });
710
- }
711
- return ctx.run("launchctl", ["bootstrap", `gui/${uidString()}`, file]);
712
- }
713
-
714
- function darwinBootout(ctx) {
715
- const file = plistPath();
716
- return ctx.run("launchctl", ["bootout", `gui/${uidString()}`, file]);
717
- }
718
-
719
- function windowsWriteDefinition(ctx) {
720
- const wrapper = winWrapperPath();
721
- const config = loadConfig();
722
- const logCfg = wrapperLogConfig();
723
- const wrapperContent = renderWindowsWrapper({
724
- nodePath: nodeBinary(),
725
- cliPath: cliEntry(),
726
- host: config.host,
727
- port: config.port,
728
- stdioLog: stdioLogPath(),
729
- aerialLog: aerialLogPath(),
730
- configDir: explicitConfigDir(),
731
- maxBytes: logCfg.maxBytes,
732
- backups: logCfg.backups
733
- });
734
- atomicWriteText(wrapper, wrapperContent);
735
- const args = buildSchtasksCreateArgs({ wrapperPath: wrapper });
736
- const create = ctx.run("schtasks.exe", args);
737
- return { wrapper, create };
738
- }
739
-
740
- function serviceAdapter(ctx) {
741
- if (process.platform === "darwin") {
742
- return {
743
- platform: "darwin",
744
- wrapperPath: darwinWrapperPath,
745
- state: () => darwinServiceState(ctx),
746
- writeDefinition: () => {
747
- const written = darwinWriteDefinition();
748
- return {
749
- ok: true,
750
- info: { file: written.file, wrapper: written.wrapper, label: SERVICE_LABEL }
751
- };
752
- },
753
- triggerStart: () => darwinBootstrap(ctx),
754
- triggerStop: () => darwinBootout(ctx),
755
- startFailureReason: "bootstrap_failed",
756
- startResultKey: "bootstrap",
757
- uninstall: (state) => darwinUninstall(ctx, state)
758
- };
759
- }
760
- if (process.platform === "win32") {
761
- return {
762
- platform: "win32",
763
- wrapperPath: winWrapperPath,
764
- state: () => windowsServiceState(ctx),
765
- writeDefinition: () => {
766
- const written = windowsWriteDefinition(ctx);
767
- const info = {
768
- taskName: WIN_TASK_NAME,
769
- wrapper: written.wrapper,
770
- create: { status: written.create.status, stderr: written.create.stderr }
771
- };
772
- return { ok: written.create.status === 0, info };
773
- },
774
- triggerStart: () => ctx.run("schtasks.exe", buildSchtasksArgs("run")),
775
- triggerStop: () => ctx.run("schtasks.exe", buildSchtasksArgs("end")),
776
- startFailureReason: "run_failed",
777
- startResultKey: "run",
778
- uninstall: (state) => windowsUninstall(ctx, state)
779
- };
780
- }
781
- return undefined;
782
- }
783
-
784
- function requireServiceAdapter(ctx, action) {
785
- const adapter = serviceAdapter(ctx);
786
- if (!adapter) throw unsupportedError(action);
787
- return adapter;
788
- }
789
-
790
- function commandResultBlock(key, result) {
791
- return { [key]: { status: result.status, stderr: result.stderr } };
792
- }
793
-
794
- function buildDefinitionRefreshFailure({ adapter, supervisor, config, definition }) {
795
- const isManaged = supervisor === "service-managed";
796
- const status = definition.info.create?.status;
797
- const reason = isManaged ? "managed_definition_refresh_failed" : "foreground_definition_refresh_failed";
798
- const message = isManaged
799
- ? `schtasks /Create failed (status ${status}) while refreshing the managed-service definition. The running service was not disturbed, but wrapper/env changes were NOT applied. Resolve the underlying schtasks error and rerun \`aerial service install\`.`
800
- : `Aerial is already running in the foreground on port ${config.port}. The wrapper was rewritten but schtasks /Create failed (status ${status}), so the Task Scheduler definition was NOT refreshed. Resolve the underlying schtasks error and rerun \`aerial service install\`.`;
801
- return {
802
- ok: false,
803
- action: "install",
804
- platform: adapter.platform,
805
- reason,
806
- ...(isManaged ? {} : { definitionUpdated: false }),
807
- message,
808
- warning: tokenWarning(),
809
- ...definition.info
810
- };
811
- }
812
-
813
- function buildRunningDefinitionResult({ adapter, supervisor, config, definition }) {
814
- const managed = supervisor === "service-managed";
815
- return {
816
- ok: managed,
817
- action: "install",
818
- platform: adapter.platform,
819
- ...(managed
820
- ? {
821
- note: "already running (service-managed); definition refreshed; run `aerial service restart` to apply wrapper/env changes"
822
- }
823
- : {
824
- reason: "foreground_running",
825
- message: `Aerial is already running in the foreground on port ${config.port}. The service definition has been updated, but the service was NOT started to avoid running two instances. Next step: stop the foreground process, then run \`aerial service start\`.`
826
- }),
827
- definitionUpdated: true,
828
- warning: tokenWarning(),
829
- ...definition.info
830
- };
831
- }
832
-
833
- async function confirmStarted({ action, result, config, wrapper, healthFetch, healthDeadlineMs }) {
834
- if (!result.ok) return result;
835
- if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
836
- return { ...result, health: { ok: true, attempts: 0, elapsedMs: 0, dryRun: true } };
837
- }
838
- const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
839
- if (poll.cls.mode === "aerial_running") {
840
- return { ...result, health: { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs } };
841
- }
842
- const diagnostics = healthFailedDiagnostics({
843
- wrapper,
844
- probe: poll.probe,
845
- attempts: poll.attempts,
846
- elapsedMs: poll.elapsedMs
847
- });
848
- if (poll.cls.mode === "port_conflict") {
849
- const prefix = action === "install" ? "After install" : "After start";
850
- return {
851
- ...result,
852
- ok: false,
853
- reason: "port_conflict",
854
- message: `${prefix}, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`,
855
- diagnostics
856
- };
857
- }
858
- const message = action === "install"
859
- ? `Service definition was written and start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`
860
- : `Start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`;
861
- return {
862
- ...result,
863
- ok: false,
864
- reason: "health_check_failed",
865
- ...(action === "install" ? { definitionWritten: true } : {}),
866
- startAttempted: true,
867
- message,
868
- diagnostics
869
- };
870
- }
871
-
872
- async function describeRunning(adapter, host, port, healthFetch, knownState) {
873
- const probe = await (healthFetch || defaultHealthFetch)(host, port);
874
- const cls = classifyHealth(probe);
875
- if (cls.mode !== "aerial_running") return { cls, probe };
876
- const state = knownState || adapter.state();
877
- const supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
878
- return { cls, probe, supervisor };
879
- }
880
-
881
- export async function serviceInstall({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
882
- const ctx = { run };
883
- const adapter = requireServiceAdapter(ctx, "install");
884
- const config = loadConfig();
885
- const { cls, supervisor } = await describeRunning(adapter, config.host, config.port, healthFetch);
886
- if (cls.mode === "port_conflict") {
887
- logEvent("service_install", { platform: adapter.platform, ok: false, reason: "port_conflict" });
888
- return {
889
- ok: false,
890
- action: "install",
891
- platform: adapter.platform,
892
- reason: "port_conflict",
893
- message: `Port ${config.port} is already in use by a non-Aerial process: ${cls.reason}. Free the port and rerun.`
894
- };
895
- }
896
- if (cls.mode === "aerial_running") {
897
- const definition = adapter.writeDefinition();
898
- if (!definition.ok) {
899
- const failure = buildDefinitionRefreshFailure({ adapter, supervisor, config, definition });
900
- logEvent("service_install", {
901
- platform: adapter.platform,
902
- ok: false,
903
- reason: failure.reason,
904
- status: definition.info.create?.status
905
- });
906
- return failure;
907
- }
908
- const result = buildRunningDefinitionResult({ adapter, supervisor, config, definition });
909
- logEvent("service_install", {
910
- platform: adapter.platform,
911
- ok: result.ok,
912
- reason: result.reason,
913
- note: supervisor === "service-managed" ? "managed_refreshed" : undefined,
914
- definitionUpdated: true
915
- });
916
- return result;
917
- }
918
-
919
- const definition = adapter.writeDefinition();
920
- let result = {
921
- ok: definition.ok,
922
- action: "install",
923
- platform: adapter.platform,
924
- ...definition.info
925
- };
926
- if (!definition.ok) {
927
- result.reason = "create_failed";
928
- } else {
929
- const start = adapter.triggerStart();
930
- const triggerOk = start.status === 0;
931
- result = {
932
- ...result,
933
- ok: triggerOk,
934
- ...commandResultBlock(adapter.startResultKey, start),
935
- ...(triggerOk ? {} : { reason: adapter.startFailureReason })
936
- };
937
- }
938
- result = await confirmStarted({
939
- action: "install",
940
- result,
941
- config,
942
- wrapper: result.wrapper,
943
- healthFetch,
944
- healthDeadlineMs
945
- });
946
- result.warning = tokenWarning();
947
- logEvent("service_install", { platform: adapter.platform, ok: result.ok, reason: result.reason });
948
- return result;
949
- }
950
-
951
- export async function serviceStart({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
952
- const ctx = { run };
953
- const adapter = requireServiceAdapter(ctx, "start");
954
- const config = loadConfig();
955
- const state = adapter.state();
956
- if (!state.installed) {
957
- return {
958
- ok: false,
959
- action: "start",
960
- platform: adapter.platform,
961
- reason: "not_installed",
962
- message: "Service is not installed. Run `aerial service install` first."
963
- };
964
- }
965
- const { cls, supervisor } = await describeRunning(adapter, config.host, config.port, healthFetch, state);
966
- if (cls.mode === "port_conflict") {
967
- return {
968
- ok: false,
969
- action: "start",
970
- platform: adapter.platform,
971
- reason: "port_conflict",
972
- message: `Port ${config.port} is already in use by a non-Aerial process: ${cls.reason}. Free the port and rerun.`
973
- };
974
- }
975
- if (cls.mode === "aerial_running" && supervisor === "foreground") {
976
- return {
977
- ok: false,
978
- action: "start",
979
- platform: adapter.platform,
980
- reason: "foreground_running",
981
- message: `Aerial is already running in the foreground on port ${config.port}. Stop the foreground process before starting the service.`
982
- };
983
- }
984
- if (cls.mode === "aerial_running" && supervisor === "service-managed") {
985
- return {
986
- ok: true,
987
- action: "start",
988
- platform: adapter.platform,
989
- note: "already running (service-managed)",
990
- warning: tokenWarning()
991
- };
992
- }
993
- const r = adapter.triggerStart();
994
- const triggerOk = r.status === 0;
995
- let result = {
996
- ok: triggerOk,
997
- action: "start",
998
- platform: adapter.platform,
999
- status: r.status,
1000
- stderr: r.stderr,
1001
- warning: tokenWarning(),
1002
- ...(triggerOk ? {} : { reason: adapter.startFailureReason })
1003
- };
1004
- if (!triggerOk) {
1005
- logEvent("service_start", { platform: adapter.platform, status: r.status, reason: result.reason });
1006
- return result;
1007
- }
1008
- result = await confirmStarted({
1009
- action: "start",
1010
- result,
1011
- config,
1012
- wrapper: adapter.wrapperPath(),
1013
- healthFetch,
1014
- healthDeadlineMs
1015
- });
1016
- logEvent("service_start", { platform: adapter.platform, status: r.status, ok: result.ok, reason: result.reason, dryRun: result.health?.dryRun });
1017
- return result;
1018
- }
1019
-
1020
- export function serviceStop({ run = defaultRunCommand } = {}) {
1021
- const ctx = { run };
1022
- const adapter = requireServiceAdapter(ctx, "stop");
1023
- const state = adapter.state();
1024
- if (!state.installed) {
1025
- return { ok: true, action: "stop", platform: adapter.platform, note: "not installed" };
1026
- }
1027
- if (!state.loaded) {
1028
- return { ok: true, action: "stop", platform: adapter.platform, note: "not running" };
1029
- }
1030
- const r = adapter.triggerStop();
1031
- logEvent("service_stop", { platform: adapter.platform, status: r.status });
1032
- return { ok: r.status === 0, action: "stop", platform: adapter.platform, status: r.status, stderr: r.stderr };
1033
- }
1034
-
1035
- export async function serviceRestart(opts = {}) {
1036
- const stop = serviceStop(opts);
1037
- if (!stop.ok) {
1038
- return { ok: false, action: "restart", platform: process.platform, stop, reason: "stop_failed" };
1039
- }
1040
- const start = await serviceStart(opts);
1041
- return { ok: start.ok, action: "restart", platform: process.platform, stop, start, warning: start.warning };
1042
- }
1043
-
1044
- function removeFileIfExists(file) {
1045
- if (!fs.existsSync(file)) return false;
1046
- try {
1047
- fs.unlinkSync(file);
1048
- return true;
1049
- } catch {
1050
- return false;
1051
- }
1052
- }
1053
-
1054
- function darwinUninstall(ctx, state) {
1055
- const file = plistPath();
1056
- const wrapper = darwinWrapperPath();
1057
- if (state.loaded) {
1058
- const bootout = darwinBootout(ctx);
1059
- if (bootout.status !== 0) {
1060
- logEvent("service_uninstall", { platform: "darwin", ok: false, reason: "bootout_failed", status: bootout.status });
1061
- return {
1062
- ok: false,
1063
- action: "uninstall",
1064
- platform: "darwin",
1065
- reason: "bootout_failed",
1066
- file,
1067
- wrapper,
1068
- bootout: { status: bootout.status, stderr: bootout.stderr },
1069
- message: `launchctl bootout failed (status ${bootout.status}). Service is still loaded; plist and wrapper were preserved. Retry with \`aerial service uninstall\`.`
1070
- };
1071
- }
1072
- removeFileIfExists(file);
1073
- removeFileIfExists(wrapper);
1074
- logEvent("service_uninstall", { platform: "darwin", ok: true });
1075
- return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: bootout.status, stderr: bootout.stderr } };
1076
- }
1077
- removeFileIfExists(file);
1078
- removeFileIfExists(wrapper);
1079
- logEvent("service_uninstall", { platform: "darwin", ok: true });
1080
- return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: 0, skipped: "not_loaded" } };
1081
- }
1082
-
1083
- function windowsUninstall(ctx, state) {
1084
- if (state.loaded) {
1085
- ctx.run("schtasks.exe", buildSchtasksArgs("end"));
1086
- }
1087
- const del = ctx.run("schtasks.exe", buildSchtasksArgs("delete"));
1088
- const wrapper = winWrapperPath();
1089
- const wrapperRemoved = del.status === 0 ? removeFileIfExists(wrapper) : false;
1090
- logEvent("service_uninstall", { platform: "win32", ok: del.status === 0 });
1091
- return {
1092
- ok: del.status === 0,
1093
- action: "uninstall",
1094
- platform: "win32",
1095
- taskName: WIN_TASK_NAME,
1096
- wrapper,
1097
- wrapperRemoved,
1098
- delete: { status: del.status, stderr: del.stderr },
1099
- ...(del.status === 0 ? {} : { reason: "delete_failed", message: `schtasks /Delete failed (status ${del.status}). Task and wrapper were preserved. Retry with \`aerial service uninstall\`.` })
1100
- };
1101
- }
1102
-
1103
- export function serviceUninstall({ run = defaultRunCommand } = {}) {
1104
- const ctx = { run };
1105
- const adapter = requireServiceAdapter(ctx, "uninstall");
1106
- const state = adapter.state();
1107
- if (!state.installed) {
1108
- return { ok: true, action: "uninstall", platform: adapter.platform, note: "no service installed" };
1109
- }
1110
- return adapter.uninstall(state);
1111
- }
1112
-
1113
- export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {}) {
1114
- const config = loadConfig();
1115
- const ctx = { run };
1116
- if (isUnsupportedPlatform()) {
1117
- return {
1118
- schema: "aerial.service-status.v1",
1119
- platform: process.platform,
1120
- supported: false,
1121
- config: { host: config.host, port: config.port },
1122
- service: { installed: false, loaded: false, reason: "unsupported_platform", platform: process.platform, wrapper: wrapperBlock({ installed: false }) },
1123
- health: { ok: false, error: "unsupported_platform" },
1124
- logs: logsBlock(),
1125
- auth: authBlock(),
1126
- summary: "unsupported"
1127
- };
1128
- }
1129
- const state = serviceState(ctx);
1130
- const probe = await (healthFetch || defaultHealthFetch)(config.host, config.port);
1131
- const cls = classifyHealth(probe);
1132
- const wrapper = wrapperBlock(state);
1133
- let supervisor;
1134
- if (cls.mode === "aerial_running") {
1135
- supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
1136
- }
1137
- const health = { ...probe };
1138
- if (cls.mode === "port_conflict") {
1139
- health.portConflict = true;
1140
- health.conflictReason = cls.reason;
1141
- }
1142
- if (cls.mode === "aerial_running") {
1143
- health.aerial = true;
1144
- health.supervisor = supervisor;
1145
- }
1146
- let summary;
1147
- if (cls.mode === "aerial_running" && supervisor === "service-managed") summary = "running (service-managed)";
1148
- else if (cls.mode === "aerial_running" && supervisor === "foreground") summary = "running (foreground)";
1149
- else if (cls.mode === "port_conflict") summary = "port conflict (non-Aerial process on port)";
1150
- else if (state.installed && state.loaded) summary = "manager reports up but health failed";
1151
- else if (state.installed) summary = "installed (not running)";
1152
- else summary = "not installed";
1153
- return {
1154
- schema: "aerial.service-status.v1",
1155
- platform: process.platform,
1156
- supported: true,
1157
- config: { host: config.host, port: config.port },
1158
- service: { platform: process.platform, ...state, wrapper },
1159
- health,
1160
- logs: logsBlock(),
1161
- auth: authBlock(),
1162
- summary
1163
- };
1164
- }
1165
-
1166
- export const _internal = {
1167
- SERVICE_LABEL,
1168
- WIN_TASK_NAME,
1169
- plistPath,
1170
- darwinWrapperPath,
1171
- winWrapperPath,
1172
- aerialLogPath,
1173
- stdioLogPath,
1174
- parseWrapperLogValues,
1175
- parseWrapperPaths,
1176
- wrapperBlock,
1177
- buildSchtasksArgs,
1178
- quoteSchtasksTR,
1179
- classifyHealth,
1180
- parseNodeMajor,
1181
- selectServiceNodeBinary
1182
- };