@jiayunxie/aerial 0.2.5 → 0.2.6

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 CHANGED
@@ -122,7 +122,7 @@ aerial status # setup, login, service, and health summary
122
122
  aerial proxy status # upstream proxy mode, egress, and route visibility
123
123
  aerial service install # install and start the background service
124
124
  aerial doctor # local diagnostics
125
- aerial disable # restore client configs and uninstall the service
125
+ aerial teardown # restore client configs and uninstall the service
126
126
  ```
127
127
 
128
128
  `aerial start` is for foreground debugging in the current terminal. Most users should use `aerial service install`.
@@ -136,7 +136,7 @@ Advanced service lifecycle commands are documented in `docs/usage.md`.
136
136
  - No models listed during setup: run `aerial login` first, then retry setup.
137
137
  - Claude models or the `messages` route are missing: run `aerial proxy enable`, then `aerial probe`.
138
138
  - Port conflict on `18181`: run `aerial status` to see whether another process is using the port.
139
- - Need to undo setup: run `aerial disable`, or restore one client with `aerial setup restore <codex|claude> --latest`.
139
+ - Need to undo setup: run `aerial teardown`, or restore one client with `aerial setup restore <codex|claude> --latest`.
140
140
 
141
141
  ## Notes
142
142
 
package/docs/usage.md CHANGED
@@ -91,7 +91,7 @@ The local key is generated and stored by Aerial automatically. Users do not need
91
91
 
92
92
  For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
93
93
 
94
- To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <low|medium|high|xhigh|max>` (`max` is an alias for `xhigh`). The chosen effort is written into the `[profiles.aerial]` block as `model_reasoning_effort = "<effort>"` and is also persisted as Aerial-wide `defaultEffort` in `~/.aerial/config.json`. Under non-TTY (CI/pipes) the wizard does not prompt and falls back to the default effort `medium`.
94
+ To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <low|medium|high|xhigh|max>` (`max` is an alias for `xhigh`). The chosen effort is written as root-level `model_reasoning_effort = "<effort>"` and is also persisted as Aerial-wide `defaultEffort` in `~/.aerial/config.json`. Under non-TTY (CI/pipes) the wizard does not prompt and falls back to the default effort `medium`.
95
95
 
96
96
  ## 6. Configure Claude Code
97
97
 
@@ -171,7 +171,7 @@ Use `responses` models for Codex, `messages` models for Claude Code, and `chat`
171
171
  ```bash
172
172
  aerial setup status [--json]
173
173
  aerial setup restore <codex|claude|all> --latest
174
- aerial disable
174
+ aerial teardown
175
175
  ```
176
176
 
177
177
  `aerial setup status` reports whether each supported client is currently configured to route through Aerial, plus the local API key and GitHub token files. The state for each client is one of:
@@ -221,7 +221,7 @@ Stability rules for `aerial.setup-status.v1`: new fields may be added at any lev
221
221
 
222
222
  `aerial setup restore <codex|claude|all> --latest` restores the most recent `*.aerial-backup-<ISO>` snapshot for the named client. Before overwriting, it takes a `*.aerial-pre-restore-<ISO>` snapshot of the current file so the restore itself is reversible. With `all`, both clients are restored best-effort and the command exits non-zero if any individual restore failed. If there is no backup to restore, the command prints a note and exits 0.
223
223
 
224
- `aerial disable` first runs `setup restore all --latest` and then, only if every client restore succeeded, uninstalls the local Aerial service via `aerial service uninstall`. If any client restore reports a failure, the service is left running and `aerial disable` exits non-zero; resolve the restore errors, then rerun `aerial disable` (or call `aerial service uninstall` directly once the client config is in the state you want).
224
+ `aerial teardown` first runs `setup restore all --latest` and then, only if every client restore succeeded, uninstalls the local Aerial service via `aerial service uninstall`. If any client restore reports a failure, the service is left running and `aerial teardown` exits non-zero; resolve the restore errors, then rerun `aerial teardown` (or call `aerial service uninstall` directly once the client config is in the state you want).
225
225
 
226
226
  ## 7.6 Run Aerial As A Local Service
227
227
 
@@ -252,7 +252,7 @@ Wrapper env values are baked in at install time. If you set `AERIAL_LOG_MAX_BYTE
252
252
  4. If the port answers as Aerial AND the local service manager does NOT report the unit/task loaded → the definition is regenerated, but the service is NOT started. Exit 1 with `reason=foreground_running`, `definitionUpdated=true`, and an actionable next step: stop the foreground process, then run `aerial service start`. This guarantees `install` never causes two Aerial instances to fight for the port.
