@fanchao8609/agent_brain_sync 1.28.0 → 1.28.2

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/bin/abs.js CHANGED
@@ -9,7 +9,7 @@ import { cmdSummon } from '../src/brain/summon-cmd.js';
9
9
  import { cmdIngest } from '../src/brain/ingest.js';
10
10
  import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
11
11
  import { runInstall, runUninstall } from '../src/install.js';
12
- import { readFileSync } from 'node:fs';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
13
  import { join, dirname, resolve } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { spawn } from 'node:child_process';
@@ -64,10 +64,17 @@ async function cmdUpdate({ yes }) {
64
64
  const cmp = cmpVersion(cur, latest);
65
65
  console.log(`当前: ${cur}`);
66
66
  console.log(`最新: ${latest}`);
67
+
67
68
  if (cmp === 0) {
68
- console.log('已是最新,无需升级。');
69
+ // ★ 版本对不代表宿主模板对(2026-11-07 实报)。
70
+ // 现场:以前被双副本的旧代码刷过宿主,版本号已被 npm 推上去,但 ~/.pi/
71
+ // 里仍是旧模板 —— 此时报「已是最新」然后什么都不做,用户以为升级完了,
72
+ // 界面却永远不变。故照样刷新一遍(install 幂等,无副作用)。
73
+ console.log('已是最新,仍刷新一次宿主模板(幂等)。');
74
+ await refreshHosts(yes);
69
75
  return;
70
76
  }
77
+
71
78
  // 本地比 registry 新 —— 典型场景:开发中从源码装过全局,或在改一个未发布的版本。
72
79
  // 此时不能走 npm i -g @latest:那会把本地版**降级**覆盖掉(2026-10-06 实际发生:
73
80
  // 源码 1.16.2 被拉回 registry 的 1.16.1)。只比较 `===` 而没有方向判断,就是这个坑。
@@ -77,6 +84,7 @@ async function cmdUpdate({ yes }) {
77
84
  console.log(' 想把它正式发布: npm publish(在源码目录,发布后 update 即恢复正常)');
78
85
  return;
79
86
  }
87
+
80
88
  console.log(`\n升级 ${cur} → ${latest} …`);
81
89
  const r = await runCmd('npm', ['i', '-g', '@fanchao8609/agent_brain_sync@latest']);
82
90
  if (r === null) {
@@ -84,9 +92,87 @@ async function cmdUpdate({ yes }) {
84
92
  process.exit(1);
85
93
  }
86
94
  console.log(r);
95
+ await refreshHosts(yes);
96
+ }
97
+
98
+ /**
99
+ * 把宿主 hook/skill/编辑器刷新一遍。
100
+ *
101
+ * ★ 必须交给**刚装好的那份新代码**去做,不能用本进程(2026-11-07 实报)。
102
+ *
103
+ * 病根:本进程是旧版 bin/abs.js,通过 import 加载的也是**旧版 src/install.js**。
104
+ * 旧 install.js 的 stableBinPath() 用 process.execPath 推导全局包路径,于是
105
+ * hook 模板里的 @@ABS_BIN@@ 被烧成**旧副本**的路径,模板快照也是旧的。
106
+ * 结果:npm 已升到新版(npm ls -g 显示 1.28.0),但 ~/.pi/ 里写的还是旧模板,
107
+ * 界面一点没变,而且 hook 从此指向旧副本 —— 下一版继续错。
108
+ * 实测(fxh-mb):/usr/bin/abs → /usr/lib/...(1.26.0,PATH 优先)
109
+ * 与 /opt/node.../bin/abs → /opt/...(1.28.0,npm 的 prefix,不在 PATH)。
110
+ *
111
+ * 修法:用 `npm prefix -g` 算出新包的真实路径,把刷新转交给那个新包的
112
+ * bin/abs.js 执行(子进程)。它算 stableBinPath 时会指向自己。
113
+ * 找不到新包才退回本进程的 runInstall(至少不比原来差)。
114
+ */
115
+ async function refreshHosts(yes) {
116
+ const freshEntry = await freshInstallEntry();
87
117
  console.log('\n刷新四宿主 hook/skill(hook 脚本烧的是绝对路径 + 模板快照,升级后必须重装)…');
88
- await runInstall({ agent: undefined, mcp: true, skill: true, yes: yes !== false });
89
- console.log(`\n完成。重启宿主(pi / omp / opencode / claude-code / codex)后新 hook 生效。`);
118
+ if (!freshEntry) {
119
+ console.log(' ⚠ 未定位到新安装的副本,用当前进程刷新(可能仍是旧模板)');
120
+ await runInstall({ agent: undefined, mcp: true, skill: true, yes: yes !== false });
121
+ console.log('\n完成。重启宿主(pi / omp / opencode / claude-code / codex)后新 hook 生效。');
122
+ return;
123
+ }
124
+ if (freshEntry !== resolveBinPath()) console.log(` 用新装的副本执行: ${freshEntry}`);
125
+ // --all:update 的语义是「把升好的版本铺到所有已装宿主」,不能二次弹窗重选。
126
+ // 旧代码调 runInstall({agent: undefined}) 在 TTY 下会弹菜单,回车只装 pi ——
127
+ // 另装过 omp / Claude Code 的机器升级后那几个宿主永远不刷新(2026-11-07 实报)。
128
+ const args = ['install', '--all'];
129
+ if (yes) args.push('--yes');
130
+ const code = await runInherit(process.execPath, [freshEntry, ...args]);
131
+ if (code !== 0) {
132
+ console.error(`\n✗ 刷新宿主失败(退出码 ${code})。手动重跑:node ${freshEntry} install --all`);
133
+ process.exit(1);
134
+ }
135
+ console.log('\n完成。重启宿主(pi / omp / opencode / claude-code / codex)后新 hook 生效。');
136
+ }
137
+
138
+ /** 本进程正在跑的那个 bin/abs.js 的绝对路径(用于判断是否需要转交)。 */
139
+ function resolveBinPath() {
140
+ try { return resolve(fileURLToPath(import.meta.url)); } catch { return ''; }
141
+ }
142
+
143
+ /**
144
+ * 找出**刚升级好的**那份 bin/abs.js。找不到返回 null。
145
+ *
146
+ * 为何不用 ABS_DIR:那正是本进程所在目录 —— 在「PATH 里的 abs 与 npm prefix 不是同一份」
147
+ * 的机器上它就是旧副本(fxh-mb 实测:PATH 里是 /usr/bin/abs 指向 1.26.0,
148
+ * npm 把 1.28.0 装在 /opt/node.../lib/node_modules)。
149
+ */
150
+ async function freshInstallEntry() {
151
+ let pkgName = '';
152
+ try { pkgName = JSON.parse(readFileSync(join(ABS_DIR, 'package.json'), 'utf8')).name || ''; } catch { return null; }
153
+ if (!pkgName) return null;
154
+ // ① npm 自己报告的全局 prefix 最可信 —— 它正是刚 `npm i -g` 写入的位置。
155
+ const prefix = await runCmd('npm', ['prefix', '-g']);
156
+ const cands = [];
157
+ if (prefix) {
158
+ cands.push(join(prefix, 'lib', 'node_modules', pkgName, 'bin', 'abs.js'));
159
+ cands.push(join(prefix, 'node_modules', pkgName, 'bin', 'abs.js'));
160
+ }
161
+ // ② 本进程 node 所在根(nvm 形态与 prefix 一致;跨副本情况靠 ① 兜)
162
+ try { cands.push(join(dirname(dirname(process.execPath)), 'lib', 'node_modules', pkgName, 'bin', 'abs.js')); } catch {}
163
+ for (const c of cands) {
164
+ try { if (existsSync(c)) return c; } catch {}
165
+ }
166
+ return null;
167
+ }
168
+
169
+ /** 跑子进程并把输出直接接到本进程。返回退出码。 */
170
+ function runInherit(cmd, args) {
171
+ return new Promise((resolvePromise) => {
172
+ const c = spawn(cmd, args, { stdio: 'inherit' });
173
+ c.on('error', () => resolvePromise(1));
174
+ c.on('close', (code) => resolvePromise(code === null ? 1 : code));
175
+ });
90
176
  }
91
177
 
92
178
  const [,, cmd, ...rest] = process.argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.28.0",
3
+ "version": "1.28.2",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/install.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // - Codex: ~/.codex/hooks.json。
6
6
  // - OpenCode / Pi: 官方只吃 TS plugin/extension(无 shell-hook 配置) → 初版给出手工指引。
7
7
  // 纪律: 幂等(重复安装=更新)、原子写(tmp+rename)、卸载只删自己装的、写入前备份。
8
+ import { execFileSync } from 'node:child_process';
8
9
  import { promises as fs } from 'node:fs';
9
10
  import * as fsSync from 'node:fs';
10
11
  import { homedir } from 'node:os';
@@ -475,6 +476,62 @@ export function staleBinWarning(binOverride) {
475
476
  ].join('\n');
476
477
  }
