@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.
@@ -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 };
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nano-step/nano-brain",
3
- "version": "2026.6.306",
3
+ "version": "2026.6.308",
4
4
  "description": "Persistent memory and code intelligence for AI coding agents",
5
5
  "bin": {
6
6
  "nano-brain": "npm/run.js"