codex-endpoint-switcher 1.6.2 → 1.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-endpoint-switcher",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "用于切换 Codex URL 和 Key 的本地网页控制台与 npm CLI",
5
5
  "main": "src/main/main.js",
6
6
  "bin": {
@@ -3,6 +3,8 @@ const { spawn } = require("node:child_process");
3
3
 
4
4
  const WAIT_PARENT_EXIT_TIMEOUT_MS = 20000;
5
5
  const WAIT_PARENT_POLL_MS = 400;
6
+ const INSTALL_RETRY_TIMES = 3;
7
+ const INSTALL_RETRY_BASE_DELAY_MS = 1500;
6
8
 
7
9
  function sleep(timeoutMs) {
8
10
  return new Promise((resolve) => {
@@ -38,9 +40,10 @@ function runProcess(command, args, options = {}) {
38
40
  stderr += chunk.toString("utf8");
39
41
  });
40
42
 
41
- child.on("exit", (code) => {
43
+ child.on("exit", (code, signal) => {
42
44
  resolve({
43
- code: Number(code || 0),
45
+ code: normalizeExitCode(code),
46
+ signal: signal || "",
44
47
  stdout,
45
48
  stderr,
46
49
  });
@@ -56,6 +59,78 @@ function runProcess(command, args, options = {}) {
56
59
  });
57
60
  }
58
61
 
62
+ function normalizeExitCode(code) {
63
+ const safeCode = Number(code || 0);
64
+ if (!Number.isFinite(safeCode)) {
65
+ return 0;
66
+ }
67
+
68
+ if (safeCode > 0x7fffffff) {
69
+ return safeCode - 0x100000000;
70
+ }
71
+
72
+ return safeCode;
73
+ }
74
+
75
+ function isBusyInstallFailure(result) {
76
+ const combined = `${result.stdout || ""}\n${result.stderr || ""}`.toUpperCase();
77
+ return (
78
+ result.code === -4082 ||
79
+ combined.includes("EBUSY") ||
80
+ combined.includes("RESOURCE BUSY OR LOCKED") ||
81
+ combined.includes("EPERM") ||
82
+ combined.includes("OPERATION NOT PERMITTED")
83
+ );
84
+ }
85
+
86
+ async function installLatestPackage(payload) {
87
+ let lastResult = null;
88
+
89
+ for (let attempt = 1; attempt <= INSTALL_RETRY_TIMES; attempt += 1) {
90
+ if (attempt > 1) {
91
+ await appendLog(
92
+ payload.logPath,
93
+ `[${new Date().toISOString()}] 检测到文件占用,开始第 ${attempt} 次重试安装。\n`,
94
+ );
95
+ await sleep(INSTALL_RETRY_BASE_DELAY_MS * attempt);
96
+ }
97
+
98
+ const result = await runProcess(
99
+ payload.nodePath,
100
+ [payload.npmCliPath, "install", "-g", `${payload.packageName}@latest`],
101
+ {
102
+ cwd: process.cwd(),
103
+ env: process.env,
104
+ },
105
+ );
106
+
107
+ if (result.stdout) {
108
+ await appendLog(payload.logPath, result.stdout);
109
+ }
110
+ if (result.stderr) {
111
+ await appendLog(payload.logPath, result.stderr);
112
+ }
113
+
114
+ lastResult = result;
115
+ if (result.code === 0) {
116
+ return result;
117
+ }
118
+
119
+ if (!isBusyInstallFailure(result) || attempt >= INSTALL_RETRY_TIMES) {
120
+ return result;
121
+ }
122
+ }
123
+
124
+ return (
125
+ lastResult || {
126
+ code: 1,
127
+ signal: "",
128
+ stdout: "",
129
+ stderr: "自动更新安装没有返回结果。",
130
+ }
131
+ );
132
+ }
133
+
59
134
  function isProcessAlive(pid) {
60
135
  try {
61
136
  process.kill(pid, 0);
@@ -145,25 +220,13 @@ async function main() {
145
220
  await sleep(Number(payload.waitSeconds || 3) * 1000);
146
221
  await waitForProcessExit(Number(payload.parentPid || 0), payload.logPath);
147
222
 
148
- const installResult = await runProcess(
149
- payload.nodePath,
150
- [payload.npmCliPath, "install", "-g", `${payload.packageName}@latest`],
151
- {
152
- cwd: process.cwd(),
153
- env: process.env,
154
- },
155
- );
156
-
157
- if (installResult.stdout) {
158
- await appendLog(payload.logPath, installResult.stdout);
159
- }
160
- if (installResult.stderr) {
161
- await appendLog(payload.logPath, installResult.stderr);
162
- }
223
+ const installResult = await installLatestPackage(payload);
163
224
 
164
225
  if (installResult.code !== 0) {
165
226
  const finishedAt = new Date().toISOString();
166
- const errorMessage = `npm 安装失败,退出码:${installResult.code}`;
227
+ const errorMessage = isBusyInstallFailure(installResult)
228
+ ? `npm 安装失败:文件仍被占用,请稍后重试或先手动关闭占用进程。退出码:${installResult.code}`
229
+ : `npm 安装失败,退出码:${installResult.code}`;
167
230
  await appendLog(payload.logPath, `[${finishedAt}] ${errorMessage}\n`);
168
231
  await writeState(payload.statePath, {
169
232
  status: "failed",
@@ -26,6 +26,36 @@ function getAutoUpdateStatePath() {
26
26
  return path.join(stateDir, "codex-switcher-auto-update.json");
27
27
  }
28
28
 
29
+ function getAutoUpdateRuntimeDir() {
30
+ const runtimeDir = path.join(os.homedir(), ".codex", "runtime");
31
+ fs.mkdirSync(runtimeDir, { recursive: true });
32
+ return runtimeDir;
33
+ }
34
+
35
+ function cleanupStagedAutoUpdateRunners(runtimeDir) {
36
+ const staleBeforeMs = Date.now() - 24 * 60 * 60 * 1000;
37
+
38
+ for (const entry of fs.readdirSync(runtimeDir, { withFileTypes: true })) {
39
+ if (
40
+ !entry.isFile() ||
41
+ !entry.name.startsWith("codex-switcher-auto-update-runner-") ||
42
+ !entry.name.endsWith(".js")
43
+ ) {
44
+ continue;
45
+ }
46
+
47
+ const fullPath = path.join(runtimeDir, entry.name);
48
+ try {
49
+ const stats = fs.statSync(fullPath);
50
+ if (stats.mtimeMs < staleBeforeMs) {
51
+ fs.unlinkSync(fullPath);
52
+ }
53
+ } catch {
54
+ // 文件被占用或刚被其他进程删除时忽略。
55
+ }
56
+ }
57
+ }
58
+
29
59
  function readAutoUpdateState() {
30
60
  try {
31
61
  const content = fs.readFileSync(getAutoUpdateStatePath(), "utf8");
@@ -100,6 +130,17 @@ function resolveRuntimePaths() {
100
130
  };
101
131
  }
102
132
 
133
+ function stageAutoUpdateRunner() {
134
+ const runtimeDir = getAutoUpdateRuntimeDir();
135
+ cleanupStagedAutoUpdateRunners(runtimeDir);
136
+ const stagedRunnerPath = path.join(
137
+ runtimeDir,
138
+ `codex-switcher-auto-update-runner-${Date.now()}.js`,
139
+ );
140
+ fs.copyFileSync(runnerEntryPath, stagedRunnerPath);
141
+ return stagedRunnerPath;
142
+ }
143
+
103
144
  function sleep(timeoutMs) {
104
145
  return new Promise((resolve) => {
105
146
  setTimeout(resolve, timeoutMs);
@@ -217,6 +258,7 @@ function scheduleAutoUpdate(options = {}) {
217
258
  const logPath = getAutoUpdateLogPath();
218
259
  const statePath = getAutoUpdateStatePath();
219
260
  const runtimePaths = resolveRuntimePaths();
261
+ const stagedRunnerPath = stageAutoUpdateRunner();
220
262
 
221
263
  writeAutoUpdateState({
222
264
  status: "scheduled",
@@ -246,7 +288,7 @@ function scheduleAutoUpdate(options = {}) {
246
288
  "utf8",
247
289
  ).toString("base64url");
248
290
 
249
- const child = spawn(process.execPath, [runnerEntryPath, payload], {
291
+ const child = spawn(process.execPath, [stagedRunnerPath, payload], {
250
292
  cwd: os.homedir(),
251
293
  detached: true,
252
294
  stdio: "ignore",