@jspg-ai/coding-bb 0.0.2-beta.28 → 0.0.3-beta.1

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/cbb/bin/cbbm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // 内部命令入口:cbbm init / check-update / wiki / wp(由流程、hook、Agent 技能调用)
4
+ // 内部命令入口:cbbm check-update / wiki / wp(由流程、hook、Agent 技能调用)
5
5
  process.env.CBB_ENTRY = 'cbbm';
6
6
  require('../lib/install/init.js');
@@ -99,6 +99,19 @@ trigger: always_on
99
99
 
100
100
  **检验标准:** 用户从未因"AI 直接改了代码没打招呼"而感到意外。
101
101
 
102
+ ## 8. 业务空间目录纪律
103
+
104
+ **仅当工作目录是业务空间根(存在 `workspace-config.json`)时生效:写操作命令必须落在需求工作树内。**
105
+
106
+ 业务空间根(主分支)只负责组织与调度,不做需求开发。接到需求先建工作树(`/cbb:worktree-init` 或 `/cbb-worktree-init`),之后:
107
+
108
+ - 一切产生写操作的命令(git commit / openspec / 构建 / 测试 / 文件修改)必须以 `.worktrees/worktree-<需求名>/`(或其应用子目录)为工作目录:显式 `cd` 或绝对路径
109
+ - 禁止在空间根执行写操作:不在主分支提交代码、不在空间根创建 openspec 变更产物、不改 `.codespace/` 基准代码
110
+ - 只读命令(git status / log、查看文件)在空间根执行无妨
111
+ - 会话开始时确认本次需求对应的工作树路径;存在多个未完成工作树时,先与用户确认目标,不要猜
112
+
113
+ **检验标准:** 空间根主分支的 `git status` 永远干净(除工具维护的配置文件外),openspec 变更产物只出现在需求工作树内。
114
+
102
115
  ---
103
116
 
104
117
  **这些准则生效的标志:** diff 中不必要的改动减少、不会因过度设计而重写、澄清问题在实现前而非出错后提出。
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * 使用方式:
10
10
  * npm install -g @jspg-ai/coding-bb # 首次使用
11
- * npm install -g @jspg-ai/coding-bb
12
- * cd your-project && cbbm init
11
+ * cbb setup # 初始化业务空间(空间根全量安装)
12
+ * cbb update # 就地升级已安装目录
13
13
  */
14
14
 
15
15
  const fs = require('fs');
@@ -23,9 +23,24 @@ const yaml = require('js-yaml');
23
23
  // 异步执行外部命令:长耗时 git 操作不阻塞事件循环,进度行才能持续刷新
24
24
  const execFileAsync = promisify(execFile);
25
25
 
26
+ /**
27
+ * 跨平台异步执行命令(不阻塞事件循环)。
28
+ * Windows 上 npm / openspec 均为 .cmd:Node 直接 spawn .cmd 会报 EINVAL,
29
+ * `shell: true` 又会触发 DEP0190 弃用警告;故统一经 cmd.exe /d /s /c 执行。
30
+ * 仅用于本文件内常量命令(不接受用户输入拼接)。
31
+ */
32
+ function execCmd(file, args, opts = {}) {
33
+ if (process.platform === 'win32') {
34
+ const line = [file, ...args].join(' ');
35
+ return execFileAsync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', line],
36
+ { windowsHide: true, ...opts });
37
+ }
38
+ return execFileAsync(file, args, opts);
39
+ }
40
+
26
41
  const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
27
42
 
28
- // 目标项目根目录:默认取当前 cwd,init() / initWorkspace() 可在调用前覆盖。
43
+ // 目标项目根目录:默认取当前 cwd,init() 可在调用前覆盖。
29
44
  // 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
30
45
  // 切到仓库根,再复用 init() 的完整安装流程,而无需把 TARGET_DIR 透传到所有 helper。
31
46
  let TARGET_DIR = process.cwd();
@@ -53,13 +68,13 @@ const SUPERPOWERS_WHITELIST = [
53
68
  'test-driven-development', // TDD 计划结构 + TDD Evidence 方法论基底
54
69
  ];
55
70
 
56
- // worktree 扩展 skills 部署到沙箱的白名单:init 只在 workspace 根执行
57
- // (由 installWorkspaceConfig 部署),沙箱内只需 push(推送)与 close(关闭)
58
- const SANDBOX_WORKTREE_SKILLS = ['cbb-worktree-push', 'cbb-worktree-close'];
71
+ // worktree 扩展 skills 部署白名单:init / close / push 三件套
72
+ // (单层安装:业务空间根是唯一安装点,worktree 管理入口 init 同样部署在根)
73
+ const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
59
74
 
