nuwax-codex 0.17.8 → 0.18.0

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.
@@ -4,7 +4,7 @@
4
4
  // run and caches it locally. No platform-specific npm packages needed.
5
5
 
6
6
  import { spawnSync } from "node:child_process";
7
- import { createWriteStream, existsSync, mkdirSync, chmodSync } from "node:fs";
7
+ import { createWriteStream, existsSync, mkdirSync, chmodSync, renameSync, rmSync, cpSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { join, dirname } from "node:path";
10
10
  import { familySync } from "detect-libc";
@@ -24,10 +24,16 @@ function getTargetTriple() {
24
24
  return a === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
25
25
  }
26
26
  if (p === "linux") {
27
- if (a !== "x64") throw new Error(`Unsupported Linux arch: ${a}`);
28
- return familySync() === "musl"
29
- ? "x86_64-unknown-linux-musl"
30
- : "x86_64-unknown-linux-gnu";
27
+ const musl = familySync() === "musl";
28
+ if (a === "x64") {
29
+ return musl ? "x86_64-unknown-linux-musl" : "x86_64-unknown-linux-gnu";
30
+ }
31
+ if (a === "arm64") {
32
+ // Release builds aarch64-unknown-linux-musl; glibc arm64 maps to it
33
+ // (static musl binaries run on glibc hosts).
34
+ return "aarch64-unknown-linux-musl";
35
+ }
36
+ throw new Error(`Unsupported Linux arch: ${a}`);
31
37
  }
32
38
  if (p === "win32") {
33
39
  return a === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc";
@@ -46,7 +52,9 @@ function getBinaryName() {
46
52
  // -- download & cache -------------------------------------------------------
47
53
 
48
54
  function cacheDir() {
49
- return join(homedir(), ".nuwax-codex-cache", VERSION);
55
+ // Include the target triple: sharing HOME across glibc/musl containers
56
+ // or architectures would otherwise execute the wrong binary.
57
+ return join(homedir(), ".nuwax-codex-cache", VERSION, getTargetTriple());
50
58
  }
51
59
 
52
60
  function cachedBinaryPath() {
@@ -67,6 +75,12 @@ async function downloadBinary(url, outPath) {
67
75
  let downloaded = 0;
68
76
  const reader = res.body.getReader();
69
77
  const ws = createWriteStream(outPath);
78
+ // Surface write errors (EACCES/ENOSPC/EMFILE) as promise rejections
79
+ // instead of unhandled 'error' events that kill the process before the
80
+ // outer catch can make the install fault-tolerant.
81
+ const writeFailed = new Promise((_, reject) => {
82
+ ws.on("error", reject);
83
+ });
70
84
  const logInterval = setInterval(() => {
71
85
  if (total > 0) {
72
86
  process.stderr.write(
@@ -81,6 +95,11 @@ async function downloadBinary(url, outPath) {
81
95
  if (done) break;
82
96
  ws.write(value);
83
97
  downloaded += value.length;
98
+ // A failed write rejects `writeFailed`; race so it escapes as a
99
+ // normal rejection the outer catch can tolerate.
100
+ await Promise.race([writeFailed.then(() => {}, () => {
101
+ throw new Error(`Failed to write ${outPath} (disk full or permission denied)`);
102
+ }), Promise.resolve()]);
84
103
  }
85
104
  } finally {
86
105
  clearInterval(logInterval);
@@ -161,16 +180,47 @@ async function ensureBinary() {
161
180
  console.error(` ${url}`);
162
181
 
163
182
  const dir = cacheDir();
164
- mkdirSync(dir, { recursive: true });
165
- const archivePath = join(dir, `nuwax-codex.${ext}`);
166
183
 
167
- await downloadBinary(url, archivePath);
184
+ // Atomic install (#38): download+extract into unique staging, verify,
185
+ // then rename into final cache. A crash mid-extract leaves only the
186
+ // staging dir; the cache never holds a partial install.
187
+ const lockDir = `${dir}.lock-${process.pid}-${Date.now()}`;
188
+ mkdirSync(lockDir, { recursive: true });
189
+ const stagingDir = join(lockDir, "stage");
190
+ mkdirSync(stagingDir, { recursive: true });
191
+ const archivePath = join(lockDir, `nuwax-codex.${ext}`);
192
+
193
+ try {
194
+ await downloadBinary(url, archivePath);
195
+
196
+ console.error(" Extracting …");
197
+ if (ext === "tar.gz") {
198
+ await extractTarGz(archivePath, stagingDir);
199
+ } else {
200
+ await extractZip(archivePath, stagingDir);
201
+ }
202
+
203
+ const stagedBinary = join(stagingDir, getBinaryName());
204
+ if (!existsSync(stagedBinary)) {
205
+ throw new Error(
206
+ `Extraction completed but ${getBinaryName()} not found in staging directory`,
207
+ );
208
+ }
168
209
 
169
- console.error(" Extracting …");
170
- if (ext === "tar.gz") {
171
- await extractTarGz(archivePath, dir);
172
- } else {
173
- await extractZip(archivePath, dir);
210
+ mkdirSync(dirname(dir), { recursive: true });
211
+ try {
212
+ renameSync(stagingDir, dir);
213
+ } catch (e) {
214
+ if (e.code === "ENOTEMPTY" || e.code === "EEXIST") {
215
+ if (!existsSync(cached)) throw e;
216
+ } else if (e.code === "EXDEV") {
217
+ cpSync(stagingDir, dir, { recursive: true });
218
+ } else {
219
+ throw e;
220
+ }
221
+ }
222
+ } finally {
223
+ rmSync(lockDir, { recursive: true, force: true });
174
224
  }
175
225
 
176
226
  if (!existsSync(cached)) {
@@ -183,9 +233,13 @@ async function ensureBinary() {
183
233
 
184
234
  function run() {
185
235
  ensureBinary().then((binaryPath) => {
236
+ // Tag the child as installed via the nuwax npm package so codex's
237
+ // update flow targets nuwax-codex (not upstream @openai/codex).
238
+ const env = { ...process.env, CODEX_INSTALL_SOURCE: "npm_nuwax" };
186
239
  const result = spawnSync(binaryPath, process.argv.slice(2), {
187
240
  stdio: "inherit",
188
241
  windowsHide: true,
242
+ env,
189
243
  });
190
244
  if (result.error) {
191
245
  console.error(`Failed to execute ${binaryPath}:`, result.error);
@@ -2,7 +2,7 @@
2
2
  // Postinstall script: pre-downloads the native `nuwax-codex` binary from
3
3
  // Alibaba Cloud OSS so the first CLI invocation is instant.
4
4
 
5
- import { createWriteStream, existsSync, mkdirSync, chmodSync } from "node:fs";
5
+ import { createWriteStream, existsSync, mkdirSync, chmodSync, renameSync, rmSync, cpSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { join, dirname } from "node:path";
8
8
  import { createRequire } from "node:module";
@@ -43,7 +43,9 @@ function getBinaryName() {
43
43
  }
44
44
 
45
45
  function cacheDir() {
46
- return join(homedir(), ".nuwax-codex-cache", VERSION);
46
+ // Include the target triple: sharing HOME across glibc/musl containers
47
+ // or architectures would otherwise execute the wrong binary.
48
+ return join(homedir(), ".nuwax-codex-cache", VERSION, getTargetTriple());
47
49
  }
48
50
 
49
51
  function cachedBinaryPath() {
@@ -66,6 +68,12 @@ async function downloadBinary(url, outPath) {
66
68
  let downloaded = 0;
67
69
  const reader = res.body.getReader();
68
70
  const ws = createWriteStream(outPath);
71
+ // Surface write errors (EACCES/ENOSPC/EMFILE) as promise rejections
72
+ // instead of unhandled 'error' events that kill the process before the
73
+ // outer catch can make the install fault-tolerant.
74
+ const writeFailed = new Promise((_, reject) => {
75
+ ws.on("error", reject);
76
+ });
69
77
  const logInterval = setInterval(() => {
70
78
  if (total > 0) {
71
79
  process.stderr.write(
@@ -79,6 +87,11 @@ async function downloadBinary(url, outPath) {
79
87
  if (done) break;
80
88
  ws.write(value);
81
89
  downloaded += value.length;
90
+ // A failed write rejects `writeFailed`; race so it escapes as a
91
+ // normal rejection the outer catch can tolerate.
92
+ await Promise.race([writeFailed.then(() => {}, () => {
93
+ throw new Error(`Failed to write ${outPath} (disk full or permission denied)`);
94
+ }), Promise.resolve()]);
82
95
  }
83
96
  } finally {
84
97
  clearInterval(logInterval);
@@ -165,16 +178,43 @@ async function main() {
165
178
  process.stderr.write(` ${url}\n`);
166
179
 
167
180
  const dir = cacheDir();
168
- mkdirSync(dir, { recursive: true });
169
- const archivePath = join(dir, `nuwax-codex.${ext}`);
170
181
 
171
- await downloadBinary(url, archivePath);
182
+ // Atomic install (#38): staging directory + verify + rename commit.
183
+ const lockDir = `${dir}.lock-${process.pid}-${Date.now()}`;
184
+ mkdirSync(lockDir, { recursive: true });
185
+ const stagingDir = join(lockDir, "stage");
186
+ mkdirSync(stagingDir, { recursive: true });
187
+ const archivePath = join(lockDir, `nuwax-codex.${ext}`);
172
188
 
173
- process.stderr.write(" Extracting …\n");
174
- if (ext === "tar.gz") {
175
- await extractTarGz(archivePath, dir);
176
- } else {
177
- await extractZip(archivePath, dir);
189
+ try {
190
+ await downloadBinary(url, archivePath);
191
+
192
+ process.stderr.write(" Extracting …\n");
193
+ if (ext === "tar.gz") {
194
+ await extractTarGz(archivePath, stagingDir);
195
+ } else {
196
+ await extractZip(archivePath, stagingDir);
197
+ }
198
+
199
+ const stagedBinary = join(stagingDir, getBinaryName());
200
+ if (!existsSync(stagedBinary)) {
201
+ throw new Error(`Extraction completed but ${getBinaryName()} not found in staging`);
202
+ }
203
+
204
+ mkdirSync(dirname(dir), { recursive: true });
205
+ try {
206
+ renameSync(stagingDir, dir);
207
+ } catch (e) {
208
+ if (e.code === "ENOTEMPTY" || e.code === "EEXIST") {
209
+ if (!existsSync(cached)) throw e;
210
+ } else if (e.code === "EXDEV") {
211
+ cpSync(stagingDir, dir, { recursive: true });
212
+ } else {
213
+ throw e;
214
+ }
215
+ }
216
+ } finally {
217
+ rmSync(lockDir, { recursive: true, force: true });
178
218
  }
179
219
 
180
220
  if (existsSync(cached)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuwax-codex",
3
- "version": "0.17.8",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "description": "Fork of OpenAI Codex CLI with domestic (Chinese) LLM support via genai",
6
6
  "license": "Apache-2.0",