477
478
 
479
+ /**
480
+ * 双副本检测:本机是否还装着**另一份**本包?
481
+ *
482
+ * ★ 为何需要(2026-11-07 实报):远程机器有两份全局安装 ——
483
+ * /usr/bin/abs → /usr/lib/node_modules/...(旧)
484
+ * npm prefix → /opt/node.../lib/node_modules/...(新)
485
+ * PATH 里 `/usr/bin` 在前,于是 `abs update` 跑的是旧副本;它用自己那份旧
486
+ * install.js 刷宿主,把旧模板和旧 bin 路径写进 ~/.pi/,界面一点不变。
487
+ *
488
+ * 这里只做**检测与报告**,不自动改 PATH —— 改用户 shell 配置是越权动作。
489
+ * 正确修法在 bin/abs.js 的 cmdUpdate 里(把刷新动作转交给 npm prefix 下的新副本);
490
+ * 本函数负责让「装完之后仍然指错」这件事在输出里可见,而不是静默。
491
+ *
492
+ * @returns {string|null} 有另一份时返回提示,否则 null。
493
+ */
494
+ export function duplicateInstallWarning() {
495
+ let pkgName = '';
496
+ try {
497
+ pkgName = JSON.parse(fsSync.readFileSync(join(ABS_DIR, 'package.json'), 'utf8')).name || '';
498
+ } catch { return null; }
499
+ if (!pkgName) return null;
500
+
501
+ // ★ 只关心「两份都是全局安装」的情形。
502
+ // 从开发仓库直接跑 install(self 不在 node_modules 下)是正常的开发流程,
503
+ // 此时与全局那份版本不同是预期的 —— 那不是双副本故障,报出来只会是噪声
504
+ // (2026-11-07 自测时先误报了一次)。
505
+ if (!/[\\/]node_modules[\\/]/.test(ABS_DIR)) return null;
506
+
507
+ let npmRoot;
508
+ try {
509
+ npmRoot = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf8' }).trim();
510
+ } catch { return null; }
511
+ if (!npmRoot) return null;
512
+
513
+ // npm prefix 下的那份(「正确」的那份)与本进程所在那份(可能正是旧副本)
514
+ const correct = [join(npmRoot, 'lib', 'node_modules', pkgName), join(npmRoot, 'node_modules', pkgName)]
515
+ .find((x) => fsSync.existsSync(x));
516
+ if (!correct) return null;
517
+ const self = ABS_DIR;
518
+ try {
519
+ if (fsSync.realpathSync(correct) === fsSync.realpathSync(self)) return null; // 同一份,正常
520
+ } catch { return null; }
521
+
522
+ let correctVer = '?', selfVer = '?';
523
+ try { correctVer = JSON.parse(fsSync.readFileSync(join(correct, 'package.json'), 'utf8')).version || '?'; } catch {}
524
+ try { selfVer = JSON.parse(fsSync.readFileSync(join(self, 'package.json'), 'utf8')).version || '?'; } catch {}
525
+
526
+ return [
527
+ '⚠ 本机存在两份 abs 全局安装,本次安装用的是:',
528
+ ` ${self} (v${selfVer})`,
529
+ ' npm 装的那份在:',
530
+ ` ${correct} (v${correctVer})`,
531
+ ` 修法: 用新那份重跑 —— node ${join(correct, 'bin', 'abs.js')} install --all`,
532
+ ].join('\n');
533
+ }
534
+
478
535
  function agentsSkillWarning() {
479
536
  const p = join(homedir(), '.agents', 'skills', 'abs-agent-brain-sync', 'SKILL.md');
480
537
  if (!fsSync.existsSync(p)) return null;
@@ -1005,6 +1062,8 @@ export async function runInstall({ agent, all = false, mcp = true, skill = true,
1005
1062
  }
1006
1063
  const stale = staleBinWarning();
1007
1064
  if (stale) console.log('\n' + stale);
1065
+ const dup = duplicateInstallWarning();
1066
+ if (dup) console.log('\n' + dup);
1008
1067
 
1009
1068
  console.log('\n完成。项目内运行 abs init 建图谱; 会话里说 "abs load" 续接。');
1010
1069
  }