@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/cli.js DELETED
@@ -1,684 +0,0 @@
1
- #!/usr/bin/env node
2
- import { fileURLToPath } from "node:url";
3
- import { startDeviceFlow, pollDeviceFlow, readGitHubToken, gitHubTokenSource } from "./auth.js";
4
- import { defaultConfig, ensureApiKey, loadConfig, saveConfig } from "./config.js";
5
- import { configPath } from "./paths.js";
6
- import { startServer } from "./server.js";
7
- import { setupClaude, setupCodex, setupStatus, restoreClient, restoreAllClients } from "./setup.js";
8
- import { serviceInstall, serviceStart, serviceStop, serviceRestart, serviceUninstall, serviceStatus } from "./service.js";
9
- import { doctor, renderDoctorText } from "./doctor.js";
10
- import { runProbe, formatProbeReport } from "./probe.js";
11
- import { chooseSetupModel, formatModelChoices } from "./model-selection.js";
12
- import { chooseSetupEffort, formatEffortSelection, assertValidEffort } from "./setup-selection.js";
13
- import { printVersion } from "./version.js";
14
- import { computeAppStatus } from "./app-status.js";
15
- import { disableUpstreamProxy, enableUpstreamProxy, probeEgress, upstreamProxyState } from "./upstream-fetch.js";
16
- import { redactProxyEndpoint, redactProxySource } from "./proxy-config.js";
17
-
18
- const CLI_ENTRY = fileURLToPath(import.meta.url);
19
-
20
- function codexAuthCommand() {
21
- return {
22
- command: process.execPath,
23
- args: [CLI_ENTRY, "key", "print"],
24
- timeout_ms: 5000,
25
- refresh_interval_ms: 0
26
- };
27
- }
28
-
29
- function quoteCommandPart(value) {
30
- return `"${String(value).replace(/"/g, '\\"')}"`;
31
- }
32
-
33
- function claudeApiKeyHelper() {
34
- return [process.execPath, CLI_ENTRY, "key", "print"].map(quoteCommandPart).join(" ");
35
- }
36
-
37
- function printHelp() {
38
- console.log(`Aerial local Copilot proxy
39
-
40
- Usage:
41
- aerial --version
42
- aerial login
43
- aerial setup codex [--model <id>] [--effort <low|medium|high|xhigh|max>]
44
- aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
45
- aerial service install
46
- aerial status [--json]
47
- aerial proxy status|enable|disable [--json]
48
-
49
- Diagnostics and rollback:
50
- aerial setup status [--json]
51
- aerial setup restore <codex|claude|all> --latest
52
- aerial service status [--json]
53
- aerial disable
54
- aerial doctor
55
- aerial probe [--live] [--json]
56
-
57
- Debug:
58
- aerial start [--host 127.0.0.1] [--port 18181]`);
59
- }
60
-
61
- function argValue(args, name) {
62
- const index = args.indexOf(name);
63
- return index >= 0 ? args[index + 1] : undefined;
64
- }
65
-
66
- function requiredArgValue(args, name) {
67
- const index = args.indexOf(name);
68
- if (index < 0) return undefined;
69
- const value = args[index + 1];
70
- if (value === undefined || value.startsWith("--")) {
71
- throw new Error(`${name} requires a value.`);
72
- }
73
- return value;
74
- }
75
-
76
- function parseConfigPort(value) {
77
- const text = String(value).trim();
78
- if (!/^\d+$/.test(text)) throw new Error("port must be an integer between 1 and 65535");
79
- const port = Number(text);
80
- if (port < 1 || port > 65535) throw new Error("port must be an integer between 1 and 65535");
81
- return port;
82
- }
83
-
84
- function parseConfigHost(value) {
85
- const host = String(value).trim().toLowerCase();
86
- if (host === "127.0.0.1" || host === "localhost" || host === "::1") return host;
87
- throw new Error("host must be a loopback address: 127.0.0.1, localhost, or ::1");
88
- }
89
-
90
- async function selectSetupOptions(target, route, args) {
91
- const explicitEffort = requiredArgValue(args, "--effort");
92
- if (explicitEffort !== undefined) assertValidEffort(explicitEffort);
93
- const selected = await chooseSetupModel({ target, route, explicitModel: requiredArgValue(args, "--model") });
94
- if (!selected.displayed) {
95
- for (const line of formatModelChoices({ target, route, choices: selected.choices, selectedModel: selected.model, source: selected.source, recommended: selected.recommended })) {
96
- console.log(line);
97
- }
98
- } else {
99
- console.log(`Selected ${target} model: ${selected.model}`);
100
- }
101
- const effortChoice = await chooseSetupEffort({ target, explicitEffort });
102
- console.log(formatEffortSelection({ target, effort: effortChoice.effort, source: effortChoice.source }));
103
- return {
104
- model: selected.model,
105
- effort: effortChoice.effort,
106
- modelSource: selected.source,
107
- effortSource: effortChoice.source,
108
- modelDisplayed: Boolean(selected.displayed),
109
- effortDisplayed: Boolean(effortChoice.displayed)
110
- };
111
- }
112
-
113
- function printSetupCompletionSummary({ heading, cli, model, effort, proxy, configFile, aerialConfigFile, aerialDefaultEffort, backup, auth, notes = [] }) {
114
- console.log(heading);
115
- console.log(` cli: ${cli}`);
116
- console.log(` model: ${model}`);
117
- console.log(` effort: ${effort}`);
118
- console.log(` proxy: ${proxy}`);
119
- console.log(` client config: ${configFile}`);
120
- console.log(` aerial config: ${aerialConfigFile}`);
121
- console.log(` aerial defaultEffort: ${aerialDefaultEffort}`);
122
- console.log(` backup: ${backup || "none"}`);
123
- console.log(` auth: ${auth}`);
124
- for (const note of notes) console.log(` note: ${note}`);
125
- }
126
-
127
- function printSetupSummary(status) {
128
- console.log("clients:");
129
- for (const client of Object.values(status.clients)) {
130
- const model = client.model ? ` model=${client.model}` : "";
131
- const effort = ` effort=${client.effort || "missing"}`;
132
- console.log(` ${client.target}: ${client.state}${model}${effort}`);
133
- }
134
- console.log(`api key: ${status.auth.api_key.exists ? "present" : "missing"}`);
135
- const ghSource = status.auth.github_token.source;
136
- const ghText = ghSource === "missing" ? "missing" : `present (${ghSource})`;
137
- console.log(`github login: ${ghText}`);
138
- }
139
-
140
- function printServiceSummary(status) {
141
- console.log(`service: ${status.summary}`);
142
- if (status.supported === false) {
143
- console.log(`platform: ${status.platform} (service unsupported)`);
144
- return;
145
- }
146
- const health = status.health?.aerial ? `ok (${status.health.supervisor})`
147
- : status.health?.portConflict ? `port conflict (${status.health.conflictReason})`
148
- : status.health?.ok ? "ok"
149
- : `unreachable (${status.health?.error || `http ${status.health?.status}`})`;
150
- console.log(`health: ${health}`);
151
- }
152
-
153
- function printRestoreResults(results) {
154
- for (const r of Object.values(results)) {
155
- if (r.restored) {
156
- console.log(`Restored ${r.target}: ${r.file} <- ${r.from}`);
157
- if (r.snapshot) console.log(` pre-restore snapshot: ${r.snapshot}`);
158
- } else if (r.reason === "no_backup") {
159
- console.log(`Restored ${r.target}: no backup to restore`);
160
- } else if (r.error) {
161
- console.log(`Restored ${r.target}: FAILED ${r.error}`);
162
- }
163
- }
164
- }
165
-
166
- function printServiceDiagnostics(diagnostics) {
167
- if (!diagnostics) return;
168
- if (diagnostics.stdioLog) console.log(` stdio log: ${diagnostics.stdioLog}`);
169
- if (diagnostics.aerialLog) console.log(` aerial log: ${diagnostics.aerialLog}`);
170
- if (diagnostics.wrapperNode) console.log(` wrapper node: ${diagnostics.wrapperNode}`);
171
- if (diagnostics.health) {
172
- const h = diagnostics.health;
173
- const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
174
- console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
175
- }
176
- console.log(` Run: aerial service status --json`);
177
- }
178
-
179
- function printServiceWarning(result) {
180
- if (result.warning) console.log(` WARNING: ${result.warning.message}`);
181
- }
182
-
183
- function formatEgress(egress) {
184
- if (!egress?.ok) return `unavailable (${egress?.error || "unknown"})`;
185
- const place = [egress.city, egress.region, egress.country].filter(Boolean).join(", ");
186
- return [egress.ip, place].filter(Boolean).join(" ");
187
- }
188
-
189
- async function proxyRouteSummary() {
190
- try {
191
- const report = await runProbe();
192
- if (!report.ok) return { ok: false, error: JSON.stringify(report.error) };
193
- return {
194
- ok: true,
195
- models: report.models.length,
196
- responses: report.summary.responses,
197
- responsesWebSocket: report.summary.websocketResponses,
198
- messages: report.summary.messages,
199
- chat: report.summary.chat
200
- };
201
- } catch (err) {
202
- return { ok: false, error: err.message };
203
- }
204
- }
205
-
206
- async function proxyStatus({ includeRoutes = true } = {}) {
207
- const state = upstreamProxyState(loadConfig());
208
- const egress = await probeEgress({ endpoint: state.endpoint });
209
- return {
210
- schema: "aerial.proxy-status.v1",
211
- mode: state.mode,
212
- enabled: state.enabled,
213
- endpoint: state.endpoint,
214
- source: state.source,
215
- egress,
216
- routes: includeRoutes ? await proxyRouteSummary() : undefined
217
- };
218
- }
219
-
220
- function publicProxyCandidate(candidate) {
221
- if (!candidate || typeof candidate !== "object") return candidate;
222
- return {
223
- ...candidate,
224
- endpoint: redactProxyEndpoint(candidate.endpoint),
225
- source: redactProxySource(candidate.source)
226
- };
227
- }
228
-
229
- function publicProxyStatus(status) {
230
- if (!status || typeof status !== "object") return status;
231
- return {
232
- ...status,
233
- endpoint: redactProxyEndpoint(status.endpoint),
234
- source: redactProxySource(status.source)
235
- };
236
- }
237
-
238
- function publicProxyEnableResult(result) {
239
- if (!result || typeof result !== "object") return result;
240
- return {
241
- ...result,
242
- selected: publicProxyCandidate(result.selected),
243
- candidates: Array.isArray(result.candidates) ? result.candidates.map(publicProxyCandidate) : result.candidates,
244
- status: publicProxyStatus(result.status)
245
- };
246
- }
247
-
248
- function printProxyStatus(status) {
249
- const safeStatus = publicProxyStatus(status);
250
- console.log(`proxy: ${status.enabled ? `enabled (${status.mode})` : "disabled"}`);
251
- console.log(`endpoint: ${status.enabled ? `${safeStatus.endpoint} (${safeStatus.source})` : "direct"}`);
252
- console.log(`egress: ${formatEgress(status.egress)}`);
253
- if (status.routes) {
254
- if (status.routes.ok) {
255
- console.log(`copilot routes: responses=${status.routes.responses}, responsesWebSocket=${status.routes.responsesWebSocket}, messages=${status.routes.messages}, chat=${status.routes.chat}`);
256
- } else {
257
- console.log(`copilot routes: unavailable (${status.routes.error})`);
258
- }
259
- }
260
- }
261
-
262
- async function runProxyCli(subcommand, rest) {
263
- const json = rest.includes("--json") || subcommand === "--json";
264
- if (!subcommand || subcommand === "status" || subcommand === "--json") {
265
- const status = await proxyStatus();
266
- if (json) console.log(JSON.stringify(publicProxyStatus(status), null, 2));
267
- else printProxyStatus(status);
268
- return;
269
- }
270
- if (subcommand === "enable") {
271
- const result = await enableUpstreamProxy();
272
- if (!result.ok) {
273
- if (json) console.log(JSON.stringify(publicProxyEnableResult(result), null, 2));
274
- else {
275
- console.error(`Proxy enable failed: ${result.error}`);
276
- if (result.candidates?.length) {
277
- console.error(`Checked: ${result.candidates.map((c) => redactProxyEndpoint(c.endpoint)).join(", ")}`);
278
- }
279
- }
280
- process.exitCode = 1;
281
- return;
282
- }
283
- const status = await proxyStatus();
284
- if (json) console.log(JSON.stringify(publicProxyEnableResult({ ...result, status }), null, 2));
285
- else {
286
- console.log(`Proxy enabled: ${redactProxyEndpoint(result.selected.endpoint)} (${redactProxySource(result.selected.source)})`);
287
- printProxyStatus(status);
288
- }
289
- return;
290
- }
291
- if (subcommand === "disable") {
292
- disableUpstreamProxy();
293
- const status = await proxyStatus();
294
- if (json) console.log(JSON.stringify(publicProxyStatus(status), null, 2));
295
- else {
296
- console.log("Proxy disabled.");
297
- printProxyStatus(status);
298
- }
299
- return;
300
- }
301
- throw new Error("Usage: aerial proxy status|enable|disable [--json]");
302
- }
303
-
304
- function printServiceUninstallResult(r, { prefix = "Service uninstall" } = {}) {
305
- if (r.note === "no service installed") console.log(`${prefix}: no service installed`);
306
- else if (r.ok) console.log(`${prefix}: ok (${r.platform})`);
307
- else {
308
- console.log(`${prefix}: FAILED (${r.reason || "see stderr"})`);
309
- if (r.message) console.log(` ${r.message}`);
310
- else console.log(` Retry with: aerial service uninstall`);
311
- }
312
- if (r.delete?.stderr) console.log(` schtasks stderr: ${r.delete.stderr.trim()}`);
313
- if (r.bootout?.stderr) console.log(` bootout stderr: ${r.bootout.stderr.trim()}`);
314
- }
315
-
316
- async function runServiceCliAction(action, render) {
317
- try {
318
- const result = await action();
319
- render(result);
320
- process.exitCode = result.ok ? 0 : 1;
321
- } catch (err) {
322
- console.error(err.message);
323
- process.exitCode = 1;
324
- }
325
- }
326
-
327
- async function appStatus({ json = false } = {}) {
328
- const setup = setupStatus();
329
- const service = await serviceStatus();
330
- const status = computeAppStatus(setup, service);
331
- if (json) {
332
- console.log(JSON.stringify(status, null, 2));
333
- return status;
334
- }
335
- console.log("Aerial status");
336
- printSetupSummary(setup);
337
- printServiceSummary(service);
338
- if (status.nextSteps.length) {
339
- console.log("next:");
340
- for (const step of status.nextSteps) console.log(` - ${step}`);
341
- }
342
- if (status.hints.length) {
343
- console.log("hints:");
344
- for (const hint of status.hints) console.log(` - ${hint}`);
345
- }
346
- return status;
347
- }
348
-
349
- async function main() {
350
- const args = process.argv.slice(2);
351
- const [command, subcommand, ...rest] = args;
352
- if (!command || command === "--help" || command === "-h") return printHelp();
353
- if (command === "--version") return printVersion();
354
- if (command === "status") {
355
- const status = await appStatus({ json: args.includes("--json") });
356
- process.exitCode = status.ok ? 0 : 1;
357
- return;
358
- }
359
-
360
- if (command === "proxy") {
361
- await runProxyCli(subcommand, rest);
362
- return;
363
- }
364
-
365
- if (command === "login") {
366
- const loginArgs = args.slice(1);
367
- const force = loginArgs.includes("--force");
368
- const source = gitHubTokenSource();
369
- if (force && source === "env") {
370
- console.error("AERIAL_GITHUB_TOKEN is set; unset it before running `aerial login --force`, otherwise the env value will continue to shadow any new file token.");
371
- process.exit(1);
372
- }
373
- if (!force && source === "env") {
374
- console.log("GitHub login is provided by AERIAL_GITHUB_TOKEN (not verified). To use a different account, unset it or run with a different environment.");
375
- return;
376
- }
377
- if (!force && source === "file") {
378
- console.log("GitHub login already exists (not verified). To sign in again, run aerial login --force.");
379
- return;
380
- }
381
- if (process.env.AERIAL_TEST_LOGIN_NO_NETWORK === "1") {
382
- console.log("AERIAL_TEST_LOGIN_NO_NETWORK=1 set; skipping GitHub device flow (test mode).");
383
- return;
384
- }
385
- const flow = await startDeviceFlow();
386
- console.log(`Open: ${flow.verification_uri}`);
387
- console.log(`Code: ${flow.user_code}`);
388
- console.log("Waiting for GitHub authorization...");
389
- await pollDeviceFlow(flow.device_code, flow.interval);
390
- console.log("GitHub login saved.");
391
- return;
392
- }
393
-
394
- if (command === "key") {
395
- if (subcommand === "generate") {
396
- const result = ensureApiKey();
397
- if (result.apiKey) {
398
- console.log("Local Aerial key generated and stored privately.");
399
- } else {
400
- console.log("Aerial API key already configured.");
401
- }
402
- return;
403
- }
404
- if (subcommand === "print") {
405
- const result = ensureApiKey();
406
- if (result.apiKey) console.log(result.apiKey);
407
- else throw new Error("Raw API key is not available. Run: aerial key generate");
408
- return;
409
- }
410
- }
411
-
412
- if (command === "start") {
413
- const config = loadConfig();
414
- const host = argValue(args, "--host") || config.host;
415
- const port = Number(argValue(args, "--port") || config.port);
416
- ensureApiKey();
417
- startServer({ host, port });
418
- return;
419
- }
420
-
421
- if (command === "setup") {
422
- if (subcommand === "codex") {
423
- const options = await selectSetupOptions("Codex", "responses", rest);
424
- const result = setupCodex({ model: options.model, effort: options.effort, authCommand: codexAuthCommand() });
425
- const config = loadConfig();
426
- printSetupCompletionSummary({
427
- heading: "Configured Codex",
428
- cli: "Codex",
429
- model: result.model,
430
- effort: result.effort || "missing",
431
- proxy: `http://${config.host}:${config.port}/v1`,
432
- configFile: result.file,
433
- aerialConfigFile: configPath(),
434
- aerialDefaultEffort: config.defaultEffort || "missing",
435
- backup: result.backup,
436
- auth: "command-backed local Aerial key",
437
- notes: ["restart Codex if it was already running so it reloads the profile."]
438
- });
439
- return;
440
- }
441
- if (subcommand === "claude") {
442
- const options = await selectSetupOptions("Claude Code", "messages", rest);
443
- const result = setupClaude({ model: options.model, effort: options.effort, apiKeyHelper: claudeApiKeyHelper() });
444
- const config = loadConfig();
445
- printSetupCompletionSummary({
446
- heading: "Configured Claude Code",
447
- cli: "Claude Code",
448
- model: result.model || "preserved",
449
- effort: result.effort || config.defaultEffort || "missing",
450
- proxy: `http://${config.host}:${config.port}`,
451
- configFile: result.file,
452
- aerialConfigFile: configPath(),
453
- aerialDefaultEffort: config.defaultEffort || "missing",
454
- backup: result.backup,
455
- auth: "apiKeyHelper local Aerial key",
456
- notes: ["effort is applied via Aerial defaultEffort and proxy fallback; Claude settings.json does not store an effort value."]
457
- });
458
- return;
459
- }
460
- if (subcommand === "all") {
461
- throw new Error("aerial setup all has been removed. Run `aerial setup codex` and/or `aerial setup claude` instead.");
462
- }
463
- if (subcommand === "status") {
464
- const status = setupStatus();
465
- if (rest.includes("--json")) {
466
- console.log(JSON.stringify(status, null, 2));
467
- return;
468
- }
469
- console.log(`Aerial: http://${status.config.host}:${status.config.port} (platform: ${status.platform})`);
470
- console.log(`API key file: ${status.auth.api_key.file} (${status.auth.api_key.exists ? "present" : "missing"})`);
471
- const ghSourceText = status.auth.github_token.source === "missing"
472
- ? `(missing)`
473
- : status.auth.github_token.source === "env"
474
- ? `(present, source=env; file path ${status.auth.github_token.file} is not consulted while AERIAL_GITHUB_TOKEN is set)`
475
- : `${status.auth.github_token.file} (present, source=file)`;
476
- console.log(`GitHub token: ${ghSourceText}`);
477
- for (const cs of Object.values(status.clients)) {
478
- const head = `${cs.target.padEnd(7)} state=${cs.state}`;
479
- const effortText = ` effort=${cs.effort || "missing"}`;
480
- console.log(`${head}${effortText} file=${cs.file}`);
481
- if (cs.backups.length) console.log(` backups=${cs.backups.length}`);
482
- if (cs.error) console.log(` error=${cs.error}`);
483
- }
484
- return;
485
- }
486
- if (subcommand === "restore") {
487
- const which = rest[0];
488
- if (!which) throw new Error("Usage: aerial setup restore <codex|claude|all> --latest");
489
- if (!rest.includes("--latest")) throw new Error("aerial setup restore: only --latest is supported in this release");
490
- if (which === "all") {
491
- const { ok, results } = restoreAllClients();
492
- printRestoreResults(results);
493
- process.exitCode = ok ? 0 : 1;
494
- return;
495
- }
496
- if (which !== "codex" && which !== "claude") throw new Error(`Unknown restore target: ${which}. Use codex, claude, or all.`);
497
- const r = restoreClient(which);
498
- if (r.restored) {
499
- console.log(`Restored ${which}: ${r.file} <- ${r.from}`);
500
- if (r.snapshot) console.log(` pre-restore snapshot: ${r.snapshot}`);
501
- } else if (r.reason === "no_backup") {
502
- console.log(`Restored ${which}: no backup to restore`);
503
- }
504
- return;
505
- }
506
- }
507
-
508
- if (command === "disable") {
509
- const { ok: restoreOk, results } = restoreAllClients();
510
- printRestoreResults(results);
511
- if (!restoreOk) {
512
- console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial disable` or `aerial service uninstall`.");
513
- process.exitCode = 1;
514
- return;
515
- }
516
- try {
517
- const r = serviceUninstall();
518
- printServiceUninstallResult(r, { prefix: "service uninstall" });
519
- process.exitCode = r.ok ? 0 : 1;
520
- } catch (err) {
521
- if (/unsupported platform/.test(err.message)) {
522
- console.log(`service uninstall: skipped (${err.message})`);
523
- process.exitCode = 0;
524
- } else {
525
- console.log(`service uninstall: FAILED (${err.message})`);
526
- console.log(` Retry with: aerial service uninstall`);
527
- process.exitCode = 1;
528
- }
529
- }
530
- return;
531
- }
532
-
533
- if (command === "service") {
534
- if (subcommand === "install") {
535
- await runServiceCliAction(serviceInstall, (r) => {
536
- if (r.ok && r.note) console.log(`Service install: ${r.note} (${r.platform}).`);
537
- else if (r.ok) console.log(`Service installed (${r.platform}).`);
538
- else console.log(`Service install: FAILED (${r.reason || "unknown"}): ${r.message || ""}`);
539
- if (r.file) console.log(` unit: ${r.file}`);
540
- if (r.taskName) console.log(` task: ${r.taskName}`);
541
- if (r.wrapper) console.log(` wrapper: ${r.wrapper}`);
542
- if (r.bootstrap?.stderr) console.log(` bootstrap stderr: ${r.bootstrap.stderr.trim()}`);
543
- if (r.create?.stderr) console.log(` schtasks stderr: ${r.create.stderr.trim()}`);
544
- if (r.run?.stderr) console.log(` schtasks /Run stderr: ${r.run.stderr.trim()}`);
545
- printServiceDiagnostics(r.diagnostics);
546
- printServiceWarning(r);
547
- });
548
- return;
549
- }
550
- if (subcommand === "start") {
551
- await runServiceCliAction(serviceStart, (r) => {
552
- if (r.ok && r.note) console.log(`Service start: ${r.note} (${r.platform})`);
553
- else if (r.ok) console.log(`Service start: ok (${r.platform})`);
554
- else console.log(`Service start: FAILED (${r.reason || `status=${r.status}`})${r.message ? ": " + r.message : ""}`);
555
- if (r.stderr) console.log(` ${r.stderr.trim()}`);
556
- printServiceDiagnostics(r.diagnostics);
557
- printServiceWarning(r);
558
- });
559
- return;
560
- }
561
- if (subcommand === "stop") {
562
- await runServiceCliAction(serviceStop, (r) => {
563
- if (r.note) console.log(`Service stop: ${r.note} (${r.platform})`);
564
- else if (r.ok) console.log(`Service stop: ok (${r.platform})`);
565
- else console.log(`Service stop: FAILED (status=${r.status})`);
566
- if (r.stderr) console.log(` ${r.stderr.trim()}`);
567
- });
568
- return;
569
- }
570
- if (subcommand === "restart") {
571
- await runServiceCliAction(serviceRestart, (r) => {
572
- if (!r.ok && r.reason === "stop_failed") console.log(`Service restart: FAILED on stop; start not attempted`);
573
- else if (r.ok) console.log(`Service restart: ok`);
574
- else console.log(`Service restart: FAILED`);
575
- if (r.stop?.stderr) console.log(` stop: ${r.stop.stderr.trim()}`);
576
- if (r.start?.stderr) console.log(` start: ${r.start.stderr.trim()}`);
577
- printServiceWarning(r);
578
- });
579
- return;
580
- }
581
- if (subcommand === "uninstall") {
582
- await runServiceCliAction(serviceUninstall, printServiceUninstallResult);
583
- return;
584
- }
585
- if (subcommand === "status") {
586
- const status = await serviceStatus();
587
- if (rest.includes("--json")) {
588
- console.log(JSON.stringify(status, null, 2));
589
- process.exitCode = status.supported === false ? 1 : 0;
590
- return;
591
- }
592
- console.log(`Aerial: http://${status.config.host}:${status.config.port} (platform: ${status.platform})`);
593
- if (status.supported === false) {
594
- console.log(`service: unsupported on ${status.platform}`);
595
- console.log(`summary: ${status.summary}`);
596
- process.exitCode = 1;
597
- return;
598
- }
599
- const svc = status.service;
600
- const stateLine = svc.loaded ? "running" : (svc.installed ? "installed (not running)" : "not installed");
601
- console.log(`service: ${stateLine}`);
602
- if (svc.pid) console.log(` pid: ${svc.pid}`);
603
- if (svc.status) console.log(` win status: ${svc.status}`);
604
- if (svc.lastExitStatus !== undefined) console.log(` last exit: ${svc.lastExitStatus}`);
605
- const h = status.health;
606
- let healthLine;
607
- if (h.aerial && h.supervisor === "service-managed") healthLine = "ok (Aerial, service-managed)";
608
- else if (h.aerial && h.supervisor === "foreground") healthLine = "ok (Aerial, foreground)";
609
- else if (h.portConflict) healthLine = `port conflict (${h.conflictReason})`;
610
- else if (h.ok) healthLine = "ok";
611
- else healthLine = `unreachable (${h.error || `http ${h.status}`})`;
612
- console.log(`health: ${healthLine}`);
613
- console.log(`summary: ${status.summary}`);
614
- console.log(`logs: ${status.logs.dir}`);
615
- console.log(` primary: ${status.logs.primary.exists ? `${status.logs.primary.size} bytes` : "missing"} ${status.logs.primary.file}`);
616
- console.log(` stdio: ${status.logs.stdio.exists ? `${status.logs.stdio.size} bytes` : "missing"} ${status.logs.stdio.file}`);
617
- console.log(`auth: api_key=${status.auth.api_key.state} github_token=${status.auth.github_token.state}`);
618
- return;
619
- }
620
- console.error(`Unknown service subcommand: ${subcommand}. Use install, start, stop, restart, status, or uninstall.`);
621
- process.exitCode = 1;
622
- return;
623
- }
624
-
625
- if (command === "doctor") {
626
- const report = await doctor();
627
- if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
628
- else console.log(renderDoctorText(report));
629
- process.exitCode = report.ok ? 0 : 1;
630
- return;
631
- }
632
-
633
- if (command === "probe") {
634
- const report = await runProbe({ live: args.includes("--live") });
635
- if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
636
- else console.log(formatProbeReport(report));
637
- process.exitCode = report.ok ? 0 : 1;
638
- return;
639
- }
640
-
641
- if (command === "config") {
642
- if (subcommand === "reset") {
643
- saveConfig(defaultConfig());
644
- console.log(`Reset Aerial config: ${configPath()}`);
645
- return;
646
- }
647
- const config = loadConfig();
648
- if (subcommand === "set") {
649
- const [key, value] = rest;
650
- if (!key || value === undefined) throw new Error("Usage: aerial config set <key> <value>");
651
- if (!["host", "port", "defaultModel", "defaultEffort", "logLevel", "promptCacheRetention", "promptCacheKey"].includes(key)) throw new Error(`Unsupported config key: ${key}`);
652
- if (key === "promptCacheRetention" && !["in_memory", "24h", "off"].includes(value)) throw new Error("promptCacheRetention must be one of: in_memory, 24h, off");
653
- if (key === "promptCacheKey" && !value.trim()) throw new Error("promptCacheKey must be auto, off, or a non-empty string");
654
- if (key === "host") {
655
- config.host = parseConfigHost(value);
656
- saveConfig(config);
657
- return;
658
- }
659
- if (key === "port") {
660
- config.port = parseConfigPort(value);
661
- saveConfig(config);
662
- return;
663
- }
664
- if (key === "defaultEffort") {
665
- config.defaultEffort = assertValidEffort(value);
666
- saveConfig(config);
667
- return;
668
- }
669
- config[key] = value;
670
- saveConfig(config);
671
- return;
672
- }
673
- console.log(JSON.stringify(config, null, 2));
674
- return;
675
- }
676
-
677
- printHelp();
678
- process.exitCode = 1;
679
- }
680
-
681
- main().catch((error) => {
682
- console.error(error.message);
683
- process.exitCode = 1;
684
- });