@nano-step/nano-brain 2026.6.306 → 2026.6.308
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/npm/postinstall.js +44 -1
- package/npm/postinstall.test.js +234 -1
- package/package.json +1 -1
package/npm/postinstall.js
CHANGED
|
@@ -184,6 +184,47 @@ async function resolveTagFromAPI(version, assetName) {
|
|
|
184
184
|
return null;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// --- Opt-in auto-link ---
|
|
188
|
+
function tryAutoLink(srcBinPath, platform) {
|
|
189
|
+
platform = platform || process.platform;
|
|
190
|
+
|
|
191
|
+
const optIn = process.env.NANO_BRAIN_AUTO_LINK === "1" ||
|
|
192
|
+
fs.existsSync(path.join(os.homedir(), ".nano-brain", "auto-link"));
|
|
193
|
+
|
|
194
|
+
if (!optIn) return;
|
|
195
|
+
|
|
196
|
+
if (platform === "win32") {
|
|
197
|
+
console.log(`INFO: Auto-link is not supported on Windows; nano-brain binary remains at ${srcBinPath}.`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let targetDir, targetPath;
|
|
202
|
+
if (platform === "linux") {
|
|
203
|
+
targetDir = path.join(os.homedir(), ".local", "bin");
|
|
204
|
+
targetPath = path.join(targetDir, "nano-brain");
|
|
205
|
+
} else if (platform === "darwin") {
|
|
206
|
+
targetDir = path.join(os.homedir(), "Library", "nano-brain", "bin");
|
|
207
|
+
targetPath = path.join(targetDir, "nano-brain");
|
|
208
|
+
} else {
|
|
209
|
+
console.log(`INFO: Auto-link is not supported on ${platform}; nano-brain binary remains at ${srcBinPath}.`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (fs.existsSync(targetPath)) {
|
|
214
|
+
console.log(`WARN: ${targetPath} already exists; skipping auto-link to preserve your existing file.`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
fs.mkdirSync(targetDir, { recursive: true, mode: 0o755 });
|
|
220
|
+
fs.copyFileSync(srcBinPath, targetPath);
|
|
221
|
+
fs.chmodSync(targetPath, 0o755);
|
|
222
|
+
console.log(`Copied nano-brain to ${targetPath}. Ensure ${targetDir} is in your PATH.`);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.log(`WARN: failed to auto-link binary: ${e.message}. Binary remains at ${srcBinPath}.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
187
228
|
async function main() {
|
|
188
229
|
const platformKey = getPlatformKey();
|
|
189
230
|
const binName = os.platform() === "win32" ? "nano-brain.exe" : "nano-brain";
|
|
@@ -221,6 +262,7 @@ async function main() {
|
|
|
221
262
|
}
|
|
222
263
|
fs.chmodSync(binPath, 0o755);
|
|
223
264
|
console.log(`nano-brain v${VERSION} installed successfully from ${tag}.`);
|
|
265
|
+
tryAutoLink(binPath);
|
|
224
266
|
return;
|
|
225
267
|
} catch (err) {
|
|
226
268
|
if (err && typeof err.message === "string" && err.message.startsWith("SECURITY:")) {
|
|
@@ -243,6 +285,7 @@ async function main() {
|
|
|
243
285
|
}
|
|
244
286
|
fs.chmodSync(binPath, 0o755);
|
|
245
287
|
console.log(`nano-brain v${VERSION} installed successfully from ${tag} (API fallback).`);
|
|
288
|
+
tryAutoLink(binPath);
|
|
246
289
|
return;
|
|
247
290
|
}
|
|
248
291
|
} catch (err) {
|
|
@@ -262,4 +305,4 @@ if (require.main === module) {
|
|
|
262
305
|
main();
|
|
263
306
|
}
|
|
264
307
|
|
|
265
|
-
module.exports = { parseSHA256Line, downloadWithHash, verifySHA256 };
|
|
308
|
+
module.exports = { parseSHA256Line, downloadWithHash, verifySHA256, tryAutoLink };
|
package/npm/postinstall.test.js
CHANGED
|
@@ -6,7 +6,7 @@ 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 { parseSHA256Line } = require("./postinstall");
|
|
9
|
+
const { parseSHA256Line, tryAutoLink } = require("./postinstall");
|
|
10
10
|
|
|
11
11
|
test("parseSHA256Line: returns hash for matching filename in single-line content", () => {
|
|
12
12
|
const content = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 nano-brain-linux-amd64\n";
|
|
@@ -110,3 +110,236 @@ test("end-to-end: full integrity flow with mocked content matches expected hash"
|
|
|
110
110
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
test("TestAutoLink_OptIn_Linux_HappyPath", (t) => {
|
|
115
|
+
process.env.NANO_BRAIN_AUTO_LINK = "1";
|
|
116
|
+
t.after(() => { delete process.env.NANO_BRAIN_AUTO_LINK; });
|
|
117
|
+
|
|
118
|
+
const HOME = os.homedir();
|
|
119
|
+
const targetDir = path.join(HOME, ".local", "bin");
|
|
120
|
+
const targetPath = path.join(targetDir, "nano-brain");
|
|
121
|
+
const calls = [];
|
|
122
|
+
|
|
123
|
+
const origExists = fs.existsSync;
|
|
124
|
+
const origMkdir = fs.mkdirSync;
|
|
125
|
+
const origCopy = fs.copyFileSync;
|
|
126
|
+
const origChmod = fs.chmodSync;
|
|
127
|
+
|
|
128
|
+
fs.existsSync = (p) => {
|
|
129
|
+
if (p === path.join(HOME, ".nano-brain", "auto-link")) return false;
|
|
130
|
+
if (p === targetPath) return false;
|
|
131
|
+
return origExists(p);
|
|
132
|
+
};
|
|
133
|
+
fs.mkdirSync = (dir, opts) => { calls.push({ fn: "mkdirSync", dir, opts }); };
|
|
134
|
+
fs.copyFileSync = (src, dst) => { calls.push({ fn: "copyFileSync", src, dst }); };
|
|
135
|
+
fs.chmodSync = (p, mode) => { calls.push({ fn: "chmodSync", p, mode }); };
|
|
136
|
+
|
|
137
|
+
const output = [];
|
|
138
|
+
const origLog = console.log;
|
|
139
|
+
console.log = (...args) => output.push(args.join(" "));
|
|
140
|
+
try {
|
|
141
|
+
tryAutoLink("/fake/nano-brain", "linux");
|
|
142
|
+
} finally {
|
|
143
|
+
console.log = origLog;
|
|
144
|
+
fs.existsSync = origExists;
|
|
145
|
+
fs.mkdirSync = origMkdir;
|
|
146
|
+
fs.copyFileSync = origCopy;
|
|
147
|
+
fs.chmodSync = origChmod;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
assert.ok(calls.some((c) => c.fn === "copyFileSync" && c.src === "/fake/nano-brain" && c.dst === targetPath), "should copy binary to ~/.local/bin/nano-brain");
|
|
151
|
+
assert.ok(calls.some((c) => c.fn === "chmodSync" && c.p === targetPath && c.mode === 0o755), "should chmod 0755");
|
|
152
|
+
assert.ok(output.some((line) => line.includes(targetPath) && line.includes("PATH")), "should print PATH guidance");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("TestAutoLink_OptIn_MarkerFile_Linux", () => {
|
|
156
|
+
delete process.env.NANO_BRAIN_AUTO_LINK;
|
|
157
|
+
|
|
158
|
+
const HOME = os.homedir();
|
|
159
|
+
const markerPath = path.join(HOME, ".nano-brain", "auto-link");
|
|
160
|
+
const targetDir = path.join(HOME, ".local", "bin");
|
|
161
|
+
const targetPath = path.join(targetDir, "nano-brain");
|
|
162
|
+
const calls = [];
|
|
163
|
+
|
|
164
|
+
const origExists = fs.existsSync;
|
|
165
|
+
const origMkdir = fs.mkdirSync;
|
|
166
|
+
const origCopy = fs.copyFileSync;
|
|
167
|
+
const origChmod = fs.chmodSync;
|
|
168
|
+
|
|
169
|
+
fs.existsSync = (p) => {
|
|
170
|
+
if (p === markerPath) return true;
|
|
171
|
+
if (p === targetPath) return false;
|
|
172
|
+
return origExists(p);
|
|
173
|
+
};
|
|
174
|
+
fs.mkdirSync = (dir, opts) => { calls.push({ fn: "mkdirSync", dir }); };
|
|
175
|
+
fs.copyFileSync = (src, dst) => { calls.push({ fn: "copyFileSync", src, dst }); };
|
|
176
|
+
fs.chmodSync = (p, mode) => { calls.push({ fn: "chmodSync", p, mode }); };
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
tryAutoLink("/fake/nano-brain", "linux");
|
|
180
|
+
} finally {
|
|
181
|
+
fs.existsSync = origExists;
|
|
182
|
+
fs.mkdirSync = origMkdir;
|
|
183
|
+
fs.copyFileSync = origCopy;
|
|
184
|
+
fs.chmodSync = origChmod;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
assert.ok(calls.some((c) => c.fn === "copyFileSync" && c.dst === targetPath), "marker file should trigger copy to " + targetPath);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("TestAutoLink_ExistingTarget_Skipped", (t) => {
|
|
191
|
+
process.env.NANO_BRAIN_AUTO_LINK = "1";
|
|
192
|
+
t.after(() => { delete process.env.NANO_BRAIN_AUTO_LINK; });
|
|
193
|
+
|
|
194
|
+
const HOME = os.homedir();
|
|
195
|
+
const targetDir = path.join(HOME, ".local", "bin");
|
|
196
|
+
const targetPath = path.join(targetDir, "nano-brain");
|
|
197
|
+
|
|
198
|
+
const origExists = fs.existsSync;
|
|
199
|
+
const origCopy = fs.copyFileSync;
|
|
200
|
+
|
|
201
|
+
let copied = false;
|
|
202
|
+
fs.existsSync = (p) => {
|
|
203
|
+
if (p === path.join(HOME, ".nano-brain", "auto-link")) return false;
|
|
204
|
+
if (p === targetPath) return true;
|
|
205
|
+
return origExists(p);
|
|
206
|
+
};
|
|
207
|
+
fs.copyFileSync = () => { copied = true; };
|
|
208
|
+
|
|
209
|
+
const output = [];
|
|
210
|
+
const origLog = console.log;
|
|
211
|
+
console.log = (...args) => output.push(args.join(" "));
|
|
212
|
+
try {
|
|
213
|
+
tryAutoLink("/fake/nano-brain", "linux");
|
|
214
|
+
} finally {
|
|
215
|
+
console.log = origLog;
|
|
216
|
+
fs.existsSync = origExists;
|
|
217
|
+
fs.copyFileSync = origCopy;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
assert.strictEqual(copied, false, "should not copy when target exists");
|
|
221
|
+
assert.ok(output.some((line) => line.includes("WARN") && line.includes("already exists")), "should warn about existing file");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("TestAutoLink_Windows_Skipped", (t) => {
|
|
225
|
+
process.env.NANO_BRAIN_AUTO_LINK = "1";
|
|
226
|
+
t.after(() => { delete process.env.NANO_BRAIN_AUTO_LINK; });
|
|
227
|
+
|
|
228
|
+
const origCopy = fs.copyFileSync;
|
|
229
|
+
let copied = false;
|
|
230
|
+
fs.copyFileSync = () => { copied = true; };
|
|
231
|
+
|
|
232
|
+
const output = [];
|
|
233
|
+
const origLog = console.log;
|
|
234
|
+
console.log = (...args) => output.push(args.join(" "));
|
|
235
|
+
try {
|
|
236
|
+
tryAutoLink("/fake/nano-brain", "win32");
|
|
237
|
+
} finally {
|
|
238
|
+
console.log = origLog;
|
|
239
|
+
fs.copyFileSync = origCopy;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
assert.strictEqual(copied, false, "should not copy on Windows");
|
|
243
|
+
assert.ok(output.some((line) => line.includes("INFO") && line.includes("Windows")), "should print INFO for Windows");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("TestAutoLink_OptOut_Default", () => {
|
|
247
|
+
delete process.env.NANO_BRAIN_AUTO_LINK;
|
|
248
|
+
|
|
249
|
+
const HOME = os.homedir();
|
|
250
|
+
const origExists = fs.existsSync;
|
|
251
|
+
const origCopy = fs.copyFileSync;
|
|
252
|
+
|
|
253
|
+
let copied = false;
|
|
254
|
+
fs.existsSync = (p) => {
|
|
255
|
+
if (p === path.join(HOME, ".nano-brain", "auto-link")) return false;
|
|
256
|
+
return origExists(p);
|
|
257
|
+
};
|
|
258
|
+
fs.copyFileSync = () => { copied = true; };
|
|
259
|
+
|
|
260
|
+
const output = [];
|
|
261
|
+
const origLog = console.log;
|
|
262
|
+
console.log = (...args) => output.push(args.join(" "));
|
|
263
|
+
try {
|
|
264
|
+
tryAutoLink("/fake/nano-brain", "linux");
|
|
265
|
+
} finally {
|
|
266
|
+
console.log = origLog;
|
|
267
|
+
fs.existsSync = origExists;
|
|
268
|
+
fs.copyFileSync = origCopy;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
assert.strictEqual(copied, false, "should not copy when opt-out");
|
|
272
|
+
assert.strictEqual(output.length, 0, "should produce no output when opted out");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("TestAutoLink_CopyFailure_NonFatal", (t) => {
|
|
276
|
+
process.env.NANO_BRAIN_AUTO_LINK = "1";
|
|
277
|
+
t.after(() => { delete process.env.NANO_BRAIN_AUTO_LINK; });
|
|
278
|
+
|
|
279
|
+
const HOME = os.homedir();
|
|
280
|
+
const targetDir = path.join(HOME, ".local", "bin");
|
|
281
|
+
const targetPath = path.join(targetDir, "nano-brain");
|
|
282
|
+
|
|
283
|
+
const origExists = fs.existsSync;
|
|
284
|
+
const origMkdir = fs.mkdirSync;
|
|
285
|
+
const origCopy = fs.copyFileSync;
|
|
286
|
+
|
|
287
|
+
fs.existsSync = (p) => {
|
|
288
|
+
if (p === path.join(HOME, ".nano-brain", "auto-link")) return false;
|
|
289
|
+
if (p === targetPath) return false;
|
|
290
|
+
return origExists(p);
|
|
291
|
+
};
|
|
292
|
+
fs.mkdirSync = () => {};
|
|
293
|
+
fs.copyFileSync = () => { throw new Error("EACCES: permission denied"); };
|
|
294
|
+
|
|
295
|
+
const output = [];
|
|
296
|
+
const origLog = console.log;
|
|
297
|
+
console.log = (...args) => output.push(args.join(" "));
|
|
298
|
+
|
|
299
|
+
assert.doesNotThrow(() => {
|
|
300
|
+
tryAutoLink("/fake/nano-brain", "linux");
|
|
301
|
+
}, "should not throw on copy failure");
|
|
302
|
+
|
|
303
|
+
console.log = origLog;
|
|
304
|
+
fs.existsSync = origExists;
|
|
305
|
+
fs.mkdirSync = origMkdir;
|
|
306
|
+
fs.copyFileSync = origCopy;
|
|
307
|
+
|
|
308
|
+
assert.ok(output.some((line) => line.includes("WARN") && line.includes("failed to auto-link")), "should warn on failure");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("TestAutoLink_MacOS_TargetPath", (t) => {
|
|
312
|
+
process.env.NANO_BRAIN_AUTO_LINK = "1";
|
|
313
|
+
t.after(() => { delete process.env.NANO_BRAIN_AUTO_LINK; });
|
|
314
|
+
|
|
315
|
+
const HOME = os.homedir();
|
|
316
|
+
const targetDir = path.join(HOME, "Library", "nano-brain", "bin");
|
|
317
|
+
const targetPath = path.join(targetDir, "nano-brain");
|
|
318
|
+
const calls = [];
|
|
319
|
+
|
|
320
|
+
const origExists = fs.existsSync;
|
|
321
|
+
const origMkdir = fs.mkdirSync;
|
|
322
|
+
const origCopy = fs.copyFileSync;
|
|
323
|
+
const origChmod = fs.chmodSync;
|
|
324
|
+
|
|
325
|
+
fs.existsSync = (p) => {
|
|
326
|
+
if (p === path.join(HOME, ".nano-brain", "auto-link")) return false;
|
|
327
|
+
if (p === targetPath) return false;
|
|
328
|
+
return origExists(p);
|
|
329
|
+
};
|
|
330
|
+
fs.mkdirSync = (dir, opts) => { calls.push({ fn: "mkdirSync", dir }); };
|
|
331
|
+
fs.copyFileSync = (src, dst) => { calls.push({ fn: "copyFileSync", src, dst }); };
|
|
332
|
+
fs.chmodSync = (p, mode) => { calls.push({ fn: "chmodSync", p, mode }); };
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
tryAutoLink("/fake/nano-brain", "darwin");
|
|
336
|
+
} finally {
|
|
337
|
+
fs.existsSync = origExists;
|
|
338
|
+
fs.mkdirSync = origMkdir;
|
|
339
|
+
fs.copyFileSync = origCopy;
|
|
340
|
+
fs.chmodSync = origChmod;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
assert.ok(calls.some((c) => c.fn === "copyFileSync" && c.dst === targetPath), "should copy to ~/Library/nano-brain/bin/nano-brain");
|
|
344
|
+
assert.ok(calls.some((c) => c.fn === "mkdirSync" && c.dir === targetDir), "should mkdir ~/Library/nano-brain/bin");
|
|
345
|
+
});
|