60
- // 装到沙箱的 tools skills:仅 cbb-design-to-wiki(openspec 设计阶段动作)。
75
+ // 装到空间的 tools skills:仅 cbb-design-to-wiki(openspec 设计阶段动作)。
61
76
  // cbb-wiki-ops 当前不分发(保留在源仓库,有用户需要时再加入 USER_LEVEL_SKILLS);Java 工具不装。
62
- const SANDBOX_TOOLS_SKILLS = ['cbb-design-to-wiki'];
77
+ const TOOLS_SKILLS = ['cbb-design-to-wiki'];
63
78
 
64
79
  // settings.json hook 工具
65
80
  const settingsManager = require('../utils/settings');
@@ -94,6 +109,18 @@ function detectExistingInstall() {
94
109
  return fs.existsSync(path.join(TARGET_DIR, MANIFEST_FILE));
95
110
  }
96
111
 
112
+ /**
113
+ * 判定目标目录是否为业务空间根:workspace-config.json 存在且 .git 是目录。
114
+ * 需求工作树内也有 workspace-config.json(tracked 副本),但其 .git 是文件(gitdir 指针),
115
+ * 必须以 .git 形态排除——避免「在 worktree 里误跑 cbb update」被当作空间根
116
+ * (误装 worktree 管理入口、在错误位置报旧导航提示)。
117
+ */
118
+ function detectWorkspaceRoot() {
119
+ if (!fs.existsSync(path.join(TARGET_DIR, 'workspace-config.json'))) return false;
120
+ const gitPath = path.join(TARGET_DIR, '.git');
121
+ return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
122
+ }
123
+
97
124
  // ── 清单管理 ──────────────────────────────────────────────
98
125
 
99
126
  /** 读取清单,返回 Set<path> */
@@ -211,7 +238,7 @@ function ask(question) {
211
238
  output: process.stdout,
212
239
  });
