@jiayunxie/aerial 0.1.11 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/docs/usage.md +24 -1
- package/package.json +9 -4
- package/src/auth.js +4 -3
- package/src/cli.js +199 -98
- package/src/config.js +22 -1
- package/src/copilot.js +18 -12
- package/src/http-utils.js +9 -0
- package/src/model-selection.js +7 -32
- package/src/model-utils.js +20 -0
- package/src/probe.js +7 -24
- package/src/prompt-utils.js +8 -0
- package/src/proxy-config.js +51 -0
- package/src/responses-websocket.js +4 -2
- package/src/service.js +305 -214
- package/src/setup-selection.js +2 -9
- package/src/setup.js +41 -22
- package/src/socks5-bridge.js +189 -0
- package/src/upstream-fetch.js +354 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ It runs locally on `127.0.0.1:18181`. Your GitHub token and Aerial key stay on y
|
|
|
11
11
|
|
|
12
12
|
Requirements:
|
|
13
13
|
|
|
14
|
-
- Node.js
|
|
14
|
+
- Node.js 24+
|
|
15
15
|
- A GitHub account with an active Copilot subscription
|
|
16
16
|
- Codex CLI and/or Claude Code installed
|
|
17
17
|
|
|
@@ -21,6 +21,14 @@ npm install -g @jiayunxie/aerial
|
|
|
21
21
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
|
+
If you are on a restricted network or Claude models are missing from `aerial probe`, enable Aerial's upstream proxy mode first:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
aerial proxy enable
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`proxy enable` discovers local HTTP(S) and SOCKS5 proxies from macOS network settings, PAC files, environment hints, and common local proxy ports, then validates the route before saving anything.
|
|
31
|
+
|
|
24
32
|
1. Sign in to GitHub:
|
|
25
33
|
|
|
26
34
|
```bash
|
|
@@ -93,10 +101,25 @@ To inspect the full model matrix:
|
|
|
93
101
|
aerial probe
|
|
94
102
|
```
|
|
95
103
|
|
|
104
|
+
## Upstream Proxy
|
|
105
|
+
|
|
106
|
+
Aerial can route its own GitHub and Copilot upstream traffic through a local HTTP(S) or SOCKS5 proxy while keeping the Codex/Claude client-facing server on `127.0.0.1:18181`.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
aerial proxy status # show direct/proxy egress IP, region, and Copilot route counts
|
|
110
|
+
aerial proxy enable # auto-discover and validate a working local proxy
|
|
111
|
+
aerial proxy disable # return upstream traffic to direct mode
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This is useful when Copilot exposes different model routes by region. In direct mode, `proxy status` reports the direct egress. In enabled mode, it reports the selected local proxy endpoint, proxied egress, and whether the `messages` route is visible for Claude Code. SOCKS5 endpoints, including Shadowsocks PAC entries, are bridged internally; users do not need to find or configure an HTTP proxy port by hand.
|
|
115
|
+
|
|
116
|
+
Advanced override: `AERIAL_UPSTREAM_PROXY` can be set to a bare `http://`, `https://`, `socks://`, `socks5://`, or `socks5h://` proxy endpoint. Path-prefixed proxy URLs are intentionally ignored.
|
|
117
|
+
|
|
96
118
|
## Daily Commands
|
|
97
119
|
|
|
98
120
|
```bash
|
|
99
121
|
aerial status # setup, login, service, and health summary
|
|
122
|
+
aerial proxy status # upstream proxy mode, egress, and route visibility
|
|
100
123
|
aerial service install # install and start the background service
|
|
101
124
|
aerial doctor # local diagnostics
|
|
102
125
|
aerial disable # restore client configs and uninstall the service
|
|
@@ -111,6 +134,7 @@ Advanced service lifecycle commands are documented in `docs/usage.md`.
|
|
|
111
134
|
- `Missing GitHub token`: run `aerial login`.
|
|
112
135
|
- `Invalid or missing Aerial API key`: rerun `aerial setup codex` or `aerial setup claude`, then restart the client.
|
|
113
136
|
- No models listed during setup: run `aerial login` first, then retry setup.
|
|
137
|
+
- Claude models or the `messages` route are missing: run `aerial proxy enable`, then `aerial probe`.
|
|
114
138
|
- Port conflict on `18181`: run `aerial status` to see whether another process is using the port.
|
|
115
139
|
- Need to undo setup: run `aerial disable`, or restore one client with `aerial setup restore <codex|claude> --latest`.
|
|
116
140
|
|
package/docs/usage.md
CHANGED
|
@@ -39,6 +39,26 @@ aerial login
|
|
|
39
39
|
|
|
40
40
|
Open the printed URL, enter the user code, and authorize the GitHub OAuth device flow. Aerial saves the GitHub access token locally and exchanges it for short-lived Copilot JWTs when proxy requests arrive.
|
|
41
41
|
|
|
42
|
+
## 3.5 Upstream Proxy Mode
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
aerial proxy status
|
|
46
|
+
aerial proxy enable
|
|
47
|
+
aerial proxy disable
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Aerial's local server always listens on `127.0.0.1:18181`, but its own GitHub and Copilot upstream traffic can optionally go through a local HTTP(S) or SOCKS5 proxy. This helps in restricted regions where direct Copilot egress may return a smaller model catalog or omit Claude-compatible `/v1/messages` routes.
|
|
51
|
+
|
|
52
|
+
`aerial proxy enable` auto-discovers candidates from `AERIAL_UPSTREAM_PROXY`, standard proxy environment variables, macOS `scutil --proxy`, PAC files, common local SOCKS5 ports such as `1086` and `1080`, and common local HTTP proxy ports such as `1087` and `7890`. It validates that a candidate can reach GitHub and return GitHub's `/rate_limit` JSON before saving `upstreamProxyMode=auto` and the selected endpoint in Aerial's config. If no candidate works, it exits non-zero and leaves the previous config unchanged. PAC files are read with a 1 MiB limit.
|
|
53
|
+
|
|
54
|
+
SOCKS5 endpoints are first-class candidates. When Aerial selects `socks5://127.0.0.1:1086` from a Shadowsocks PAC file, the config stores that original endpoint; at runtime Aerial starts a loopback-only HTTP CONNECT bridge internally so GitHub/Copilot `fetch` and WebSocket traffic can use the same proxy dispatcher path as HTTP(S) proxies. Users do not need to discover or configure a separate HTTP bridge port.
|
|
55
|
+
|
|
56
|
+
For explicit overrides, `AERIAL_UPSTREAM_PROXY` accepts bare `http://`, `https://`, `socks://`, `socks5://`, and `socks5h://` endpoints. Aerial intentionally ignores proxy URLs with path, query, or hash components, because upstream proxy dispatchers expect a host-level proxy endpoint.
|
|
57
|
+
|
|
58
|
+
`aerial proxy status` prints the active mode, the direct or proxied egress IP/region, and the current Copilot route counts from `aerial probe` (`responses`, `responsesWebSocket`, `messages`, and `chat`). In disabled mode the endpoint is `direct`; in enabled mode the endpoint is the selected local proxy plus its discovery source. Use `--json` for the stable `aerial.proxy-status.v1` shape. If a proxy URL contains userinfo, status and enable diagnostics redact it before printing.
|
|
59
|
+
|
|
60
|
+
`aerial proxy disable` switches back to direct upstream traffic and clears the saved proxy endpoint. Running Aerial processes read this config for each upstream request, so no service restart is required for proxy enable/disable.
|
|
61
|
+
|
|
42
62
|
## 4. Start Server
|
|
43
63
|
|
|
44
64
|
```bash
|
|
@@ -99,12 +119,15 @@ Non-Opus-4.7 Claude requests are not modified.
|
|
|
99
119
|
|
|
100
120
|
```bash
|
|
101
121
|
aerial status
|
|
122
|
+
aerial proxy status
|
|
102
123
|
aerial doctor
|
|
103
124
|
aerial probe
|
|
104
125
|
```
|
|
105
126
|
|
|
106
127
|
`aerial status` is the top-level daily check. It combines setup state, local auth files, service state, and health into one short report. Use `--json` for a machine-readable `aerial.status.v1` document.
|
|
107
128
|
|
|
129
|
+
`aerial proxy status` is the network-path check. Use it when Claude models disappear, when `/v1/messages` is `0`, or when you want to confirm whether direct or proxied egress is being used.
|
|
130
|
+
|
|
108
131
|
`aerial status --json` top-level fields: `schema`, `ok`, `setup`, `service`, `nextSteps`, `hints`. `nextSteps` lists actions you must take to reach `ok: true` (login / setup / service install / recover local key). `hints` lists non-blocking advisories that you should still pay attention to, such as `AERIAL_GITHUB_TOKEN` being set for the current shell only (the background service may not see it after reboot). Example:
|
|
109
132
|
|
|
110
133
|
```json
|
|
@@ -217,7 +240,7 @@ Aerial ships a thin platform wrapper around the user-mode service primitives pro
|
|
|
217
240
|
- Windows: a Task Scheduler task named `AerialLocalProxy`, `/SC ONLOGON /RL LIMITED`, executes a PowerShell wrapper at `<config-dir>\bin\aerial-service.ps1` (default `%APPDATA%\aerial\bin\aerial-service.ps1`). The wrapper is regenerated on every `aerial service install`. The `/TR` argument is wrapped in escaped quotes so paths that contain spaces or non-ASCII characters work without manual quoting.
|
|
218
241
|
- Linux: not implemented in this release. `aerial service install|start|stop|restart|uninstall` throws an unsupported-platform error and exits 1; `aerial service status --json` still emits a schema-valid document with `"supported": false` and exits 1. Run `aerial start` directly or wrap it in your own init system.
|
|
219
242
|
|
|
220
|
-
Both wrappers do the same three things before exec-ing the proxy: (1) startup-rotate the captured stdio log (`aerial-stdio.log` → `.1` → `.2` → `.3`) if it has grown beyond the configured cap; (2) export `AERIAL_LOG_FILE=<config-dir>/logs/aerial.log`, plus `AERIAL_LOG_MAX_BYTES` and `AERIAL_LOG_BACKUPS` (default `5242880` / `3`, or whatever value was present in the installer's environment — see below), and — when `AERIAL_CONFIG_DIR` was set at install time — re-export `AERIAL_CONFIG_DIR` so the service sees the same config root as the installer; (3) `exec` `node src/cli.js start --host <host> --port <port>` with stdout and stderr appended to `aerial-stdio.log`. The structured event log is opt-in via `AERIAL_LOG_FILE`: when this env var is set (always set by the wrapper, never by foreground `aerial start`), structured events go to that file only; when unset, they go to stderr only. There is no double-write.
|
|
243
|
+
Both wrappers do the same three things before exec-ing the proxy: (1) startup-rotate the captured stdio log (`aerial-stdio.log` → `.1` → `.2` → `.3`) if it has grown beyond the configured cap; (2) export `AERIAL_LOG_FILE=<config-dir>/logs/aerial.log`, plus `AERIAL_LOG_MAX_BYTES` and `AERIAL_LOG_BACKUPS` (default `5242880` / `3`, or whatever value was present in the installer's environment — see below), and — when `AERIAL_CONFIG_DIR` was set at install time — re-export `AERIAL_CONFIG_DIR` so the service sees the same config root as the installer; (3) `exec` `node src/cli.js start --host <host> --port <port>` with stdout and stderr appended to `aerial-stdio.log`. The service wrapper prefers a Node.js 24+ binary; on macOS, if the installer is running under an older Node and Codex's bundled Node 24+ exists at `/Applications/Codex.app/Contents/Resources/node`, Aerial uses that for the generated wrapper. Set `AERIAL_SERVICE_NODE` before `aerial service install` to override this selection explicitly. The structured event log is opt-in via `AERIAL_LOG_FILE`: when this env var is set (always set by the wrapper, never by foreground `aerial start`), structured events go to that file only; when unset, they go to stderr only. There is no double-write.
|
|
221
244
|
|
|
222
245
|
Wrapper env values are baked in at install time. If you set `AERIAL_LOG_MAX_BYTES` and/or `AERIAL_LOG_BACKUPS` in the shell before running `aerial service install`, the generated wrapper hard-codes those values and the next start of the service inherits them. Changing the env in another shell after install has no effect on the already-installed wrapper. To apply new values to an already-managed service, rerun `aerial service install` (which always regenerates the definition, including the wrapper) and then `aerial service restart` to swap the running process onto the regenerated wrapper.
|
|
223
246
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiayunxie/aerial",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Local
|
|
3
|
+
"version": "0.2.1",
|
|
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,
|
|
7
7
|
"homepage": "https://github.com/Xiejiayun/aerial#readme",
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
"copilot",
|
|
17
17
|
"codex",
|
|
18
18
|
"claude-code",
|
|
19
|
-
"local-proxy"
|
|
19
|
+
"local-proxy",
|
|
20
|
+
"upstream-proxy",
|
|
21
|
+
"socks5",
|
|
22
|
+
"shadowsocks",
|
|
23
|
+
"restricted-network"
|
|
20
24
|
],
|
|
21
25
|
"files": [
|
|
22
26
|
"src",
|
|
@@ -34,7 +38,7 @@
|
|
|
34
38
|
"prepublishOnly": "npm test"
|
|
35
39
|
},
|
|
36
40
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
41
|
+
"node": ">=24"
|
|
38
42
|
},
|
|
39
43
|
"license": "MIT",
|
|
40
44
|
"publishConfig": {
|
|
@@ -42,6 +46,7 @@
|
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
44
48
|
"smol-toml": "^1.6.1",
|
|
49
|
+
"socks": "^2.8.9",
|
|
45
50
|
"undici": "^7.25.0"
|
|
46
51
|
}
|
|
47
52
|
}
|
package/src/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { COPILOT_TOKEN_URL, GITHUB_CLIENT_ID } from "./constants.js";
|
|
3
3
|
import { githubTokenPath, writePrivateFile } from "./paths.js";
|
|
4
4
|
import { logEvent } from "./log.js";
|
|
5
|
+
import { upstreamFetch } from "./upstream-fetch.js";
|
|
5
6
|
|
|
6
7
|
let cachedCopilotToken;
|
|
7
8
|
let refreshPromise;
|
|
@@ -12,7 +13,7 @@ function formBody(values) {
|
|
|
12
13
|
|
|
13
14
|
export async function startDeviceFlow() {
|
|
14
15
|
logEvent("login_start");
|
|
15
|
-
const response = await
|
|
16
|
+
const response = await upstreamFetch("https://github.com/login/device/code", {
|
|
16
17
|
method: "POST",
|
|
17
18
|
headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
|
|
18
19
|
body: formBody({ client_id: GITHUB_CLIENT_ID, scope: "read:user copilot" })
|
|
@@ -25,7 +26,7 @@ export async function pollDeviceFlow(deviceCode, intervalSeconds) {
|
|
|
25
26
|
let interval = Math.max(Number(intervalSeconds || 5), 1);
|
|
26
27
|
while (true) {
|
|
27
28
|
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
28
|
-
const response = await
|
|
29
|
+
const response = await upstreamFetch("https://github.com/login/oauth/access_token", {
|
|
29
30
|
method: "POST",
|
|
30
31
|
headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
|
|
31
32
|
body: formBody({ client_id: GITHUB_CLIENT_ID, device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" })
|
|
@@ -75,7 +76,7 @@ function jwtExpirySeconds(token) {
|
|
|
75
76
|
|
|
76
77
|
export async function exchangeCopilotToken(githubToken = readGitHubToken()) {
|
|
77
78
|
if (!githubToken) throw new Error("Missing GitHub token. Run: aerial login");
|
|
78
|
-
const response = await
|
|
79
|
+
const response = await upstreamFetch(COPILOT_TOKEN_URL, {
|
|
79
80
|
headers: {
|
|
80
81
|
authorization: `Bearer ${githubToken}`,
|
|
81
82
|
accept: "application/json",
|
package/src/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { chooseSetupModel, formatModelChoices } from "./model-selection.js";
|
|
|
12
12
|
import { chooseSetupEffort, formatEffortSelection, assertValidEffort } from "./setup-selection.js";
|
|
13
13
|
import { printVersion } from "./version.js";
|
|
14
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";
|
|
15
17
|
|
|
16
18
|
const CLI_ENTRY = fileURLToPath(import.meta.url);
|
|
17
19
|
|
|
@@ -42,6 +44,7 @@ Usage:
|
|
|
42
44
|
aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
|
|
43
45
|
aerial service install
|
|
44
46
|
aerial status [--json]
|
|
47
|
+
aerial proxy status|enable|disable [--json]
|
|
45
48
|
|
|
46
49
|
Diagnostics and rollback:
|
|
47
50
|
aerial setup status [--json]
|
|
@@ -147,6 +150,180 @@ function printServiceSummary(status) {
|
|
|
147
150
|
console.log(`health: ${health}`);
|
|
148
151
|
}
|
|
149
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
|
+
|
|
150
327
|
async function appStatus({ json = false } = {}) {
|
|
151
328
|
const setup = setupStatus();
|
|
152
329
|
const service = await serviceStatus();
|
|
@@ -180,6 +357,11 @@ async function main() {
|
|
|
180
357
|
return;
|
|
181
358
|
}
|
|
182
359
|
|
|
360
|
+
if (command === "proxy") {
|
|
361
|
+
await runProxyCli(subcommand, rest);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
183
365
|
if (command === "login") {
|
|
184
366
|
const loginArgs = args.slice(1);
|
|
185
367
|
const force = loginArgs.includes("--force");
|
|
@@ -307,16 +489,7 @@ async function main() {
|
|
|
307
489
|
if (!rest.includes("--latest")) throw new Error("aerial setup restore: only --latest is supported in this release");
|
|
308
490
|
if (which === "all") {
|
|
309
491
|
const { ok, results } = restoreAllClients();
|
|
310
|
-
|
|
311
|
-
if (r.restored) {
|
|
312
|
-
console.log(`Restored ${r.target}: ${r.file} <- ${r.from}`);
|
|
313
|
-
if (r.snapshot) console.log(` pre-restore snapshot: ${r.snapshot}`);
|
|
314
|
-
} else if (r.reason === "no_backup") {
|
|
315
|
-
console.log(`Restored ${r.target}: no backup to restore`);
|
|
316
|
-
} else if (r.error) {
|
|
317
|
-
console.log(`Restored ${r.target}: FAILED ${r.error}`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
492
|
+
printRestoreResults(results);
|
|
320
493
|
process.exitCode = ok ? 0 : 1;
|
|
321
494
|
return;
|
|
322
495
|
}
|
|
@@ -334,16 +507,7 @@ async function main() {
|
|
|
334
507
|
|
|
335
508
|
if (command === "disable") {
|
|
336
509
|
const { ok: restoreOk, results } = restoreAllClients();
|
|
337
|
-
|
|
338
|
-
if (r.restored) {
|
|
339
|
-
console.log(`Restored ${r.target}: ${r.file} <- ${r.from}`);
|
|
340
|
-
if (r.snapshot) console.log(` pre-restore snapshot: ${r.snapshot}`);
|
|
341
|
-
} else if (r.reason === "no_backup") {
|
|
342
|
-
console.log(`Restored ${r.target}: no backup to restore`);
|
|
343
|
-
} else if (r.error) {
|
|
344
|
-
console.log(`Restored ${r.target}: FAILED ${r.error}`);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
510
|
+
printRestoreResults(results);
|
|
347
511
|
if (!restoreOk) {
|
|
348
512
|
console.log("service uninstall: skipped because client restore reported failures; resolve restore errors then rerun `aerial disable` or `aerial service uninstall`.");
|
|
349
513
|
process.exitCode = 1;
|
|
@@ -351,15 +515,7 @@ async function main() {
|
|
|
351
515
|
}
|
|
352
516
|
try {
|
|
353
517
|
const r = serviceUninstall();
|
|
354
|
-
|
|
355
|
-
else if (r.ok) console.log(`service uninstall: ok (${r.platform})`);
|
|
356
|
-
else {
|
|
357
|
-
console.log(`service uninstall: FAILED (${r.reason || "see stderr"})`);
|
|
358
|
-
if (r.message) console.log(` ${r.message}`);
|
|
359
|
-
else console.log(` Retry with: aerial service uninstall`);
|
|
360
|
-
}
|
|
361
|
-
if (r.bootout?.stderr) console.log(` bootout stderr: ${r.bootout.stderr.trim()}`);
|
|
362
|
-
if (r.delete?.stderr) console.log(` schtasks stderr: ${r.delete.stderr.trim()}`);
|
|
518
|
+
printServiceUninstallResult(r, { prefix: "service uninstall" });
|
|
363
519
|
process.exitCode = r.ok ? 0 : 1;
|
|
364
520
|
} catch (err) {
|
|
365
521
|
if (/unsupported platform/.test(err.message)) {
|
|
@@ -376,8 +532,7 @@ async function main() {
|
|
|
376
532
|
|
|
377
533
|
if (command === "service") {
|
|
378
534
|
if (subcommand === "install") {
|
|
379
|
-
|
|
380
|
-
const r = await serviceInstall();
|
|
535
|
+
await runServiceCliAction(serviceInstall, (r) => {
|
|
381
536
|
if (r.ok && r.note) console.log(`Service install: ${r.note} (${r.platform}).`);
|
|
382
537
|
else if (r.ok) console.log(`Service installed (${r.platform}).`);
|
|
383
538
|
else console.log(`Service install: FAILED (${r.reason || "unknown"}): ${r.message || ""}`);
|
|
@@ -387,98 +542,44 @@ async function main() {
|
|
|
387
542
|
if (r.bootstrap?.stderr) console.log(` bootstrap stderr: ${r.bootstrap.stderr.trim()}`);
|
|
388
543
|
if (r.create?.stderr) console.log(` schtasks stderr: ${r.create.stderr.trim()}`);
|
|
389
544
|
if (r.run?.stderr) console.log(` schtasks /Run stderr: ${r.run.stderr.trim()}`);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (r.diagnostics.wrapperNode) console.log(` wrapper node: ${r.diagnostics.wrapperNode}`);
|
|
394
|
-
if (r.diagnostics.health) {
|
|
395
|
-
const h = r.diagnostics.health;
|
|
396
|
-
const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
|
|
397
|
-
console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
|
|
398
|
-
}
|
|
399
|
-
console.log(` Run: aerial service status --json`);
|
|
400
|
-
}
|
|
401
|
-
if (r.warning) console.log(` WARNING: ${r.warning.message}`);
|
|
402
|
-
process.exitCode = r.ok ? 0 : 1;
|
|
403
|
-
} catch (err) {
|
|
404
|
-
console.error(err.message);
|
|
405
|
-
process.exitCode = 1;
|
|
406
|
-
}
|
|
545
|
+
printServiceDiagnostics(r.diagnostics);
|
|
546
|
+
printServiceWarning(r);
|
|
547
|
+
});
|
|
407
548
|
return;
|
|
408
549
|
}
|
|
409
550
|
if (subcommand === "start") {
|
|
410
|
-
|
|
411
|
-
const r = await serviceStart();
|
|
551
|
+
await runServiceCliAction(serviceStart, (r) => {
|
|
412
552
|
if (r.ok && r.note) console.log(`Service start: ${r.note} (${r.platform})`);
|
|
413
553
|
else if (r.ok) console.log(`Service start: ok (${r.platform})`);
|
|
414
554
|
else console.log(`Service start: FAILED (${r.reason || `status=${r.status}`})${r.message ? ": " + r.message : ""}`);
|
|
415
555
|
if (r.stderr) console.log(` ${r.stderr.trim()}`);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (r.diagnostics.wrapperNode) console.log(` wrapper node: ${r.diagnostics.wrapperNode}`);
|
|
420
|
-
if (r.diagnostics.health) {
|
|
421
|
-
const h = r.diagnostics.health;
|
|
422
|
-
const tail = h.lastError ? `, last error: ${h.lastError}` : (h.lastStatus !== undefined ? `, last status: ${h.lastStatus}` : "");
|
|
423
|
-
console.log(` health probe: ${h.attempts} attempts over ${h.elapsedMs}ms${tail}`);
|
|
424
|
-
}
|
|
425
|
-
console.log(` Run: aerial service status --json`);
|
|
426
|
-
}
|
|
427
|
-
if (r.warning) console.log(` WARNING: ${r.warning.message}`);
|
|
428
|
-
process.exitCode = r.ok ? 0 : 1;
|
|
429
|
-
} catch (err) {
|
|
430
|
-
console.error(err.message);
|
|
431
|
-
process.exitCode = 1;
|
|
432
|
-
}
|
|
556
|
+
printServiceDiagnostics(r.diagnostics);
|
|
557
|
+
printServiceWarning(r);
|
|
558
|
+
});
|
|
433
559
|
return;
|
|
434
560
|
}
|
|
435
561
|
if (subcommand === "stop") {
|
|
436
|
-
|
|
437
|
-
const r = serviceStop();
|
|
562
|
+
await runServiceCliAction(serviceStop, (r) => {
|
|
438
563
|
if (r.note) console.log(`Service stop: ${r.note} (${r.platform})`);
|
|
439
564
|
else if (r.ok) console.log(`Service stop: ok (${r.platform})`);
|
|
440
565
|
else console.log(`Service stop: FAILED (status=${r.status})`);
|
|
441
566
|
if (r.stderr) console.log(` ${r.stderr.trim()}`);
|
|
442
|
-
|
|
443
|
-
} catch (err) {
|
|
444
|
-
console.error(err.message);
|
|
445
|
-
process.exitCode = 1;
|
|
446
|
-
}
|
|
567
|
+
});
|
|
447
568
|
return;
|
|
448
569
|
}
|
|
449
570
|
if (subcommand === "restart") {
|
|
450
|
-
|
|
451
|
-
const r = await serviceRestart();
|
|
571
|
+
await runServiceCliAction(serviceRestart, (r) => {
|
|
452
572
|
if (!r.ok && r.reason === "stop_failed") console.log(`Service restart: FAILED on stop; start not attempted`);
|
|
453
573
|
else if (r.ok) console.log(`Service restart: ok`);
|
|
454
574
|
else console.log(`Service restart: FAILED`);
|
|
455
575
|
if (r.stop?.stderr) console.log(` stop: ${r.stop.stderr.trim()}`);
|
|
456
576
|
if (r.start?.stderr) console.log(` start: ${r.start.stderr.trim()}`);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
} catch (err) {
|
|
460
|
-
console.error(err.message);
|
|
461
|
-
process.exitCode = 1;
|
|
462
|
-
}
|
|
577
|
+
printServiceWarning(r);
|
|
578
|
+
});
|
|
463
579
|
return;
|
|
464
580
|
}
|
|
465
581
|
if (subcommand === "uninstall") {
|
|
466
|
-
|
|
467
|
-
const r = serviceUninstall();
|
|
468
|
-
if (r.note === "no service installed") console.log("Service uninstall: no service installed");
|
|
469
|
-
else if (r.ok) console.log(`Service uninstall: ok (${r.platform})`);
|
|
470
|
-
else {
|
|
471
|
-
console.log(`Service uninstall: FAILED (${r.reason || "see stderr"})`);
|
|
472
|
-
if (r.message) console.log(` ${r.message}`);
|
|
473
|
-
else console.log(` Retry with: aerial service uninstall`);
|
|
474
|
-
}
|
|
475
|
-
if (r.delete?.stderr) console.log(` schtasks stderr: ${r.delete.stderr.trim()}`);
|
|
476
|
-
if (r.bootout?.stderr) console.log(` bootout stderr: ${r.bootout.stderr.trim()}`);
|
|
477
|
-
process.exitCode = r.ok ? 0 : 1;
|
|
478
|
-
} catch (err) {
|
|
479
|
-
console.error(err.message);
|
|
480
|
-
process.exitCode = 1;
|
|
481
|
-
}
|
|
582
|
+
await runServiceCliAction(serviceUninstall, printServiceUninstallResult);
|
|
482
583
|
return;
|
|
483
584
|
}
|
|
484
585
|
if (subcommand === "status") {
|
package/src/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CONFIG_VERSION, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_VERSIONS } from "./
|
|
|
3
3
|
import { apiKeyPath, configPath, readJsonIfExists, writeJsonPrivate, writePrivateFile } from "./paths.js";
|
|
4
4
|
import { hashApiKey, randomApiKey, verifyApiKey } from "./crypto.js";
|
|
5
5
|
import { DEFAULT_EFFORT, normalizeEffort } from "./setup-selection.js";
|
|
6
|
+
import { PROXY_MODE_AUTO, normalizeProxyEndpoint, normalizeProxyMode } from "./proxy-config.js";
|
|
6
7
|
|
|
7
8
|
export function defaultConfig() {
|
|
8
9
|
return {
|
|
@@ -15,6 +16,9 @@ export function defaultConfig() {
|
|
|
15
16
|
logLevel: "info",
|
|
16
17
|
promptCacheRetention: "in_memory",
|
|
17
18
|
promptCacheKey: "auto",
|
|
19
|
+
upstreamProxyMode: "disabled",
|
|
20
|
+
upstreamProxyEndpoint: undefined,
|
|
21
|
+
upstreamProxySource: undefined,
|
|
18
22
|
versions: DEFAULT_VERSIONS
|
|
19
23
|
};
|
|
20
24
|
}
|
|
@@ -42,7 +46,24 @@ export function loadConfig() {
|
|
|
42
46
|
const promptCacheRetention = envRetention === undefined ? (loaded.promptCacheRetention ?? defaults.promptCacheRetention) : envRetention;
|
|
43
47
|
const promptCacheKey = envCacheKey === undefined ? (loaded.promptCacheKey ?? defaults.promptCacheKey) : envCacheKey;
|
|
44
48
|
const defaultEffort = normalizeEffort(loaded.defaultEffort) || DEFAULT_EFFORT;
|
|
45
|
-
|
|
49
|
+
const loadedProxyMode = normalizeProxyMode(loaded.upstreamProxyMode);
|
|
50
|
+
const loadedProxyEndpoint = normalizeProxyEndpoint(loaded.upstreamProxyEndpoint);
|
|
51
|
+
const upstreamProxyEndpoint = loadedProxyMode === PROXY_MODE_AUTO ? loadedProxyEndpoint : undefined;
|
|
52
|
+
const upstreamProxyMode = upstreamProxyEndpoint ? PROXY_MODE_AUTO : "disabled";
|
|
53
|
+
const upstreamProxySource = upstreamProxyEndpoint && typeof loaded.upstreamProxySource === "string"
|
|
54
|
+
? loaded.upstreamProxySource
|
|
55
|
+
: undefined;
|
|
56
|
+
return {
|
|
57
|
+
...defaults,
|
|
58
|
+
...loaded,
|
|
59
|
+
defaultEffort,
|
|
60
|
+
promptCacheRetention,
|
|
61
|
+
promptCacheKey,
|
|
62
|
+
upstreamProxyMode,
|
|
63
|
+
upstreamProxyEndpoint,
|
|
64
|
+
upstreamProxySource,
|
|
65
|
+
versions: { ...DEFAULT_VERSIONS, ...(loaded.versions || {}) }
|
|
66
|
+
};
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
export function saveConfig(config) {
|