253
253
  5. Otherwise (port absent) → write the definition, then start the service (`launchctl bootstrap` on macOS, `schtasks /Run` on Windows). Exit 0.
254
254
 
255
- `aerial service start` enforces a similar shape: it refuses with `reason=not_installed` and exit 1 if the unit/task does not exist; it refuses with `reason=port_conflict` and exit 1 if a non-Aerial process owns the port; it refuses with `reason=foreground_running` and exit 1 if Aerial is already running in the foreground (not via the service manager); it returns idempotent success with `note=already running (service-managed)` if the service is already up; otherwise it starts the service. `aerial service stop` is idempotent (exit 0 + `note` when nothing is installed or nothing is running). `aerial service uninstall` is idempotent on the "no service installed" branch, but does NOT swallow real teardown failures: on macOS, if the service is loaded and `launchctl bootout` returns non-zero, the plist and wrapper are preserved and the command exits 1 with `reason=bootout_failed`; on Windows, if `schtasks /Delete` returns non-zero, the wrapper is preserved and the command exits 1 with `reason=delete_failed`. In both cases the message includes a retry pointer. `aerial service restart` blocks the start step when the stop step fails: if `stop` returns `ok=false`, the response includes `reason=stop_failed` and `start` is not attempted. `aerial disable` follows the same contract: it restores client configs first, then calls `serviceUninstall`. Only an `unsupported platform` exception is treated as a silent skip (Linux); any supported-platform uninstall failure (`ok=false` or non-`unsupported-platform` throw) propagates as exit 1 with a retry pointer at `aerial service uninstall`.
255
+ `aerial service start` enforces a similar shape: it refuses with `reason=not_installed` and exit 1 if the unit/task does not exist; it refuses with `reason=port_conflict` and exit 1 if a non-Aerial process owns the port; it refuses with `reason=foreground_running` and exit 1 if Aerial is already running in the foreground (not via the service manager); it returns idempotent success with `note=already running (service-managed)` if the service is already up; otherwise it starts the service. `aerial service stop` is idempotent (exit 0 + `note` when nothing is installed or nothing is running). `aerial service uninstall` is idempotent on the "no service installed" branch, but does NOT swallow real teardown failures: on macOS, if the service is loaded and `launchctl bootout` returns non-zero, the plist and wrapper are preserved and the command exits 1 with `reason=bootout_failed`; on Windows, if `schtasks /Delete` returns non-zero, the wrapper is preserved and the command exits 1 with `reason=delete_failed`. In both cases the message includes a retry pointer. `aerial service restart` blocks the start step when the stop step fails: if `stop` returns `ok=false`, the response includes `reason=stop_failed` and `start` is not attempted. `aerial teardown` follows the same contract: it restores client configs first, then calls `serviceUninstall`. Only an `unsupported platform` exception is treated as a silent skip (Linux); any supported-platform uninstall failure (`ok=false` or non-`unsupported-platform` throw) propagates as exit 1 with a retry pointer at `aerial service uninstall`.
256
256
 
257
257
  If GitHub login is not yet configured, install and start still succeed (when reachable) and emit a structured warning pointing at `aerial login`; inference proxy requests return 503 until you log in, while `GET /v1/models` returns 401 with `error.aerial.status = "login_required"`. After rotating the local API key or moving the config directory, restart the service with `aerial service restart` so the new credentials are picked up.
