@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
@@ -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 "./config.js";
6
- import { apiKeyPath, githubTokenPath } from "./paths.js";
7
- import { gitHubTokenSource } from "./auth.js";
8
- import { logEvent } from "./log.js";
9
- import { assertValidEffort, normalizeEffort } from "./setup-selection.js";
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 = path.join(os.homedir(), ".codex", "config.toml");
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
- fs.writeFileSync(file, content, "utf8");
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 dir = path.join(os.homedir(), ".claude");
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
- fs.writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
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,4 @@
1
+ export { findLatestBackup } from "./backup.js";
2
+ export { setupCodex, setupClaude, codexStatus, claudeStatus } from "./clients.js";
3
+ export { setupStatus } from "./status.js";
4
+ export { restoreClient, restoreAllClients } from "./restore.js";
@@ -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 "./upstream-fetch.js";
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 "./setup-selection.js";
6
- import { PROXY_MODE_AUTO, normalizeProxyEndpoint, normalizeProxyMode } from "./proxy-config.js";
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
- ensureDir(path.dirname(file));
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 "./config.js";
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