nano-brain 2026.7.1102 → 2026.7.1104
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 -0
- package/npm/postinstall.js +139 -74
- package/npm/postinstall.test.js +81 -1
- package/npm/run.js +37 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Agent-oriented memory and code intelligence over MCP. AI agents don't read docs
|
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://github.com/nano-step/nano-brain)
|
|
10
10
|
[](https://www.npmjs.com/package/@nano-step/nano-brain)
|
|
11
|
+
[](https://www.npmjs.com/package/@nano-step/nano-brain)
|
|
11
12
|
[](https://hub.docker.com/r/nano-step/nano-brain)
|
|
12
13
|
[](https://discord.gg/nano-brain)
|
|
13
14
|
|
package/npm/postinstall.js
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const crypto = require("crypto");
|
|
9
|
-
const {
|
|
9
|
+
const { execFileSync } = require("child_process");
|
|
10
10
|
|
|
11
11
|
const VERSION = require("../package.json").version;
|
|
12
12
|
const REPO = "nano-step/nano-brain";
|
|
@@ -24,34 +24,69 @@ function getPlatformKey() {
|
|
|
24
24
|
const platform = PLATFORM_MAP[os.platform()];
|
|
25
25
|
const arch = ARCH_MAP[os.arch()];
|
|
26
26
|
if (!platform || !arch) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// Throw (do not process.exit) so ensureBinary's "throws, never exits"
|
|
28
|
+
// contract holds: main() turns this into a non-fatal install exit(0),
|
|
29
|
+
// run.js reports it and exits 1 (#594).
|
|
30
|
+
throw new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
|
|
30
31
|
}
|
|
31
32
|
return `${platform}-${arch}`;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// Remove a partial download without throwing. A socket error can fire before
|
|
36
|
+
// createWriteStream has opened the file, or after a redirect branch already
|
|
37
|
+
// removed it — an unguarded fs.unlinkSync then throws ENOENT *inside* the
|
|
38
|
+
// event callback, which is uncaught and crashes the whole postinstall (#592).
|
|
39
|
+
function safeUnlink(p) {
|
|
40
|
+
// No existsSync pre-check: the try/catch already swallows ENOENT, and a
|
|
41
|
+
// check-then-unlink is a TOCTOU race. Best-effort cleanup.
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(p);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
/* best-effort cleanup */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// file.close() is asynchronous; the fd is only released when its callback
|
|
50
|
+
// fires. Every unlink / recursive-redirect / reject is therefore deferred into
|
|
51
|
+
// file.close(() => …) so the partial file is gone (and not re-opened by a
|
|
52
|
+
// racing redirect) before we act — otherwise Windows unlink fails with EPERM
|
|
53
|
+
// while the fd is open, and a raw request error would leak the write stream.
|
|
34
54
|
function download(url, dest) {
|
|
35
55
|
return new Promise((resolve, reject) => {
|
|
36
56
|
const file = fs.createWriteStream(dest);
|
|
37
57
|
https.get(url, (res) => {
|
|
38
58
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
39
|
-
file.close()
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
file.close(() => {
|
|
60
|
+
safeUnlink(dest);
|
|
61
|
+
download(res.headers.location, dest).then(resolve).catch(reject);
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
42
64
|
}
|
|
43
65
|
if (res.statusCode !== 200) {
|
|
44
|
-
file.close()
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
file.close(() => {
|
|
67
|
+
safeUnlink(dest);
|
|
68
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
47
71
|
}
|
|
48
72
|
res.pipe(file);
|
|
73
|
+
// pipe() does NOT forward source 'error' events; without this a socket
|
|
74
|
+
// reset mid-transfer emits an unhandled 'error' on res and crashes the
|
|
75
|
+
// postinstall (#592) — mirror downloadWithHash's handler.
|
|
76
|
+
res.on("error", (err) => {
|
|
77
|
+
file.close(() => {
|
|
78
|
+
safeUnlink(dest);
|
|
79
|
+
reject(err);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
49
82
|
file.on("finish", () => {
|
|
50
83
|
file.close(resolve);
|
|
51
84
|
});
|
|
52
85
|
}).on("error", (err) => {
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
file.close(() => {
|
|
87
|
+
safeUnlink(dest);
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
55
90
|
});
|
|
56
91
|
});
|
|
57
92
|
}
|
|
@@ -62,14 +97,18 @@ function downloadWithHash(url, dest) {
|
|
|
62
97
|
const hash = crypto.createHash("sha256");
|
|
63
98
|
https.get(url, (res) => {
|
|
64
99
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
65
|
-
file.close()
|
|
66
|
-
|
|
67
|
-
|
|
100
|
+
file.close(() => {
|
|
101
|
+
safeUnlink(dest);
|
|
102
|
+
downloadWithHash(res.headers.location, dest).then(resolve).catch(reject);
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
68
105
|
}
|
|
69
106
|
if (res.statusCode !== 200) {
|
|
70
|
-
file.close()
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
file.close(() => {
|
|
108
|
+
safeUnlink(dest);
|
|
109
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
73
112
|
}
|
|
74
113
|
res.on("data", (chunk) => hash.update(chunk));
|
|
75
114
|
res.pipe(file);
|
|
@@ -77,13 +116,16 @@ function downloadWithHash(url, dest) {
|
|
|
77
116
|
file.close(() => resolve(hash.digest("hex")));
|
|
78
117
|
});
|
|
79
118
|
res.on("error", (err) => {
|
|
80
|
-
file.close()
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
file.close(() => {
|
|
120
|
+
safeUnlink(dest);
|
|
121
|
+
reject(err);
|
|
122
|
+
});
|
|
83
123
|
});
|
|
84
124
|
}).on("error", (err) => {
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
file.close(() => {
|
|
126
|
+
safeUnlink(dest);
|
|
127
|
+
reject(err);
|
|
128
|
+
});
|
|
87
129
|
});
|
|
88
130
|
});
|
|
89
131
|
}
|
|
@@ -110,9 +152,7 @@ async function verifySHA256(tag, assetName, binPath, computedHex) {
|
|
|
110
152
|
console.warn(`⚠ SHA256SUMS not available for ${tag} (${err.message}); skipping integrity verification.`);
|
|
111
153
|
return;
|
|
112
154
|
} finally {
|
|
113
|
-
|
|
114
|
-
try { fs.unlinkSync(sumsPath); } catch (_) {}
|
|
115
|
-
}
|
|
155
|
+
safeUnlink(sumsPath);
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
const expectedHex = parseSHA256Line(sumsContent, assetName);
|
|
@@ -122,7 +162,10 @@ async function verifySHA256(tag, assetName, binPath, computedHex) {
|
|
|
122
162
|
}
|
|
123
163
|
|
|
124
164
|
if (expectedHex.toLowerCase() !== computedHex.toLowerCase()) {
|
|
125
|
-
|
|
165
|
+
// safeUnlink so an unrelated fs error (e.g. EACCES) can't turn a SHA
|
|
166
|
+
// mismatch into a non-"SECURITY:" rejection that main() would treat as a
|
|
167
|
+
// retryable download failure, bypassing the hard-fail fail-safe.
|
|
168
|
+
safeUnlink(binPath);
|
|
126
169
|
throw new Error(
|
|
127
170
|
`SECURITY: SHA-256 mismatch for ${assetName}\n` +
|
|
128
171
|
` expected: ${expectedHex}\n` +
|
|
@@ -225,84 +268,106 @@ function tryAutoLink(srcBinPath, platform) {
|
|
|
225
268
|
}
|
|
226
269
|
}
|
|
227
270
|
|
|
228
|
-
|
|
229
|
-
const platformKey = getPlatformKey();
|
|
271
|
+
function binaryPath() {
|
|
230
272
|
const binName = os.platform() === "win32" ? "nano-brain.exe" : "nano-brain";
|
|
231
|
-
|
|
273
|
+
return path.join(__dirname, binName);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Download-and-verify the platform binary to binaryPath() and return that path.
|
|
277
|
+
// THROWS on failure (never calls process.exit) so both the install-time caller
|
|
278
|
+
// (main) and the run-time caller (run.js) can choose their own exit behavior.
|
|
279
|
+
// A "SECURITY:"-prefixed error signals an integrity failure that must hard-fail.
|
|
280
|
+
// Progress goes to stderr so it never pollutes a command's stdout when run.js
|
|
281
|
+
// downloads lazily on first invocation (#594).
|
|
282
|
+
async function ensureBinary() {
|
|
283
|
+
const platformKey = getPlatformKey();
|
|
284
|
+
const binPath = binaryPath();
|
|
232
285
|
|
|
233
286
|
const skipVerify = !!process.env.NANO_BRAIN_SKIP_SHA_VERIFY;
|
|
234
287
|
if (skipVerify) {
|
|
235
|
-
console.
|
|
288
|
+
console.error("⚠ NANO_BRAIN_SKIP_SHA_VERIFY is set; binary integrity check will be skipped.");
|
|
236
289
|
}
|
|
237
290
|
|
|
238
291
|
if (fs.existsSync(binPath)) {
|
|
239
292
|
try {
|
|
240
|
-
const output =
|
|
293
|
+
const output = execFileSync(binPath, ["version", "--json"], { timeout: 5000 }).toString();
|
|
241
294
|
if (output.includes(VERSION)) {
|
|
242
|
-
|
|
243
|
-
return;
|
|
295
|
+
return binPath;
|
|
244
296
|
}
|
|
245
|
-
|
|
246
|
-
|
|
297
|
+
console.error("Existing nano-brain binary is a different version; re-downloading.");
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error(`Existing nano-brain binary failed verification (${err.message}); re-downloading.`);
|
|
247
300
|
}
|
|
301
|
+
// Reached only when the existing binary is stale/broken. Unlink before
|
|
302
|
+
// overwriting: a running process keeps its handle to the old inode, so
|
|
303
|
+
// this avoids ETXTBSY on re-download (Linux/macOS).
|
|
304
|
+
safeUnlink(binPath);
|
|
248
305
|
}
|
|
249
306
|
|
|
250
|
-
console.
|
|
307
|
+
console.error(`Downloading nano-brain v${VERSION} for ${platformKey}...`);
|
|
251
308
|
|
|
252
309
|
const assetName = `nano-brain-${platformKey}`;
|
|
253
310
|
let lastErr;
|
|
254
|
-
|
|
311
|
+
|
|
312
|
+
const attempt = async (tag, suffix) => {
|
|
255
313
|
const url = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
|
|
314
|
+
if (skipVerify) {
|
|
315
|
+
await download(url, binPath);
|
|
316
|
+
} else {
|
|
317
|
+
const computedHex = await downloadWithHash(url, binPath);
|
|
318
|
+
await verifySHA256(tag, assetName, binPath, computedHex);
|
|
319
|
+
}
|
|
320
|
+
fs.chmodSync(binPath, 0o755);
|
|
321
|
+
console.error(`nano-brain v${VERSION} installed successfully from ${tag}${suffix || ""}.`);
|
|
322
|
+
return binPath;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
for (const tag of candidateTagsForVersion(VERSION)) {
|
|
256
326
|
try {
|
|
257
|
-
|
|
258
|
-
await download(url, binPath);
|
|
259
|
-
} else {
|
|
260
|
-
const computedHex = await downloadWithHash(url, binPath);
|
|
261
|
-
await verifySHA256(tag, assetName, binPath, computedHex);
|
|
262
|
-
}
|
|
263
|
-
fs.chmodSync(binPath, 0o755);
|
|
264
|
-
console.log(`nano-brain v${VERSION} installed successfully from ${tag}.`);
|
|
265
|
-
tryAutoLink(binPath);
|
|
266
|
-
return;
|
|
327
|
+
return await attempt(tag);
|
|
267
328
|
} catch (err) {
|
|
268
|
-
if (err && typeof err.message === "string" && err.message.startsWith("SECURITY:"))
|
|
269
|
-
console.error(err.message);
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
329
|
+
if (err && typeof err.message === "string" && err.message.startsWith("SECURITY:")) throw err;
|
|
272
330
|
lastErr = err;
|
|
273
331
|
}
|
|
274
332
|
}
|
|
275
333
|
|
|
334
|
+
let apiTag;
|
|
276
335
|
try {
|
|
277
|
-
|
|
278
|
-
if (tag) {
|
|
279
|
-
const url = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
|
|
280
|
-
if (skipVerify) {
|
|
281
|
-
await download(url, binPath);
|
|
282
|
-
} else {
|
|
283
|
-
const computedHex = await downloadWithHash(url, binPath);
|
|
284
|
-
await verifySHA256(tag, assetName, binPath, computedHex);
|
|
285
|
-
}
|
|
286
|
-
fs.chmodSync(binPath, 0o755);
|
|
287
|
-
console.log(`nano-brain v${VERSION} installed successfully from ${tag} (API fallback).`);
|
|
288
|
-
tryAutoLink(binPath);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
336
|
+
apiTag = await resolveTagFromAPI(VERSION, assetName);
|
|
291
337
|
} catch (err) {
|
|
292
|
-
if (err && typeof err.message === "string" && err.message.startsWith("SECURITY:")) {
|
|
293
|
-
console.error(err.message);
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
296
338
|
lastErr = err;
|
|
297
339
|
}
|
|
340
|
+
if (apiTag) {
|
|
341
|
+
// Not wrapped: a SECURITY error from the API-fallback attempt must propagate.
|
|
342
|
+
return await attempt(apiTag, " (API fallback)");
|
|
343
|
+
}
|
|
298
344
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
throw new Error(`Failed to download binary: ${lastErr && lastErr.message}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function main() {
|
|
349
|
+
try {
|
|
350
|
+
const bin = await ensureBinary();
|
|
351
|
+
// Auto-link only at install time — never from run.js's lazy path, so its
|
|
352
|
+
// console.log chatter can't pollute a command's stdout (#594).
|
|
353
|
+
tryAutoLink(bin);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const msg = err && err.message ? err.message : String(err);
|
|
356
|
+
if (msg.startsWith("SECURITY:")) {
|
|
357
|
+
console.error(msg);
|
|
358
|
+
process.exit(1); // integrity failure — never leave a bad binary
|
|
359
|
+
}
|
|
360
|
+
// Non-fatal at install time: don't fail `npm install`. run.js retries the
|
|
361
|
+
// download on first invocation, which also covers toolchains that don't
|
|
362
|
+
// persist postinstall-created files (#594).
|
|
363
|
+
console.error(msg);
|
|
364
|
+
console.error("Build from source: CGO_ENABLED=0 go build -o npm/nano-brain ./cmd/nano-brain");
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
302
367
|
}
|
|
303
368
|
|
|
304
369
|
if (require.main === module) {
|
|
305
370
|
main();
|
|
306
371
|
}
|
|
307
372
|
|
|
308
|
-
module.exports = { parseSHA256Line, downloadWithHash, verifySHA256, tryAutoLink };
|
|
373
|
+
module.exports = { parseSHA256Line, download, downloadWithHash, verifySHA256, tryAutoLink, safeUnlink, ensureBinary, binaryPath };
|
package/npm/postinstall.test.js
CHANGED
|
@@ -6,7 +6,87 @@ const fs = require("node:fs");
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const os = require("node:os");
|
|
8
8
|
const crypto = require("node:crypto");
|
|
9
|
-
const {
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
|
+
const { parseSHA256Line, tryAutoLink, safeUnlink, download, downloadWithHash, ensureBinary, binaryPath } = require("./postinstall");
|
|
11
|
+
const VERSION = require("../package.json").version;
|
|
12
|
+
const isWin = os.platform() === "win32";
|
|
13
|
+
|
|
14
|
+
// #594: when a valid binary is already at binaryPath(), ensureBinary() must
|
|
15
|
+
// return it WITHOUT any network download. Stub the binary with a tiny script
|
|
16
|
+
// that echoes the expected `version --json` payload. Any real binary already
|
|
17
|
+
// there is moved aside and restored so the assertion always runs (not skipped).
|
|
18
|
+
test("ensureBinary: returns existing binary without downloading", { skip: isWin }, async () => {
|
|
19
|
+
const bin = binaryPath();
|
|
20
|
+
const backup = `${bin}.itest.bak`;
|
|
21
|
+
const hadReal = fs.existsSync(bin);
|
|
22
|
+
if (hadReal) fs.renameSync(bin, backup);
|
|
23
|
+
fs.writeFileSync(bin, `#!/bin/sh\necho '{"version":"${VERSION}"}'\n`);
|
|
24
|
+
fs.chmodSync(bin, 0o755);
|
|
25
|
+
try {
|
|
26
|
+
const resolved = await ensureBinary();
|
|
27
|
+
assert.strictEqual(resolved, bin);
|
|
28
|
+
} finally {
|
|
29
|
+
safeUnlink(bin);
|
|
30
|
+
if (hadReal) fs.renameSync(backup, bin);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// #594 BLOCKER regression: an unsupported platform must REJECT (not process.exit),
|
|
35
|
+
// so run.js can report it and exit 1. getPlatformKey throws; ensureBinary propagates.
|
|
36
|
+
test("ensureBinary: rejects (does not exit) on unsupported platform", async () => {
|
|
37
|
+
const realPlatform = os.platform;
|
|
38
|
+
os.platform = () => "freebsd";
|
|
39
|
+
try {
|
|
40
|
+
await assert.rejects(() => ensureBinary(), /Unsupported platform/);
|
|
41
|
+
} finally {
|
|
42
|
+
os.platform = realPlatform;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// #594: run.js executes the binary NANO_BRAIN_BIN points to (override path),
|
|
47
|
+
// end-to-end via a subprocess — proves the wrapper's exec path works.
|
|
48
|
+
test("run.js: runs NANO_BRAIN_BIN target and exits 0", { skip: isWin }, () => {
|
|
49
|
+
const stub = path.join(os.tmpdir(), `nb-runjs-stub-${process.pid}-${Date.now()}`);
|
|
50
|
+
fs.writeFileSync(stub, `#!/bin/sh\necho NB_STUB_OK\n`);
|
|
51
|
+
fs.chmodSync(stub, 0o755);
|
|
52
|
+
try {
|
|
53
|
+
const r = spawnSync(process.execPath, [path.join(__dirname, "run.js"), "anyarg"], {
|
|
54
|
+
env: { ...process.env, NANO_BRAIN_BIN: stub },
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
});
|
|
57
|
+
assert.strictEqual(r.status, 0);
|
|
58
|
+
assert.match(r.stdout, /NB_STUB_OK/);
|
|
59
|
+
} finally {
|
|
60
|
+
safeUnlink(stub);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// #592 regression: a request/socket error must REJECT the promise (so main()'s
|
|
65
|
+
// per-tag retry loop continues), never throw synchronously or crash the
|
|
66
|
+
// process via an unguarded unlink. Point at a closed port → ECONNREFUSED.
|
|
67
|
+
test("download: rejects (not throws) on connection error, no lingering file", async () => {
|
|
68
|
+
const dest = path.join(os.tmpdir(), `nb-dl-refused-${process.pid}-${Date.now()}`);
|
|
69
|
+
await assert.rejects(() => download("https://127.0.0.1:1/x", dest));
|
|
70
|
+
assert.strictEqual(fs.existsSync(dest), false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("downloadWithHash: rejects (not throws) on connection error, no lingering file", async () => {
|
|
74
|
+
const dest = path.join(os.tmpdir(), `nb-dlh-refused-${process.pid}-${Date.now()}`);
|
|
75
|
+
await assert.rejects(() => downloadWithHash("https://127.0.0.1:1/x", dest));
|
|
76
|
+
assert.strictEqual(fs.existsSync(dest), false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("safeUnlink: does not throw on a nonexistent path (#592 regression)", () => {
|
|
80
|
+
const missing = path.join(os.tmpdir(), `nb-safeunlink-missing-${process.pid}-${Date.now()}`);
|
|
81
|
+
assert.doesNotThrow(() => safeUnlink(missing));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("safeUnlink: removes an existing file", () => {
|
|
85
|
+
const p = path.join(os.tmpdir(), `nb-safeunlink-${process.pid}-${Date.now()}`);
|
|
86
|
+
fs.writeFileSync(p, "x");
|
|
87
|
+
safeUnlink(p);
|
|
88
|
+
assert.strictEqual(fs.existsSync(p), false);
|
|
89
|
+
});
|
|
10
90
|
|
|
11
91
|
test("parseSHA256Line: returns hash for matching filename in single-line content", () => {
|
|
12
92
|
const content = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 nano-brain-linux-amd64\n";
|
package/npm/run.js
CHANGED
|
@@ -9,6 +9,23 @@ const os = require("os");
|
|
|
9
9
|
const binName = os.platform() === "win32" ? "nano-brain.exe" : "nano-brain";
|
|
10
10
|
const binPath = path.join(__dirname, binName);
|
|
11
11
|
|
|
12
|
+
function runBinary(bin) {
|
|
13
|
+
try {
|
|
14
|
+
execFileSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// A numeric status means the binary ran and chose that exit code (it has
|
|
17
|
+
// already produced its own output). A non-numeric status means we could
|
|
18
|
+
// not spawn it at all (arch mismatch, missing libc, ETXTBSY, perms) —
|
|
19
|
+
// surface that, since stdio:"inherit" shows nothing for a spawn failure.
|
|
20
|
+
if (typeof e.status !== "number") {
|
|
21
|
+
process.stderr.write(`Error: failed to execute binary at ${bin}: ${e.message}\n`);
|
|
22
|
+
}
|
|
23
|
+
process.exit(e.status || 1);
|
|
24
|
+
}
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Explicit override wins and skips any download.
|
|
12
29
|
const envBin = process.env.NANO_BRAIN_BIN;
|
|
13
30
|
if (envBin && envBin.trim() !== "") {
|
|
14
31
|
const trimmed = envBin.trim();
|
|
@@ -16,26 +33,30 @@ if (envBin && envBin.trim() !== "") {
|
|
|
16
33
|
process.stderr.write(`Error: NANO_BRAIN_BIN points to ${trimmed} which does not exist. Unset the variable or correct the path.\n`);
|
|
17
34
|
process.exit(1);
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
if ((mode & 0o111) === 0) {
|
|
36
|
+
if ((fs.statSync(trimmed).mode & 0o111) === 0) {
|
|
21
37
|
process.stderr.write(`Error: NANO_BRAIN_BIN points to ${trimmed} which is not executable. Run: chmod +x ${trimmed}\n`);
|
|
22
38
|
process.exit(1);
|
|
23
39
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
process.exit(0);
|
|
40
|
+
runBinary(trimmed);
|
|
41
|
+
return; // runBinary exits; explicit return makes the branches structurally exclusive
|
|
27
42
|
}
|
|
28
43
|
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.error("Try: npm install -g @nano-step/nano-brain --foreground-scripts");
|
|
33
|
-
console.error("Or build from source: CGO_ENABLED=0 go build -o npm/nano-brain ./cmd/nano-brain");
|
|
34
|
-
process.exit(1);
|
|
44
|
+
if (fs.existsSync(binPath)) {
|
|
45
|
+
runBinary(binPath);
|
|
46
|
+
return;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
// The binary is missing — the postinstall download either failed (offline /
|
|
50
|
+
// proxy) or was not persisted by the package manager (some npm/node versions
|
|
51
|
+
// discard postinstall-created files, #594). Download it now, at run time, where
|
|
52
|
+
// the write always persists, then run. Progress goes to stderr.
|
|
53
|
+
process.stderr.write("nano-brain: binary not present; downloading on first run...\n");
|
|
54
|
+
require("./postinstall")
|
|
55
|
+
.ensureBinary()
|
|
56
|
+
.then((bin) => runBinary(bin))
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
const msg = err && err.message ? err.message : String(err);
|
|
59
|
+
process.stderr.write(`nano-brain: could not obtain the binary: ${msg}\n`);
|
|
60
|
+
process.stderr.write("Fix: set NANO_BRAIN_BIN=/path/to/nano-brain, or build from source: CGO_ENABLED=0 go build -o npm/nano-brain ./cmd/nano-brain\n");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|