@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 +1 -1
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +13 -0
- package/cbb/lib/install/init.js +122 -209
- package/cbb/lib/install/workspaces.js +24 -0
- package/cbb/lib/utils/check-update.js +1 -1
- package/cbb/lib/utils/checkbox.js +11 -11
- package/cbb/lib/utils/output.js +38 -16
- package/cbb/worktrees/commands/worktree-close.md +1 -1
- package/cbb/worktrees/commands/worktree-init.md +6 -10
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +4 -4
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +26 -51
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env-deep.js +3 -33
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +5 -14
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +2 -2
- package/config/workspace-agents.sample.md +39 -37
- package/config/workspaces.json +5 -0
- package/package.json +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/auto-open.js +0 -136
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-ai.js +0 -177
package/cbb/bin/cbbm.js
CHANGED
|
@@ -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 中不必要的改动减少、不会因过度设计而重写、澄清问题在实现前而非出错后提出。
|
package/cbb/lib/install/init.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 使用方式:
|
|
10
10
|
* npm install -g @jspg-ai/coding-bb # 首次使用
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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()
|
|
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
|
|
57
|
-
//
|
|
58
|
-
const
|
|
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
|
-
//
|
|
75
|
+
// 装到空间的 tools skills:仅 cbb-design-to-wiki(openspec 设计阶段动作)。
|
|
61
76
|
// cbb-wiki-ops 当前不分发(保留在源仓库,有用户需要时再加入 USER_LEVEL_SKILLS);Java 工具不装。
|
|
62
|
-
const
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
{
|
|
641
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
{
|
|
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
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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
|
|
838
|
-
const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc,
|
|
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,
|
|
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('
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1413
|
+
// 6. 安装空间 AI 配置(单层全量:开发配置 + worktree 管理,统一安装器自动识别空间根)
|
|
1484
1414
|
try {
|
|
1485
|
-
await
|
|
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
|
|
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(`
|
|
1624
|
-
console.log(`
|
|
1552
|
+
console.log(` ${c('yellow', '[' + w.businessLine + ']')} ${w.name}`);
|
|
1553
|
+
console.log(` ${c('gray', w.repo)}`);
|
|
1625
1554
|
});
|
|
1626
|
-
console.log('
|
|
1627
|
-
console.log('
|
|
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
|
-
*
|
|
1699
|
-
*
|
|
1627
|
+
* 单层安装:统一安装器按空间根判定(workspace-config.json + .git 目录)补齐
|
|
1628
|
+
* worktree 管理入口与空间导航;旧子集清单升级时由清单差集机制自动补齐全量内容。
|
|
1700
1629
|
*/
|
|
1701
1630
|
async function updateInstalled() {
|
|
1702
|
-
|
|
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
|
-
|
|
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 = ['
|
|
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;
|