@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.
- package/README.md +1 -1
- package/docs/usage.md +2 -2
- package/package.json +5 -5
- package/src/cli/args.js +28 -0
- package/src/cli/config-command.js +39 -0
- package/src/cli/disable-command.js +28 -0
- package/src/{doctor.js → cli/doctor.js} +3 -3
- package/src/cli/help.js +23 -0
- package/src/cli/index.js +92 -0
- package/src/cli/key-command.js +20 -0
- package/src/cli/login-command.js +28 -0
- package/src/{model-selection.js → cli/model-selection.js} +5 -5
- package/src/cli/output.js +75 -0
- package/src/{probe.js → cli/probe.js} +3 -3
- package/src/cli/proxy-command.js +120 -0
- package/src/cli/runtime-auth.js +21 -0
- package/src/cli/service-command.js +120 -0
- package/src/cli/setup-command.js +122 -0
- package/src/{setup-selection.js → cli/setup-selection.js} +3 -20
- package/src/cli/start-command.js +11 -0
- package/src/cli/status-command.js +26 -0
- package/src/{version.js → cli/version.js} +1 -1
- package/src/proxy/cache-policy.js +89 -0
- package/src/proxy/cache-telemetry.js +172 -0
- package/src/proxy/effort.js +120 -0
- package/src/proxy/headers.js +33 -0
- package/src/proxy/index.js +85 -0
- package/src/proxy/models.js +27 -0
- package/src/{responses-websocket.js → proxy/responses-websocket.js} +2 -2
- package/src/{server.js → proxy/server.js} +5 -5
- package/src/proxy/transport.js +55 -0
- package/src/service/health.js +54 -0
- package/src/service/index.js +38 -0
- package/src/service/lifecycle.js +301 -0
- package/src/service/platform.js +227 -0
- package/src/service/runner.js +45 -0
- package/src/service/status.js +296 -0
- package/src/service/wrapper-render.js +228 -0
- package/src/setup/backup.js +38 -0
- package/src/{setup.js → setup/clients.js} +11 -198
- package/src/setup/index.js +4 -0
- package/src/setup/restore.js +90 -0
- package/src/setup/status.js +24 -0
- package/src/setup/toml.js +41 -0
- package/src/{auth.js → shared/auth.js} +1 -1
- package/src/{config.js → shared/config.js} +2 -2
- package/src/shared/effort.js +19 -0
- package/src/shared/file-utils.js +31 -0
- package/src/{paths.js → shared/paths.js} +2 -3
- package/src/{upstream-fetch.js → upstream/fetch.js} +1 -1
- package/src/cli.js +0 -684
- package/src/copilot.js +0 -572
- package/src/service.js +0 -1182
- /package/src/{app-status.js → cli/app-status.js} +0 -0
- /package/src/{model-catalog.js → proxy/model-catalog.js} +0 -0
- /package/src/{model-utils.js → proxy/model-utils.js} +0 -0
- /package/src/{constants.js → shared/constants.js} +0 -0
- /package/src/{crypto.js → shared/crypto.js} +0 -0
- /package/src/{http-utils.js → shared/http-utils.js} +0 -0
- /package/src/{log.js → shared/log.js} +0 -0
- /package/src/{prompt-utils.js → shared/prompt-utils.js} +0 -0
- /package/src/{proxy-config.js → upstream/proxy-config.js} +0 -0
- /package/src/{socks5-bridge.js → upstream/socks5-bridge.js} +0 -0
|
@@ -2,15 +2,13 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parse as parseToml } from "smol-toml";
|
|
5
|
-
import { ensureApiKey, loadConfig, saveConfig } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { ensureApiKey, loadConfig, saveConfig } from "../shared/config.js";
|
|
6
|
+
import { logEvent } from "../shared/log.js";
|
|
7
|
+
import { atomicWriteFile } from "../shared/file-utils.js";
|
|
8
|
+
import { assertValidEffort, normalizeEffort } from "../shared/effort.js";
|
|
9
|
+
import { backupIfExists, backupPathsFor } from "./backup.js";
|
|
10
|
+
import { setTomlRootString, upsertTomlSection } from "./toml.js";
|
|
10
11
|
|
|
11
|
-
const BACKUP_PREFIX = ".aerial-backup-";
|
|
12
|
-
const PRE_RESTORE_PREFIX = ".aerial-pre-restore-";
|
|
13
|
-
const ISO_STAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;
|
|
14
12
|
const DEFAULT_CODEX_AUTH = Object.freeze({
|
|
15
13
|
command: "aerial",
|
|
16
14
|
args: ["key", "print"],
|
|
@@ -19,59 +17,10 @@ const DEFAULT_CODEX_AUTH = Object.freeze({
|
|
|
19
17
|
});
|
|
20
18
|
const DEFAULT_CLAUDE_API_KEY_HELPER = "aerial key print";
|
|
21
19
|
|
|
22
|
-
function backupIfExists(file) {
|
|
23
|
-
if (!fs.existsSync(file)) return undefined;
|
|
24
|
-
const backup = `${file}.aerial-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
25
|
-
fs.copyFileSync(file, backup);
|
|
26
|
-
return backup;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
function ensureParent(file) {
|
|
30
21
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
function tomlValue(value) {
|
|
34
|
-
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
35
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
36
|
-
return JSON.stringify(String(value));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function escapeRegExp(value) {
|
|
40
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function setTomlRootString(content, key, value) {
|
|
44
|
-
const line = `${key} = ${tomlValue(value)}`;
|
|
45
|
-
const source = content.split(/\r?\n/);
|
|
46
|
-
const firstSection = source.findIndex((sourceLine) => /^\s*\[.*\]\s*(?:#.*)?$/.test(sourceLine));
|
|
47
|
-
const rootLines = firstSection === -1 ? source : source.slice(0, firstSection);
|
|
48
|
-
const restLines = firstSection === -1 ? [] : source.slice(firstSection);
|
|
49
|
-
const root = rootLines.join("\n");
|
|
50
|
-
const rest = restLines.join("\n");
|
|
51
|
-
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=.*$`, "m");
|
|
52
|
-
const nextRoot = re.test(root) ? root.replace(re, line) : `${root.trimEnd()}${root.trimEnd() ? "\n" : ""}${line}`;
|
|
53
|
-
if (!rest) return `${nextRoot.trimEnd()}\n`;
|
|
54
|
-
return `${nextRoot.trimEnd()}\n${rest}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function upsertTomlSection(content, section, values) {
|
|
58
|
-
const heading = `[${section}]`;
|
|
59
|
-
const lines = Object.entries(values).map(([key, value]) => `${key} = ${tomlValue(value)}`).join("\n");
|
|
60
|
-
const block = `${heading}\n${lines}\n`;
|
|
61
|
-
const source = content.split(/\r?\n/);
|
|
62
|
-
const start = source.findIndex((line) => line.trim() === heading);
|
|
63
|
-
if (start === -1) return `${content.trimEnd()}\n\n${block}`;
|
|
64
|
-
let end = source.length;
|
|
65
|
-
for (let i = start + 1; i < source.length; i += 1) {
|
|
66
|
-
if (/^\s*\[.*\]\s*$/.test(source[i])) {
|
|
67
|
-
end = i;
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
source.splice(start, end - start, ...block.trimEnd().split("\n"));
|
|
72
|
-
return `${source.join("\n").trimEnd()}\n`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
24
|
function claudeEnvForAerial(currentEnv, config) {
|
|
76
25
|
const {
|
|
77
26
|
ANTHROPIC_API_KEY,
|
|
@@ -96,7 +45,7 @@ export function setupCodex({ model, effort, authCommand = DEFAULT_CODEX_AUTH } =
|
|
|
96
45
|
if (!selectedModel) {
|
|
97
46
|
throw new Error("setupCodex requires a model id; pass --model or let `aerial setup codex` select one from live Copilot models.");
|
|
98
47
|
}
|
|
99
|
-
const file =
|
|
48
|
+
const file = codexConfigFile();
|
|
100
49
|
ensureParent(file);
|
|
101
50
|
const backup = backupIfExists(file);
|
|
102
51
|
let content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
@@ -116,7 +65,7 @@ export function setupCodex({ model, effort, authCommand = DEFAULT_CODEX_AUTH } =
|
|
|
116
65
|
const profileValues = { model_provider: "aerial", model: selectedModel };
|
|
117
66
|
if (normalizedEffort) profileValues.model_reasoning_effort = normalizedEffort;
|
|
118
67
|
content = upsertTomlSection(content, "profiles.aerial", profileValues);
|
|
119
|
-
|
|
68
|
+
atomicWriteFile(file, content);
|
|
120
69
|
if (normalizedEffort && config.defaultEffort !== normalizedEffort) {
|
|
121
70
|
saveConfig({ ...config, defaultEffort: normalizedEffort });
|
|
122
71
|
}
|
|
@@ -129,8 +78,7 @@ export function setupClaude({ model, effort, apiKeyHelper = DEFAULT_CLAUDE_API_K
|
|
|
129
78
|
ensureApiKey();
|
|
130
79
|
const config = loadConfig();
|
|
131
80
|
const selectedModel = model || config.defaultModel;
|
|
132
|
-
const
|
|
133
|
-
const file = path.join(dir, "settings.json");
|
|
81
|
+
const file = claudeSettingsFile();
|
|
134
82
|
ensureParent(file);
|
|
135
83
|
const backup = backupIfExists(file);
|
|
136
84
|
const current = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : {};
|
|
@@ -140,7 +88,7 @@ export function setupClaude({ model, effort, apiKeyHelper = DEFAULT_CLAUDE_API_K
|
|
|
140
88
|
env: claudeEnvForAerial(current.env, config)
|
|
141
89
|
};
|
|
142
90
|
if (selectedModel) next.model = selectedModel;
|
|
143
|
-
|
|
91
|
+
atomicWriteFile(file, `${JSON.stringify(next, null, 2)}\n`);
|
|
144
92
|
if (normalizedEffort && config.defaultEffort !== normalizedEffort) {
|
|
145
93
|
saveConfig({ ...config, defaultEffort: normalizedEffort });
|
|
146
94
|
}
|
|
@@ -156,32 +104,6 @@ function claudeSettingsFile() {
|
|
|
156
104
|
return path.join(os.homedir(), ".claude", "settings.json");
|
|
157
105
|
}
|
|
158
106
|
|
|
159
|
-
function listBackups(file) {
|
|
160
|
-
const dir = path.dirname(file);
|
|
161
|
-
const base = path.basename(file);
|
|
162
|
-
if (!fs.existsSync(dir)) return [];
|
|
163
|
-
const prefix = `${base}${BACKUP_PREFIX}`;
|
|
164
|
-
const entries = fs.readdirSync(dir);
|
|
165
|
-
const matches = [];
|
|
166
|
-
for (const entry of entries) {
|
|
167
|
-
if (!entry.startsWith(prefix)) continue;
|
|
168
|
-
const stamp = entry.slice(prefix.length);
|
|
169
|
-
if (!ISO_STAMP_RE.test(stamp)) continue;
|
|
170
|
-
matches.push({ name: entry, path: path.join(dir, entry), stamp });
|
|
171
|
-
}
|
|
172
|
-
matches.sort((a, b) => (a.stamp < b.stamp ? -1 : a.stamp > b.stamp ? 1 : 0));
|
|
173
|
-
return matches;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function findLatestBackup(file) {
|
|
177
|
-
const all = listBackups(file);
|
|
178
|
-
return all.length ? all[all.length - 1] : undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function backupPathsFor(file) {
|
|
182
|
-
return listBackups(file).map((entry) => entry.path);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
107
|
function codexStateFromDoc(doc, expectedBaseUrl) {
|
|
186
108
|
const providerSection = doc && typeof doc === "object" ? doc.model_providers?.aerial : undefined;
|
|
187
109
|
const providerSet = doc?.model_provider === "aerial";
|
|
@@ -275,7 +197,7 @@ function validateJsonBackup(content) {
|
|
|
275
197
|
}
|
|
276
198
|
}
|
|
277
199
|
|
|
278
|
-
const CLIENTS = Object.freeze({
|
|
200
|
+
export const CLIENTS = Object.freeze({
|
|
279
201
|
codex: Object.freeze({
|
|
280
202
|
target: "codex",
|
|
281
203
|
file: codexConfigFile,
|
|
@@ -289,112 +211,3 @@ const CLIENTS = Object.freeze({
|
|
|
289
211
|
validateBackup: validateJsonBackup
|
|
290
212
|
})
|
|
291
213
|
});
|
|
292
|
-
|
|
293
|
-
function clientDescriptor(target) {
|
|
294
|
-
const descriptor = CLIENTS[target];
|
|
295
|
-
if (!descriptor) throw new Error(`Unknown restore target: ${target}. Use codex, claude, or all.`);
|
|
296
|
-
return descriptor;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export function setupStatus() {
|
|
300
|
-
const config = loadConfig();
|
|
301
|
-
const apiKeyFile = apiKeyPath();
|
|
302
|
-
const githubTokenFile = githubTokenPath();
|
|
303
|
-
return {
|
|
304
|
-
schema: "aerial.setup-status.v1",
|
|
305
|
-
platform: process.platform,
|
|
306
|
-
config: { host: config.host, port: config.port },
|
|
307
|
-
auth: {
|
|
308
|
-
api_key: { file: apiKeyFile, exists: fs.existsSync(apiKeyFile) },
|
|
309
|
-
github_token: (() => {
|
|
310
|
-
const source = gitHubTokenSource();
|
|
311
|
-
return { file: githubTokenFile, exists: source !== "missing", source };
|
|
312
|
-
})()
|
|
313
|
-
},
|
|
314
|
-
clients: Object.fromEntries(Object.entries(CLIENTS).map(([target, client]) => [target, client.status()]))
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function clientFile(target) {
|
|
319
|
-
return clientDescriptor(target).file();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function resolveWritePath(file) {
|
|
323
|
-
if (!fs.existsSync(file)) return file;
|
|
324
|
-
try {
|
|
325
|
-
return fs.realpathSync(file);
|
|
326
|
-
} catch {
|
|
327
|
-
return file;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function validateBackupContent(target, content) {
|
|
332
|
-
clientDescriptor(target).validateBackup(content);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function resolveRestoreMode(writePath, backupPath, targetExisted) {
|
|
336
|
-
if (process.platform === "win32") return undefined;
|
|
337
|
-
let preserved;
|
|
338
|
-
if (targetExisted) {
|
|
339
|
-
try { preserved = fs.statSync(writePath).mode & 0o777; } catch { preserved = undefined; }
|
|
340
|
-
}
|
|
341
|
-
if (preserved === undefined) {
|
|
342
|
-
try { preserved = fs.statSync(backupPath).mode & 0o777; } catch { preserved = 0o600; }
|
|
343
|
-
}
|
|
344
|
-
return preserved & 0o600;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export function restoreClient(target, { now = () => new Date() } = {}) {
|
|
348
|
-
const file = clientFile(target);
|
|
349
|
-
const writePath = resolveWritePath(file);
|
|
350
|
-
const latest = findLatestBackup(file);
|
|
351
|
-
if (!latest) {
|
|
352
|
-
return { target, ok: true, restored: false, reason: "no_backup", file };
|
|
353
|
-
}
|
|
354
|
-
let backupContent;
|
|
355
|
-
try {
|
|
356
|
-
backupContent = fs.readFileSync(latest.path);
|
|
357
|
-
} catch (err) {
|
|
358
|
-
throw new Error(`Restore failed: cannot read backup ${latest.path}: ${err.message}`);
|
|
359
|
-
}
|
|
360
|
-
validateBackupContent(target, backupContent);
|
|
361
|
-
const targetExisted = fs.existsSync(writePath);
|
|
362
|
-
const mode = resolveRestoreMode(writePath, latest.path, targetExisted);
|
|
363
|
-
let snapshot;
|
|
364
|
-
if (targetExisted) {
|
|
365
|
-
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
366
|
-
snapshot = `${writePath}${PRE_RESTORE_PREFIX}${stamp}`;
|
|
367
|
-
fs.copyFileSync(writePath, snapshot);
|
|
368
|
-
}
|
|
369
|
-
ensureParent(writePath);
|
|
370
|
-
const tmp = `${writePath}.aerial-restore-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
371
|
-
const writeOpts = mode !== undefined ? { mode } : undefined;
|
|
372
|
-
fs.writeFileSync(tmp, backupContent, writeOpts);
|
|
373
|
-
try {
|
|
374
|
-
fs.renameSync(tmp, writePath);
|
|
375
|
-
} catch (err) {
|
|
376
|
-
try { fs.unlinkSync(tmp); } catch {}
|
|
377
|
-
if (err.code === "EXDEV") {
|
|
378
|
-
throw new Error(`Restore failed: backup and target on different filesystems (EXDEV). File: ${writePath}. Move the backup next to the target and retry.`);
|
|
379
|
-
}
|
|
380
|
-
throw err;
|
|
381
|
-
}
|
|
382
|
-
if (mode !== undefined) {
|
|
383
|
-
try { fs.chmodSync(writePath, mode); } catch {}
|
|
384
|
-
}
|
|
385
|
-
logEvent("setup_restore", { target, file: writePath, from: latest.path, snapshot, mode });
|
|
386
|
-
return { target, ok: true, restored: true, file: writePath, from: latest.path, snapshot, mode };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function restoreAllClients(opts) {
|
|
390
|
-
const results = {};
|
|
391
|
-
for (const target of Object.keys(CLIENTS)) {
|
|
392
|
-
try {
|
|
393
|
-
results[target] = restoreClient(target, opts);
|
|
394
|
-
} catch (err) {
|
|
395
|
-
results[target] = { target, ok: false, error: err.message };
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
const ok = Object.values(results).every((r) => r.ok);
|
|
399
|
-
return { ok, results };
|
|
400
|
-
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { logEvent } from "../shared/log.js";
|
|
3
|
+
import { atomicWriteFile } from "../shared/file-utils.js";
|
|
4
|
+
import { findLatestBackup } from "./backup.js";
|
|
5
|
+
import { CLIENTS } from "./clients.js";
|
|
6
|
+
|
|
7
|
+
const PRE_RESTORE_PREFIX = ".aerial-pre-restore-";
|
|
8
|
+
|
|
9
|
+
function clientDescriptor(target) {
|
|
10
|
+
const descriptor = CLIENTS[target];
|
|
11
|
+
if (!descriptor) throw new Error(`Unknown restore target: ${target}. Use codex, claude, or all.`);
|
|
12
|
+
return descriptor;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clientFile(target) {
|
|
16
|
+
return clientDescriptor(target).file();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveWritePath(file) {
|
|
20
|
+
if (!fs.existsSync(file)) return file;
|
|
21
|
+
try {
|
|
22
|
+
return fs.realpathSync(file);
|
|
23
|
+
} catch {
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateBackupContent(target, content) {
|
|
29
|
+
clientDescriptor(target).validateBackup(content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveRestoreMode(writePath, backupPath, targetExisted) {
|
|
33
|
+
if (process.platform === "win32") return undefined;
|
|
34
|
+
let preserved;
|
|
35
|
+
if (targetExisted) {
|
|
36
|
+
try { preserved = fs.statSync(writePath).mode & 0o777; } catch { preserved = undefined; }
|
|
37
|
+
}
|
|
38
|
+
if (preserved === undefined) {
|
|
39
|
+
try { preserved = fs.statSync(backupPath).mode & 0o777; } catch { preserved = 0o600; }
|
|
40
|
+
}
|
|
41
|
+
return preserved & 0o600;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function restoreClient(target, { now = () => new Date() } = {}) {
|
|
45
|
+
const file = clientFile(target);
|
|
46
|
+
const writePath = resolveWritePath(file);
|
|
47
|
+
const latest = findLatestBackup(file);
|
|
48
|
+
if (!latest) {
|
|
49
|
+
return { target, ok: true, restored: false, reason: "no_backup", file };
|
|
50
|
+
}
|
|
51
|
+
let backupContent;
|
|
52
|
+
try {
|
|
53
|
+
backupContent = fs.readFileSync(latest.path);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`Restore failed: cannot read backup ${latest.path}: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
validateBackupContent(target, backupContent);
|
|
58
|
+
const targetExisted = fs.existsSync(writePath);
|
|
59
|
+
const mode = resolveRestoreMode(writePath, latest.path, targetExisted);
|
|
60
|
+
let snapshot;
|
|
61
|
+
if (targetExisted) {
|
|
62
|
+
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
63
|
+
snapshot = `${writePath}${PRE_RESTORE_PREFIX}${stamp}`;
|
|
64
|
+
fs.copyFileSync(writePath, snapshot);
|
|
65
|
+
}
|
|
66
|
+
const writeOpts = mode !== undefined ? { mode } : undefined;
|
|
67
|
+
try {
|
|
68
|
+
atomicWriteFile(writePath, backupContent, writeOpts);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.code === "EXDEV") {
|
|
71
|
+
throw new Error(`Restore failed: backup and target on different filesystems (EXDEV). File: ${writePath}. Move the backup next to the target and retry.`);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
logEvent("setup_restore", { target, file: writePath, from: latest.path, snapshot, mode });
|
|
76
|
+
return { target, ok: true, restored: true, file: writePath, from: latest.path, snapshot, mode };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function restoreAllClients(opts) {
|
|
80
|
+
const results = {};
|
|
81
|
+
for (const target of Object.keys(CLIENTS)) {
|
|
82
|
+
try {
|
|
83
|
+
results[target] = restoreClient(target, opts);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
results[target] = { target, ok: false, error: err.message };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const ok = Object.values(results).every((r) => r.ok);
|
|
89
|
+
return { ok, results };
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { loadConfig } from "../shared/config.js";
|
|
3
|
+
import { apiKeyPath, githubTokenPath } from "../shared/paths.js";
|
|
4
|
+
import { gitHubTokenSource } from "../shared/auth.js";
|
|
5
|
+
import { CLIENTS } from "./clients.js";
|
|
6
|
+
|
|
7
|
+
export function setupStatus() {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
const apiKeyFile = apiKeyPath();
|
|
10
|
+
const githubTokenFile = githubTokenPath();
|
|
11
|
+
return {
|
|
12
|
+
schema: "aerial.setup-status.v1",
|
|
13
|
+
platform: process.platform,
|
|
14
|
+
config: { host: config.host, port: config.port },
|
|
15
|
+
auth: {
|
|
16
|
+
api_key: { file: apiKeyFile, exists: fs.existsSync(apiKeyFile) },
|
|
17
|
+
github_token: (() => {
|
|
18
|
+
const source = gitHubTokenSource();
|
|
19
|
+
return { file: githubTokenFile, exists: source !== "missing", source };
|
|
20
|
+
})()
|
|
21
|
+
},
|
|
22
|
+
clients: Object.fromEntries(Object.entries(CLIENTS).map(([target, client]) => [target, client.status()]))
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function tomlValue(value) {
|
|
2
|
+
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
3
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4
|
+
return JSON.stringify(String(value));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function escapeRegExp(value) {
|
|
8
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setTomlRootString(content, key, value) {
|
|
12
|
+
const line = `${key} = ${tomlValue(value)}`;
|
|
13
|
+
const source = content.split(/\r?\n/);
|
|
14
|
+
const firstSection = source.findIndex((sourceLine) => /^\s*\[.*\]\s*(?:#.*)?$/.test(sourceLine));
|
|
15
|
+
const rootLines = firstSection === -1 ? source : source.slice(0, firstSection);
|
|
16
|
+
const restLines = firstSection === -1 ? [] : source.slice(firstSection);
|
|
17
|
+
const root = rootLines.join("\n");
|
|
18
|
+
const rest = restLines.join("\n");
|
|
19
|
+
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=.*$`, "m");
|
|
20
|
+
const nextRoot = re.test(root) ? root.replace(re, line) : `${root.trimEnd()}${root.trimEnd() ? "\n" : ""}${line}`;
|
|
21
|
+
if (!rest) return `${nextRoot.trimEnd()}\n`;
|
|
22
|
+
return `${nextRoot.trimEnd()}\n${rest}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function upsertTomlSection(content, section, values) {
|
|
26
|
+
const heading = `[${section}]`;
|
|
27
|
+
const lines = Object.entries(values).map(([key, value]) => `${key} = ${tomlValue(value)}`).join("\n");
|
|
28
|
+
const block = `${heading}\n${lines}\n`;
|
|
29
|
+
const source = content.split(/\r?\n/);
|
|
30
|
+
const start = source.findIndex((line) => line.trim() === heading);
|
|
31
|
+
if (start === -1) return `${content.trimEnd()}\n\n${block}`;
|
|
32
|
+
let end = source.length;
|
|
33
|
+
for (let i = start + 1; i < source.length; i += 1) {
|
|
34
|
+
if (/^\s*\[.*\]\s*$/.test(source[i])) {
|
|
35
|
+
end = i;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
source.splice(start, end - start, ...block.trimEnd().split("\n"));
|
|
40
|
+
return `${source.join("\n").trimEnd()}\n`;
|
|
41
|
+
}
|
|
@@ -2,7 +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 "
|
|
5
|
+
import { upstreamFetch } from "../upstream/fetch.js";
|
|
6
6
|
|
|
7
7
|
let cachedCopilotToken;
|
|
8
8
|
let refreshPromise;
|
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import { CONFIG_VERSION, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_VERSIONS } from "./constants.js";
|
|
3
3
|
import { apiKeyPath, configPath, readJsonIfExists, writeJsonPrivate, writePrivateFile } from "./paths.js";
|
|
4
4
|
import { hashApiKey, randomApiKey, verifyApiKey } from "./crypto.js";
|
|
5
|
-
import { DEFAULT_EFFORT, normalizeEffort } from "./
|
|
6
|
-
import { PROXY_MODE_AUTO, normalizeProxyEndpoint, normalizeProxyMode } from "
|
|
5
|
+
import { DEFAULT_EFFORT, normalizeEffort } from "./effort.js";
|
|
6
|
+
import { PROXY_MODE_AUTO, normalizeProxyEndpoint, normalizeProxyMode } from "../upstream/proxy-config.js";
|
|
7
7
|
|
|
8
8
|
export function defaultConfig() {
|
|
9
9
|
return {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const EFFORT_VALUES = Object.freeze(["low", "medium", "high", "xhigh"]);
|
|
2
|
+
export const DEFAULT_EFFORT = "medium";
|
|
3
|
+
|
|
4
|
+
export function normalizeEffort(value) {
|
|
5
|
+
if (value === undefined || value === null) return undefined;
|
|
6
|
+
const trimmed = String(value).trim().toLowerCase();
|
|
7
|
+
if (!trimmed) return undefined;
|
|
8
|
+
if (trimmed === "max") return "xhigh";
|
|
9
|
+
if (EFFORT_VALUES.includes(trimmed)) return trimmed;
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertValidEffort(raw) {
|
|
14
|
+
const normalized = normalizeEffort(raw);
|
|
15
|
+
if (!normalized) {
|
|
16
|
+
throw new Error(`Invalid --effort ${JSON.stringify(raw)}. Allowed: ${EFFORT_VALUES.join(", ")} (or alias 'max' for xhigh).`);
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function ensureParentDir(file) {
|
|
5
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function atomicWriteFile(file, content, { mode } = {}) {
|
|
9
|
+
ensureParentDir(file);
|
|
10
|
+
const tmp = `${file}.aerial-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
11
|
+
const opts = mode !== undefined ? { mode } : undefined;
|
|
12
|
+
try {
|
|
13
|
+
fs.writeFileSync(tmp, content, opts);
|
|
14
|
+
try {
|
|
15
|
+
fs.renameSync(tmp, file);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (process.platform === "win32" && fs.existsSync(file) && (err.code === "EEXIST" || err.code === "EPERM")) {
|
|
18
|
+
fs.unlinkSync(file);
|
|
19
|
+
fs.renameSync(tmp, file);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
if (process.platform !== "win32" && mode !== undefined) {
|
|
25
|
+
fs.chmodSync(file, mode);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import { atomicWriteFile } from "./file-utils.js";
|
|
4
5
|
|
|
5
6
|
export function configDir() {
|
|
6
7
|
if (process.env.AERIAL_CONFIG_DIR) return process.env.AERIAL_CONFIG_DIR;
|
|
@@ -30,9 +31,7 @@ export function ensureDir(dir = configDir()) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function writePrivateFile(file, content) {
|
|
33
|
-
|
|
34
|
-
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
35
|
-
if (process.platform !== "win32") fs.chmodSync(file, 0o600);
|
|
34
|
+
atomicWriteFile(file, content, { mode: 0o600 });
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export function readJsonIfExists(file) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { ProxyAgent } from "undici";
|
|
3
|
-
import { loadConfig, saveConfig } from "
|
|
3
|
+
import { loadConfig, saveConfig } from "../shared/config.js";
|
|
4
4
|
import { PROXY_MODE_AUTO, PROXY_MODE_DISABLED, isSocksProxyEndpoint, normalizeProxyEndpoint, normalizeProxyMode } from "./proxy-config.js";
|
|
5
5
|
import { startSocks5Bridge, _closeSocks5BridgesForTests } from "./socks5-bridge.js";
|
|
6
6
|
|