213
240
  return new Promise(resolve => {
214
- rl.question(question, answer => {
241
+ rl.question(' ' + question, answer => {
215
242
  rl.close();
216
243
  resolve(answer.trim());
217
244
  });
@@ -623,22 +650,29 @@ const OPENSPEC_REGISTRY = 'https://registry.npmmirror.com';
623
650
  /**
624
651
  * 确保 OpenSpec CLI 已安装且为最新版本。
625
652
  * 未安装或版本非最新时自动执行 npm install -g,无需用户确认。
653
+ * 命令异步执行(execFile)避免阻塞事件循环——否则长耗时的 npm 查询/安装
654
+ * 会让进度行冻结、看起来像卡死;传入 onProgress 时由调用方渲染实时进度,
655
+ * 否则退化为安装/升级前的一行灰字提示。
626
656
  */
627
- async function ensureOpenSpecCli() {
657
+ async function ensureOpenSpecCli(onProgress = null) {
658
+ const report = text => { if (onProgress) onProgress(text); };
659
+
628
660
  // 1. 获取当前版本(未安装时 currentVersion = null)
629
661
  let currentVersion = null;
662
+ report('检查 OpenSpec CLI');
630
663
  try {
631
- currentVersion = execSync('openspec --version', { stdio: 'pipe', timeout: 10000 })
632
- .toString().trim().replace(/^v/, '');
664
+ const { stdout } = await execCmd('openspec', ['--version'], { timeout: 10000 });
665
+ currentVersion = stdout.toString().trim().replace(/^v/, '');
633
666
  } catch (_) {}
634
667
 
635
668
  // 2. 查询最新版本(网络失败则跳过检测,视为无法判断)
636
669
  let latestVersion = null;
637
670
  try {
638
- latestVersion = execSync(
639
- `npm view ${OPENSPEC_PKG} version --registry ${OPENSPEC_REGISTRY}`,
640
- { stdio: 'pipe', timeout: 15000 }
641
- ).toString().trim();
671
+ const { stdout } = await execCmd(
672
+ 'npm', ['view', OPENSPEC_PKG, 'version', '--registry', OPENSPEC_REGISTRY],
673
+ { timeout: 15000 }
674
+ );
675
+ latestVersion = stdout.toString().trim();
642
676
  } catch (_) {}
643
677
 
644
678
  // 3. 判断是否需要安装/升级
@@ -650,17 +684,20 @@ async function ensureOpenSpecCli() {
650
684
  return { state: 'ok', version: currentVersion };
651
685
  }
652
686
 
653
- // 4. 执行安装/升级(耗时操作,打印进行中提示)
687
+ // 4. 执行安装/升级(耗时操作:由调用方进度行或灰字提示覆盖全程)
654
688
  const action = needInstall ? '安装' : '升级';
655
689
  const detail = needInstall
656
690
  ? `→ v${latestVersion || 'latest'}`
657
691
  : `v${currentVersion} → v${latestVersion}`;
658
- console.log(` ${c('gray', '⏳')} ${c('gray', `${action} OpenSpec CLI(${detail})...`)}`);
692
+ report(`${action} OpenSpec CLI(${detail})`);
693
+ if (!onProgress) {
694
+ console.log(` ${c('gray', '⏳')} ${c('gray', `${action} OpenSpec CLI(${detail})...`)}`);
695
+ }
659
696
 
660
697
  try {
661
- execSync(
662
- `npm install -g ${OPENSPEC_PKG}@latest --registry ${OPENSPEC_REGISTRY}`,
663
- { stdio: 'pipe', timeout: 120000 }
698
+ await execCmd(
699
+ 'npm', ['install', '-g', `${OPENSPEC_PKG}@latest`, '--registry', OPENSPEC_REGISTRY],
700
+ { timeout: 120000 }
664
701
  );
665
702
  return { state: needInstall ? 'installed' : 'upgraded', version: latestVersion || currentVersion };
666
703
  } catch (err) {
@@ -776,7 +813,7 @@ async function detectUserLevelSuperpowers(selectedAdapters, yes = false) {
776
813
  // ── 主流程 ────────────────────────────────────────────────
777
814
 
778
815
  async function init(targetDir, options = {}) {
779
- const { yes = false } = options;
816
+ const { yes = false, selectedAdapters: presetAdapters = null } = options;
780
817
 
781
818
  // 允许调用方指定目标目录(cbb setup 在拉好仓库后切到仓库根调用)。
782
819
  // 不传则保持模块级 TARGET_DIR(即调用 init 时的 cwd)。
@@ -787,19 +824,41 @@ async function init(targetDir, options = {}) {
787
824
  const action = isUpgrade ? '升级' : '安装';
788
825
  const managedPaths = readManifest();
789
826
  const newPaths = new Set();
827
+ const isWorkspaceRoot = detectWorkspaceRoot();
790
828
 
791
829
  console.log(`\n 🚀 ${action} @jspg-ai/coding-bb...\n`);
792
830
 
793
831
  // 先选适配器:后续的前置依赖检测仅针对所选适配器,避免误导(如选了 Qoder 却提示安装 Claude Code 的 Superpowers)
794
832
  // 非交互模式判断:显式 -y/--yes 或 stdin 非 TTY(pipe/ignore)均视为非交互
833
+ // 选择优先级:调用方预设(cbb setup)> .last-tools(非交互沿用)> 交互选择;无记录才回退全选
795
834
  const isNonInteractive = yes || !process.stdin.isTTY;
796
- if (yes && process.stdin.isTTY) {
797
- log('--yes 非交互模式,已默认选中所有适配器', 'info');
835
+ let selectedAdapters;
836
+ if (presetAdapters && presetAdapters.length > 0) {
837
+ selectedAdapters = presetAdapters;
838
+ } else if (isNonInteractive) {
839
+ const lastTools = readLastTools();
840
+ const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
841
+ selectedAdapters = reused.length > 0 ? reused : adapters;
842
+ if (yes && process.stdin.isTTY) {
843
+ log(reused.length > 0
844
+ ? `--yes 非交互模式,沿用上次工具选择:${reused.map(a => a.displayName).join('、')}`
845
+ : '--yes 非交互模式,无上次选择记录,已默认选中所有适配器', 'info');
846
+ }
847
+ } else {
848
+ selectedAdapters = await selectTools(isUpgrade);
849
+ }
850
+ // 业务空间根记录本次选择,供 cbb update 等非交互场景沿用
851
+ if (isWorkspaceRoot && presetAdapters && presetAdapters.length > 0) {
852
+ saveLastTools(selectedAdapters);
798
853
  }
799
- const selectedAdapters = isNonInteractive ? adapters : await selectTools(isUpgrade);
800
854
 
801
855
  // 前置依赖检测(放在选适配器之后,只检测与所选工具相关的依赖)
802
- await ensureOpenSpecCli();
856
+ const depProgress = progress('依赖');
857
+ try {
858
+ await ensureOpenSpecCli(t => depProgress.update(t));
859
+ } finally {
860
+ depProgress.done();
861
+ }
803
862
 
804
863
  // 分发核心规则到各工具目录
805
864
  console.log(`\n 📦 ${action}核心编码规范...`);
@@ -834,14 +893,14 @@ async function init(targetDir, options = {}) {
834
893
  }
835
894
  // 官方 OpenSpec Skills(openspec/skills/)不部署——工作流入口统一为 /opsx:* 斜杠命令
836
895
  //(openspec/commands/ 在下方 Commands 段部署),避免双形态重复
837
- // worktree 扩展 Skills:沙箱内只需 push / close(init workspace 根的子集安装)
838
- const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, SANDBOX_WORKTREE_SKILLS);
896
+ // worktree 扩展 Skills:init / close / push 三件套(单层安装,空间根是唯一安装点)
897
+ const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, WORKTREE_SKILLS);
839
898
  if (osExtResult.count > 0) {
840
899
  osCount = osExtResult.count;
841
900
  osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
842
901
  }
843
902
  // 工具 Skills:仅 cbb-design-to-wiki(设计文档发布属 openspec 设计阶段动作)
844
- const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, SANDBOX_TOOLS_SKILLS);
903
+ const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, TOOLS_SKILLS);
845
904
  if (extResult.count > 0) {
846
905
  extCount = extResult.count;
847
906
  extResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
@@ -872,14 +931,13 @@ async function init(targetDir, options = {}) {
872
931
  await detectUserLevelSuperpowers(selectedAdapters, yes);
873
932
  }
874
933
 
875
- // 安装 Commands(官方 12 个 → /opsx:*;worktree 管理 → /cbb:worktree-*,
876
- // 仅装 close/push,init 只在 workspace 根使用,由 installWorkspaceConfig 部署)
934
+ // 安装 Commands(官方 12 个 → /opsx:*;worktree 管理 → /cbb:worktree-*,init/close/push 三件套)
877
935
  const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
878
936
  if (commandsAdapters.length > 0) {
879
937
  console.log(`\n ⚡ ${action} Commands...`);
880
938
  const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
881
939
  const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
882
- const cmdFilter = ['worktree-close.md', 'worktree-push.md'];
940
+ const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
883
941
 
884
942
  const cmdDests = [...new Set(commandsAdapters
885
943
  .flatMap(a => [a.commandsDir, a.worktreeCommandsDir])
@@ -918,6 +976,15 @@ async function init(targetDir, options = {}) {
918
976
  setupOpenSpec(isUpgrade, managedPaths);
919
977
  newPaths.add('openspec/config.yaml');
920
978
 
979
+ // 业务空间根专属:空间导航 AGENTS.md(仅缺失时生成,永不覆盖)+ 旧版导航文案提示
980
+ let agentsMdState = null;
981
+ if (isWorkspaceRoot) {
982
+ agentsMdState = workspaces.ensureWorkspaceAgentsMd(TARGET_DIR);
983
+ if (workspaces.detectLegacyAgentsMd(TARGET_DIR)) {
984
+ log('空间根 AGENTS.md 仍是旧版导航文案(单层安装改造前模板)。建议删除该文件后重跑 cbb update 重新生成(仅缺失时生成,不会覆盖你的增补内容)', 'warn');
985
+ }
986
+ }
987
+
921
988
  // 写入自动版本检查 hook
922
989
  const hookAdapters = selectedAdapters.filter(a => a.settingsFile && a.hookEvents);
923
990
  if (hookAdapters.length > 0) {
@@ -943,7 +1010,7 @@ async function init(targetDir, options = {}) {
943
1010
  if (isUpgrade) {
944
1011
  const oldPaths = pathsToDelete(managedPaths, newPaths);
945
1012
  if (oldPaths.length > 0) {
946
- section('🧹', `清理上一版本残留(${oldPaths.length} 个)`);
1013
+ section('!', `清理上一版本残留(${oldPaths.length} 个)`);
947
1014
  // 先全部删除
948
1015
  oldPaths.forEach(oldPath => {
949
1016
  const targetPath = path.join(TARGET_DIR, oldPath);
@@ -959,7 +1026,7 @@ async function init(targetDir, options = {}) {
959
1026
 
960
1027
  const kept = protectedKeptPaths(managedPaths, newPaths);
961
1028
  if (kept.length > 0) {
962
- section('🛡️', '保留用户文档');
1029
+ section('!', '保留用户文档');
963
1030
  kept.forEach(k => {
964
1031
  log(` ${k}(本包已不再管理,文件可能含用户内容,未删除)`, 'warn');
965
1032
  });
@@ -985,7 +1052,7 @@ async function init(targetDir, options = {}) {
985
1052
  console.log(`\n ✅ ${action}完成!\n`);
986
1053
 
987
1054
  if (yes) {
988
- console.log(c('dim', ' (使用 --yes 非交互模式,已默认选中所有适配器)'));
1055
+ console.log(c('dim', ` (使用 --yes 非交互模式,工具:${selectedAdapters.map(a => a.displayName).join('、')})`));
989
1056
  }
990
1057
 
991
1058
  // 适配器映射(单行)
@@ -1015,148 +1082,13 @@ async function init(targetDir, options = {}) {
1015
1082
  if (hasSkills) extras.push('Skills');
1016
1083
  if (hasCommands) extras.push('Commands');
1017
1084
  if (hasHooks) extras.push('版本检查 hook');
1085
+ if (agentsMdState === 'created') extras.push('AGENTS.md(空间导航)');
1018
1086
  if (extras.length > 0) {
1019
1087
  console.log(` ${extras.join(' / ')} 已配置`);
1020
1088
  }
1021
1089
  console.log('');
1022
1090
  }
1023
1091
 
1024
- /**
1025
- * 为 workspace 根目录安装 AI 配置子集(由 cbb setup 调用)。
1026
- *
1027
- * 只装 worktree 生命周期管理(init/close/push)+ openspec 运行时配置 + 空间导航
1028
- * AGENTS.md(仅缺失时生成,永不覆盖),不装 base 工作流、编码规范、superpowers、
1029
- * 版本检查 hook--这些属于 sandbox worktree 内的开发配置,由 worktree 内
1030
- * `cbbm init --yes` 完整模式安装。在 workspace 根目录
1031
- * (主分支)装 base 工作流会诱导绕过 worktree 直接在主分支做需求开发。
1032
- */
1033
- async function installWorkspaceConfig(targetDir, options = {}) {
1034
- const { yes = false, selectedAdapters: presetAdapters = null } = options;
1035
- if (targetDir) TARGET_DIR = targetDir;
1036
-
1037
- const isUpgrade = detectExistingInstall();
1038
- const managedPaths = readManifest();
1039
- const newPaths = new Set();
1040
-
1041
- let selectedAdapters;
1042
- if (presetAdapters && presetAdapters.length > 0) {
1043
- // cbb setup 场景:上层已交互选定适配器,持久化供 cbb update 预选
1044
- selectedAdapters = presetAdapters;
1045
- saveLastTools(selectedAdapters);
1046
- } else {
1047
- const isNonInteractive = yes || !process.stdin.isTTY;
1048
- if (isNonInteractive) {
1049
- // 非交互(cbb update / 管道运行):沿用上次工具选择,避免误装未使用的适配器
1050
- const lastTools = readLastTools();
1051
- const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
1052
- if (reused.length > 0) {
1053
- selectedAdapters = reused;
1054
- } else {
1055
- selectedAdapters = adapters;
1056
- }
1057
- } else {
1058
- selectedAdapters = await selectTools(isUpgrade);
1059
- }
1060
- }
1061
- const openspecCli = await ensureOpenSpecCli();
1062
-
1063
- // 只装 worktree 生命周期管理 Skills(init/close/push)
1064
- const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
1065
- const commandsAdapters = selectedAdapters.filter(a => a.worktreeCommandsDir);
1066
- let skillCount = 0;
1067
- let cmdCount = 0;
1068
- let adapterRoots = [];
1069
-
1070
- if (skillsAdapters.length > 0) {
1071
- const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
1072
- const osExtFilter = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
1073
- skillsAdapters.forEach(adapter => {
1074
- const r = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, osExtFilter);
1075
- if (r.count > 0) {
1076
- skillCount = Math.max(skillCount, r.count);
1077
- r.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1078
- }
1079
- });
1080
- adapterRoots = skillsAdapters.map(a => a.skillsDir.split('/')[0]);
1081
- }
1082
-
1083
- // 只装 worktree 管理 Commands(cbb:worktree-init/close/push)
1084
- if (commandsAdapters.length > 0) {
1085
- const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
1086
- const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
1087
- commandsAdapters.forEach(adapter => {
1088
- const r = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
1089
- if (r.count > 0) {
1090
- cmdCount = Math.max(cmdCount, r.count);
1091
- r.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
1092
- }
1093
- });
1094
- if (adapterRoots.length === 0) adapterRoots = commandsAdapters.map(a => a.worktreeCommandsDir.split('/')[0]);
1095
- }
1096
-
1097
- // 设置 OpenSpec 工作流(config.yaml + schemas)
1098
- const schemasSrcs = [
1099
- path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
1100
- ];
1101
- schemasSrcs.forEach(src => {
1102
- if (fs.existsSync(src)) {
1103
- fs.readdirSync(src).filter(f2 => fs.statSync(path.join(src, f2)).isDirectory())
1104
- .forEach(name => newPaths.add(`openspec/schemas/${name}`));
1105
- }
1106
- });
1107
- const openspecRes = setupOpenSpec(isUpgrade, managedPaths);
1108
- newPaths.add('openspec/config.yaml');
1109
-
1110
- // 空间导航文件 AGENTS.md:仅缺失时生成,永不覆盖(不登记清单,卸载不删)
1111
- const agentsMdState = workspaces.ensureWorkspaceAgentsMd(TARGET_DIR);
1112
-
1113
- // 升级时清理旧路径(workspace 曾装过的 base 工作流等不属于此子集,删除)
1114
- const cleaned = [];
1115
- const keptDocs = [];
1116
- if (isUpgrade) {
1117
- const oldPaths = pathsToDelete(managedPaths, newPaths);
1118
- oldPaths.forEach(oldPath => {
1119
- const targetPath = path.join(TARGET_DIR, oldPath);
1120
- if (fs.existsSync(targetPath)) {
1121
- removePath(targetPath);
1122
- cleaned.push(oldPath);
1123
- }
1124
- });
1125
- keptDocs.push(...protectedKeptPaths(managedPaths, newPaths));
1126
- (skillsAdapters || []).forEach(adapter => {
1127
- const legacyNodeModules = path.join(TARGET_DIR, adapter.skillsDir, 'node_modules');
1128
- if (fs.existsSync(legacyNodeModules)) {
1129
- removePath(legacyNodeModules);
1130
- cleaned.push(`${adapter.skillsDir}/node_modules`);
1131
- }
1132
- });
1133
- }
1134
-
1135
- writeManifest(newPaths, selectedAdapters);
1136
- updateGitignore();
1137
-
1138
- // 汇总输出(一行主结果 + 可选清理/保留明细)
1139
- const rootLabel = [...new Set(adapterRoots)].join('/');
1140
- const parts = [];
1141
- if (skillCount > 0) parts.push(`${rootLabel} skills×${skillCount}`);
1142
- if (cmdCount > 0) parts.push(`commands×${cmdCount}`);
1143
- if (openspecRes.installed.length > 0) {
1144
- parts.push(`openspec ${openspecRes.defaultSchema || openspecRes.installed.join('/')}`);
1145
- }
1146
- if (agentsMdState === 'created') parts.push('AGENTS.md(空间导航)');
1147
- const configLabel = 'AI 配置';
1148
- step('✓', configLabel, parts.join(' · '));
1149
- if (cleaned.length > 0) {
1150
- const MAX_DISPLAY = 3;
1151
- const preview = cleaned.slice(0, MAX_DISPLAY).join('、');
1152
- step('🧹', '清理残留', `${cleaned.length} 个(${preview}${cleaned.length > MAX_DISPLAY ? ' 等' : ''})`,
1153
- { iconColor: 'yellow' });
1154
- }
1155
- if (keptDocs.length > 0) {
1156
- step('🛡️', '保留文档', `${keptDocs.join('、')}(本包已不再管理)`, { iconColor: 'yellow' });
1157
- }
1158
- }
1159
-
1160
1092
  // ── cbb setup 命令 ──────────────────────────────────────
1161
1093
 
1162
1094
  /**
@@ -1341,9 +1273,8 @@ async function syncWorkspaceRepo(targetDir, onProgress = () => {}) {
1341
1273
  if (currentBranch !== defaultBranch) {
1342
1274
  log(`本地当前分支 ${currentBranch} ≠ 远程默认分支 ${defaultBranch},正在切换...`, 'warn');
1343
1275
  try {
1344
- execSync(`git checkout -q "${defaultBranch}"`, {
1276
+ await execFileAsync('git', ['checkout', '-q', defaultBranch], {
1345
1277
  cwd: targetDir,
1346
- stdio: 'pipe',
1347
1278
  timeout: 30000,
1348
1279
  });
1349
1280
  } catch (e) {
@@ -1355,9 +1286,8 @@ async function syncWorkspaceRepo(targetDir, onProgress = () => {}) {
1355
1286
 
1356
1287
  // 4. fast-forward merge 到 origin/<默认分支>
1357
1288
  try {
1358
- execSync(`git merge --ff-only -q "origin/${defaultBranch}"`, {
1289
+ await execFileAsync('git', ['merge', '--ff-only', '-q', `origin/${defaultBranch}`], {
1359
1290
  cwd: targetDir,
1360
- stdio: 'pipe',
1361
1291
  timeout: 60000,
1362
1292
  });
1363
1293
  } catch (e) {
@@ -1480,9 +1410,9 @@ async function initSingleWorkspace(w, opts = {}) {
1480
1410
  return { ok: true, subdir: subdirName, upToDate: true };
1481
1411
  }
1482
1412
 
1483
- // 6. 安装 workspace AI 配置子集(worktree 管理技能/命令 + openspec 配置,不含 base 工作流)
1413
+ // 6. 安装空间 AI 配置(单层全量:开发配置 + worktree 管理,统一安装器自动识别空间根)
1484
1414
  try {
1485
- await installWorkspaceConfig(targetDir, { yes: true, selectedAdapters: opts.selectedAdapters });
1415
+ await init(targetDir, { yes: true, selectedAdapters: opts.selectedAdapters });
1486
1416
  return { ok: true, subdir: subdirName };
1487
1417
  } catch (e) {
1488
1418
  step('✗', 'AI 配置', e.message, { iconColor: 'red', labelColor: 'red', valueColor: 'red' });
@@ -1515,7 +1445,7 @@ function printBatchSummary(results) {
1515
1445
  }
1516
1446
  step('!', '完成', `成功 ${ok.length}/${results.length},失败项见下`, { iconColor: 'yellow', valueColor: 'yellow' });
1517
1447
  failed.forEach(r => {
1518
- step('✗', r.w.name, describeBatchError(r), { iconColor: 'red', labelColor: 'red', valueColor: 'red' });
1448
+ step('✗', '失败', `${r.w.name}:${describeBatchError(r)}`, { iconColor: 'red', labelColor: 'red', valueColor: 'red' });
1519
1449
  });
1520
1450
  }
1521
1451
 
@@ -1609,7 +1539,6 @@ async function setupWorkspace() {
1609
1539
  const result = await initSingleWorkspace(w, { selectedAdapters });
1610
1540
  results.push({ w, ...result });
1611
1541
  }
1612
- console.log('');
1613
1542
  printBatchSummary(results);
1614
1543
  }
1615
1544
 
@@ -1620,11 +1549,11 @@ function printSetupSummary(selected, selectedAdapters, parentDir) {
1620
1549
  console.log(' ' + c('bold', 'cbb setup') + ' ' + c('gray', `v${pkg.version}`));
1621
1550
  console.log('');
1622
1551
  selected.forEach(w => {
1623
- console.log(` ${c('yellow', '[' + w.businessLine + ']')} ${w.name}`);
1624
- console.log(` ${c('gray', w.repo)}`);
1552
+ console.log(` ${c('yellow', '[' + w.businessLine + ']')} ${w.name}`);
1553
+ console.log(` ${c('gray', w.repo)}`);
1625
1554
  });
1626
- console.log(' ' + c('gray', padVisual('目标目录', 10)) + ' ' + c('cyan', parentDir));
1627
- console.log(' ' + c('gray', padVisual('AI 工具', 10)) + ' ' + selectedAdapters.map(a => a.displayName).join('、'));
1555
+ console.log(' ' + c('gray', padVisual('目标目录', 10)) + ' ' + c('cyan', parentDir));
1556
+ console.log(' ' + c('gray', padVisual('AI 工具', 10)) + ' ' + selectedAdapters.map(a => a.displayName).join('、'));
1628
1557
  }
1629
1558
 
1630
1559
  // ── 用户级组件 ────────────────────────────────────────────
@@ -1693,13 +1622,16 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
1693
1622
  }
1694
1623
 
1695
1624
  /**
1696
- * cbb update:按现有安装模式升级当前目录内容 + 用户级组件。
1625
+ * cbb update:就地升级当前目录内容 + 用户级组件。
1697
1626
  *
1698
- * 安装模式判定:清单含 rules 路径 → 完整模式(worktree 沙箱/单项目),
1699
- * 否则为 workspace 子集模式(空间根,只装 worktree 管理 + openspec 配置)。
1627
+ * 单层安装:统一安装器按空间根判定(workspace-config.json + .git 目录)补齐
1628
+ * worktree 管理入口与空间导航;旧子集清单升级时由清单差集机制自动补齐全量内容。
1700
1629
  */
1701
1630
  async function updateInstalled() {
1702
- await setupUserLevelComponents();
1631
+ // 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量
1632
+ const lastTools = readLastTools();
1633
+ const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
1634
+ await setupUserLevelComponents(reused.length > 0 ? reused : adapters);
1703
1635
 
1704
1636
  const manifest = readManifest();
1705
1637
  if (manifest.size === 0) {
@@ -1707,17 +1639,7 @@ async function updateInstalled() {
1707
1639
  process.exit(1);
1708
1640
  }
1709
1641
 
1710
- const isFullInstall = [...manifest].some(p =>
1711
- adapters.some(a => p.startsWith(`${a.rulesDir}/`))
1712
- );
1713
-
1714
- if (isFullInstall) {
1715
- log('检测到完整模式安装,升级开发配置(rules + skills + commands + 工作流)...', 'info');
1716
- await init(undefined, {});
1717
- } else {
1718
- log('检测到 workspace 子集模式,升级 worktree 管理 + openspec 配置...', 'info');
1719
- await installWorkspaceConfig(undefined, {});
1720
- }
1642
+ await init(undefined, {});
1721
1643
  }
1722
1644
 
1723
1645
  // ── 卸载 ──────────────────────────────────────────────────
@@ -1822,7 +1744,7 @@ async function uninstall() {
1822
1744
  const managedPaths = readManifest();
1823
1745
 
1824
1746
  // 确认卸载
1825
- console.log('\n🗑 卸载 @jspg-ai/coding-bb...\n');
1747
+ console.log('\n 🗑 卸载 @jspg-ai/coding-bb...\n');
1826
1748
  const answer = await ask('确认卸载?此操作将移除本包管理的所有文件 (y/N): ');
1827
1749
  if (answer.toLowerCase() !== 'y') {
1828
1750
  log('已取消卸载');
@@ -1917,7 +1839,7 @@ async function uninstall() {
1917
1839
 
1918
1840
  // 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
1919
1841
  const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
1920
- const INTERNAL_COMMANDS = ['init', 'check-update', 'wiki', 'wp'];
1842
+ const INTERNAL_COMMANDS = ['check-update', 'wiki', 'wp'];
1921
1843
  const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
1922
1844
 
1923
1845
  const args = process.argv.slice(2);
@@ -2005,14 +1927,6 @@ switch (command) {
2005
1927
  }
2006
1928
  break;
2007
1929
  }
2008
- case 'init': {
2009
- const yes = restArgs.includes('-y') || restArgs.includes('--yes');
2010
- init(undefined, { yes }).catch(err => {
2011
- log(err.message, 'error');
2012
- process.exit(1);
2013
- });
2014
- break;
2015
- }
2016
1930
  case 'uninstall':
2017
1931
  uninstall().catch(err => {
2018
1932
  log(err.message, 'error');
@@ -2052,7 +1966,6 @@ function showHelp() {
2052
1966
  console.log(`
2053
1967
  @jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
2054
1968
 
2055
- cbbm init --yes worktree 沙箱内安装开发配置(/cbb:worktree-init Step 4 自动调用)
2056
1969
  cbbm check-update 版本检查(用户级 hook 每日自动触发)
2057
1970
  cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
2058
1971
 
@@ -186,6 +186,29 @@ function ensureWorkspaceAgentsMd(targetDir) {
186
186
  return 'created';
187
187
  }
188
188
 
189
+ // 旧版空间导航模板(0.0.3 单层安装改造前)的特征语句。
190
+ // 该文件归用户所有、永不覆盖,故只能提示用户删除后重跑 cbb update 重新生成。
191
+ const LEGACY_AGENTS_MD_MARKERS = [
192
+ '故意不含编码规范',
193
+ '自动安装沙箱内的编码规范',
194
+ ];
195
+
196
+ /**
197
+ * 检测空间根 AGENTS.md 是否仍为旧版导航文案(命中任一特征语句即为旧版)。
198
+ * 仅用于安装时打印提示,不修改文件。
199
+ * @returns {boolean}
200
+ */
201
+ function detectLegacyAgentsMd(targetDir) {
202
+ const agentsPath = path.join(targetDir, 'AGENTS.md');
203
+ if (!fs.existsSync(agentsPath)) return false;
204
+ try {
205
+ const content = fs.readFileSync(agentsPath, 'utf-8');
206
+ return LEGACY_AGENTS_MD_MARKERS.some(m => content.includes(m));
207
+ } catch (_) {
208
+ return false;
209
+ }
210
+ }
211
+
189
212
  /**
190
213
  * 在指定目录执行 `git remote -v`,解析出 [{ name, url }] 列表。
191
214
  * 任何异常静默返回空数组,便于上层用 state 机判定。
@@ -285,6 +308,7 @@ module.exports = {
285
308
  loadWorkspaces,
286
309
  validateWorkspaceConfig,
287
310
  ensureWorkspaceAgentsMd,
311
+ detectLegacyAgentsMd,
288
312
  readGitRemotes,
289
313
  detectRepo,
290
314
  extractRepoBasename,
@@ -17,7 +17,7 @@ const path = require('path');
17
17
  const versionUtil = require('./version');
18
18
 
19
19
  // 缓存提升到用户级:registry 最新版本号是全局属性(npm 全局包只有一个版本),
20
- // 团队多 workspace / 多 worktree 沙箱共享一份,每日真实请求一次;
20
+ // 团队多 workspace / 多 worktree 共享一份,每日真实请求一次;
21
21
  // 「本项目是否落后」仍按项目清单版本在本地判定。
22
22
  const CACHE_FILE = path.join(os.homedir(), '.cbb', '.update-check-cache.json');
23
23
  const CACHE_TTL = 24 * 60 * 60 * 1000;