258
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Local GitHub Copilot proxy for Codex CLI and Claude Code with smart HTTP(S)/SOCKS5 upstream proxy support.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,7 +1,7 @@
1
1
  import { defaultConfig, loadConfig, saveConfig } from "../../shared/config.js";
2
2
  import { configPath } from "../../shared/paths.js";
3
3
  import { assertValidEffort } from "../../shared/effort.js";
4
- import { parseConfigHost, parseConfigPort } from "../args.js";
4
+ import { parseConfigHost, parseConfigPort } from "../helpers.js";
5
5
 
6
6
  export function runConfigCli(subcommand, rest) {
7
7
  if (subcommand === "reset") {
@@ -1,10 +1,8 @@
1
1
  import { loadConfig } from "../../shared/config.js";
2
2
  import { configPath } from "../../shared/paths.js";
3
3
  import { restoreAllClients, restoreClient, setupClaude, setupCodex, setupStatus } from "../../setup/index.js";
4
- import { chooseSetupModel, formatModelChoices } from "../model-selection.js";
5
- import { assertValidEffort, chooseSetupEffort, formatEffortSelection } from "../setup-selection.js";
6
- import { requiredArgValue } from "../args.js";
7
- import { claudeApiKeyHelper, codexAuthCommand } from "../runtime-auth.js";
4
+ import { chooseSetupModel, formatModelChoices, assertValidEffort, chooseSetupEffort, formatEffortSelection } from "../select.js";
5
+ import { requiredArgValue, claudeApiKeyHelper, codexAuthCommand } from "../helpers.js";
8
6
  import { printRestoreResults, printSetupCompletionSummary } from "../output.js";
9
7
 
10
8
  async function selectSetupOptions(target, route, args) {
@@ -45,6 +43,7 @@ function printSetupStatus(status) {
45
43
  console.log(`${head}${effortText} file=${cs.file}`);
46
44
  if (cs.backups.length) console.log(` backups=${cs.backups.length}`);
47
45
  if (cs.error) console.log(` error=${cs.error}`);
46
+ if (cs.migration) console.log(` migration=${cs.migration}`);
48
47
  }
49
48
  }
50
49
 
@@ -1,6 +1,6 @@
1
1
  import { ensureApiKey, loadConfig } from "../../shared/config.js";
2
2
  import { startServer } from "../../proxy/server.js";
3
- import { argValue } from "../args.js";
3
+ import { argValue } from "../helpers.js";
4
4
 
5
5
  export function runStartCli(args) {
6
6
  const config = loadConfig();
@@ -1,6 +1,6 @@
1
1
  import { setupStatus } from "../../setup/index.js";
2
2
  import { serviceStatus } from "../../service/index.js";
3
- import { computeAppStatus } from "../app-status.js";
3
+ import { computeAppStatus } from "../helpers.js";
4
4
  import { printServiceSummary, printSetupSummary } from "../output.js";
5
5
 
6
6
  export async function appStatus({ json = false } = {}) {
@@ -3,11 +3,11 @@ import { serviceUninstall } from "../../service/index.js";
3
3
  import { printRestoreResults } from "../output.js";
4
4
  import { printServiceUninstallResult } from "./service.js";
5
5
 
6
- export function runDisableCli() {
6
+ export function runTeardownCli() {
7
7
  const { ok: restoreOk, results } = restoreAllClients();
8
8
  printRestoreResults(results);
9
9
  if (!restoreOk) {
10
- console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial disable` or `aerial service uninstall`.");
10
+ console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial teardown` or `aerial service uninstall`.");
11
11
  process.exitCode = 1;
12
12
  return;
13
13
  }
package/src/cli/doctor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { setupStatus } from "../setup/index.js";
2
2
  import { serviceStatus } from "../service/index.js";
3
- import { computeAppStatus } from "./app-status.js";
3
+ import { computeAppStatus } from "./helpers.js";
4
4
  import { readConfigFileStatus } from "../shared/config.js";
5
5
 
6
6
  const REPAIRS = Object.freeze({
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const PACKAGE_JSON_URL = new URL("../../package.json", import.meta.url);
6
+ const CLI_ENTRY = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
7
+
8
+ export function readPackageVersion(packageJsonUrl = PACKAGE_JSON_URL) {
9
+ try {
10
+ const pkg = JSON.parse(fs.readFileSync(packageJsonUrl, "utf8"));
11
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
12
+ throw new Error("missing version field");
13
+ } catch (error) {
14
+ const reason = error?.message || String(error);
15
+ console.warn(`aerial: cannot read package version (${reason}); reporting "unknown"`);
16
+ return "unknown";
17
+ }
18
+ }
19
+
20
+ export function printVersion(packageJsonUrl = PACKAGE_JSON_URL) {
21
+ console.log(readPackageVersion(packageJsonUrl));
22
+ }
23
+
24
+ export function argValue(args, name) {
25
+ const index = args.indexOf(name);
26
+ return index >= 0 ? args[index + 1] : undefined;
27
+ }
28
+
29
+ export function requiredArgValue(args, name) {
30
+ const index = args.indexOf(name);
31
+ if (index < 0) return undefined;
32
+ const value = args[index + 1];
33
+ if (value === undefined || value.startsWith("--")) {
34
+ throw new Error(`${name} requires a value.`);
35
+ }
36
+ return value;
37
+ }
38
+
39
+ export function parseConfigPort(value) {
40
+ const text = String(value).trim();
41
+ if (!/^\d+$/.test(text)) throw new Error("port must be an integer between 1 and 65535");
42
+ const port = Number(text);
43
+ if (port < 1 || port > 65535) throw new Error("port must be an integer between 1 and 65535");
44
+ return port;
45
+ }
46
+
47
+ export function parseConfigHost(value) {
48
+ const host = String(value).trim().toLowerCase();
49
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") return host;
50
+ throw new Error("host must be a loopback address: 127.0.0.1, localhost, or ::1");
51
+ }
52
+
53
+ export function codexAuthCommand() {
54
+ return {
55
+ command: process.execPath,
56
+ args: [CLI_ENTRY, "key", "print"],
57
+ timeout_ms: 5000,
58
+ refresh_interval_ms: 0
59
+ };
60
+ }
61
+
62
+ function quoteCommandPart(value) {
63
+ return `"${String(value).replace(/"/g, '\\"')}"`;
64
+ }
65
+
66
+ export function claudeApiKeyHelper() {
67
+ return [process.execPath, CLI_ENTRY, "key", "print"].map(quoteCommandPart).join(" ");
68
+ }
69
+
70
+ export function computeAppStatus(setup, service) {
71
+ const githubTokenPresent = setup.auth.github_token.source !== "missing";
72
+ const apiKeyPresent = setup.auth.api_key.exists;
73
+ const hasAerialClient = setup.clients.codex.state === "aerial" || setup.clients.claude.state === "aerial";
74
+ const serviceHealthy = service.supported !== false && service.health?.aerial === true;
75
+ const ok = apiKeyPresent && githubTokenPresent && hasAerialClient && serviceHealthy;
76
+ const nextSteps = [];
77
+ const hints = [];
78
+ if (!githubTokenPresent) nextSteps.push("run: aerial login");
79
+ if (!hasAerialClient) nextSteps.push("run: aerial setup codex or aerial setup claude");
80
+ if (hasAerialClient && !apiKeyPresent) nextSteps.push("run: aerial setup codex or aerial setup claude to recreate the local Aerial key");
81
+ if (service.supported !== false && !service.service?.loaded) {
82
+ if (serviceHealthy) {
83
+ hints.push("Aerial is running in the foreground but no background service is installed; run `aerial service install` so it starts on reboot.");
84
+ } else {
85
+ nextSteps.push("run: aerial service install");
86
+ }
87
+ }
88
+ if (setup.auth.github_token.source === "env") {
89
+ hints.push("AERIAL_GITHUB_TOKEN is set for this process only; run aerial login without that env var to persist a service-readable login.");
90
+ }
91
+ return { schema: "aerial.status.v1", ok, setup, service, nextSteps, hints };
92
+ }
package/src/cli/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { printVersion } from "./version.js";
3
- import { printHelp } from "./help.js";
2
+ import { printVersion } from "./helpers.js";
4
3
  import { appStatus } from "./commands/status.js";
5
4
  import { runConfigCli } from "./commands/config.js";
6
- import { runDisableCli } from "./commands/disable.js";
5
+ import { runTeardownCli } from "./commands/teardown.js";
7
6
  import { runKeyCli } from "./commands/key.js";
8
7
  import { runLoginCli } from "./commands/login.js";
9
8
  import { runProxyCli } from "./commands/proxy.js";
@@ -13,6 +12,30 @@ import { runStartCli } from "./commands/start.js";
13
12
  import { doctor, renderDoctorText } from "./doctor.js";
14
13
  import { formatProbeReport, runProbe } from "./probe.js";
15
14
 
15
+ function printHelp() {
16
+ console.log(`Aerial local Copilot proxy
17
+
18
+ Usage:
19
+ aerial --version
20
+ aerial login
21
+ aerial setup codex [--model <id>] [--effort <low|medium|high|xhigh|max>]
22
+ aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
23
+ aerial service install
24
+ aerial status [--json]
25
+ aerial proxy status|enable|disable [--json]
26
+
27
+ Diagnostics and rollback:
28
+ aerial setup status [--json]
29
+ aerial setup restore <codex|claude|all> --latest
30
+ aerial service status [--json]
31
+ aerial teardown
32
+ aerial doctor
33
+ aerial probe [--live] [--json]
34
+
35
+ Debug:
36
+ aerial start [--host 127.0.0.1] [--port 18181]`);
37
+ }
38
+
16
39
  async function main() {
17
40
  const args = process.argv.slice(2);
18
41
  const [command, subcommand, ...rest] = args;
@@ -51,8 +74,8 @@ async function main() {
51
74
  }
52
75
  }
53
76
 
54
- if (command === "disable") {
55
- runDisableCli();
77
+ if (command === "teardown") {
78
+ runTeardownCli();
56
79
  return;
57
80
  }
58
81
 
@@ -86,7 +109,16 @@ async function main() {
86
109
  process.exitCode = 1;
87
110
  }
88
111
 
89
- main().catch((error) => {
90
- console.error(error.message);
91
- process.exitCode = 1;
92
- });
112
+ main()
113
+ .then(() => {
114
+ // One-shot commands may leave keep-alive sockets (undici pools), a SOCKS5
115
+ // bridge, or stdin open, which keeps the event loop alive and hangs the CLI
116
+ // after the work is done. `start` is the only long-running command, so for
117
+ // everything else exit explicitly once main() resolves.
118
+ const command = process.argv[2];
119
+ if (command !== "start") process.exit(process.exitCode ?? 0);
120
+ })
121
+ .catch((error) => {
122
+ console.error(error.message);
123
+ process.exit(1);
124
+ });
package/src/cli/output.js CHANGED
@@ -18,6 +18,7 @@ export function printSetupSummary(status) {
18
18
  const model = client.model ? ` model=${client.model}` : "";
19
19
  const effort = ` effort=${client.effort || "missing"}`;
20
20
  console.log(` ${client.target}: ${client.state}${model}${effort}`);
21
+ if (client.migration) console.log(` migration: ${client.migration}`);
21
22
  }
22
23
  console.log(`api key: ${status.auth.api_key.exists ? "present" : "missing"}`);
23
24
  const ghSource = status.auth.github_token.source;
package/src/cli/probe.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "../proxy/index.js";
2
- import { readJsonSafely } from "../shared/http-utils.js";
3
- import { aerialRoutes, usageSummary } from "../proxy/model-utils.js";
2
+ import { readJsonSafely } from "../shared/utils.js";
3
+ import { aerialRoutes, usageSummary } from "../proxy/models.js";
4
4
 
5
5
  function modelRoutes(model) {
6
6
  return aerialRoutes(model);