@jspg-ai/coding-bb 0.0.3-beta.3 → 0.0.3-beta.31
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/dev-standards/rules/cbb-ai-behavior.md +135 -116
- package/cbb/lib/external/index.js +228 -0
- package/cbb/lib/install/agents-nav.js +92 -0
- package/cbb/lib/install/claude-code.js +7 -2
- package/cbb/lib/install/command-skill.js +105 -0
- package/cbb/lib/install/init.js +502 -168
- package/cbb/lib/install/minimax.js +60 -0
- package/cbb/lib/install/opencode.js +3 -5
- package/cbb/lib/install/qoder.js +1 -3
- package/cbb/lib/install/rule-rewrite.js +27 -0
- package/cbb/lib/install/setup-decision.js +48 -0
- package/cbb/lib/install/tool-selection.js +45 -0
- package/cbb/lib/install/trae.js +43 -0
- package/cbb/lib/install/workbuddy.js +36 -0
- package/cbb/lib/install/workspaces.js +21 -17
- package/cbb/lib/openspec/index.js +337 -554
- package/cbb/lib/superpowers/index.js +246 -265
- package/cbb/lib/utils/check-update.js +17 -2
- package/cbb/lib/utils/githook.js +226 -0
- package/cbb/lib/utils/gitignore.js +16 -3
- package/cbb/lib/utils/plugin-hook.js +92 -0
- package/cbb/lib/utils/settings.js +17 -4
- package/cbb/lib/utils/tar.js +92 -0
- package/cbb/lib/utils/upstream.js +90 -0
- package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +7 -7
- package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/_shared/scripts/junction-ai-config.js +127 -0
- package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +158 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +60 -43
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +75 -48
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +13 -4
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +11 -2
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +138 -129
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +127 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +11 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +37 -9
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +140 -71
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
- package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
- package/config/openspec/schemas/spec-driven/schema.yaml +40 -30
- package/config/openspec/schemas/spec-driven/templates/design.md +0 -18
- package/config/openspec/schemas/spec-driven/templates/proposal.md +4 -3
- package/config/upstream-mirrors.json +17 -0
- package/config/workspace-agents.sample.md +4 -33
- package/external/frontend-design/.version +6 -0
- package/external/frontend-design/LICENSE.txt +177 -0
- package/external/frontend-design/SKILL.md +71 -0
- package/openspec/.version +2 -3
- package/openspec/commands/apply.md +189 -175
- package/openspec/commands/archive.md +244 -216
- package/openspec/commands/bulk-archive.md +362 -327
- package/openspec/commands/continue.md +115 -105
- package/openspec/commands/explore.md +230 -199
- package/openspec/commands/ff.md +115 -104
- package/openspec/commands/new.md +74 -63
- package/openspec/commands/onboard.md +562 -548
- package/openspec/commands/propose.md +161 -150
- package/openspec/commands/sync.md +277 -249
- package/openspec/commands/update.md +98 -80
- package/openspec/commands/verify.md +206 -162
- package/openspec/skills/openspec-apply-change/SKILL.md +20 -5
- package/openspec/skills/openspec-archive-change/SKILL.md +40 -11
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +46 -9
- package/openspec/skills/openspec-continue-change/SKILL.md +14 -3
- package/openspec/skills/openspec-explore/SKILL.md +22 -10
- package/openspec/skills/openspec-ff-change/SKILL.md +15 -3
- package/openspec/skills/openspec-new-change/SKILL.md +13 -1
- package/openspec/skills/openspec-onboard/SKILL.md +58 -43
- package/openspec/skills/openspec-propose/SKILL.md +15 -3
- package/openspec/skills/openspec-sync-specs/SKILL.md +31 -2
- package/openspec/skills/openspec-update-change/SKILL.md +34 -15
- package/openspec/skills/openspec-verify-change/SKILL.md +72 -26
- package/package.json +3 -2
- package/superpowers/.version +4 -4
- package/superpowers/skills/brainstorming/SKILL.md +47 -12
- package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -213
- package/superpowers/skills/brainstorming/scripts/server.cjs +723 -723
- package/superpowers/skills/brainstorming/visual-companion.md +6 -6
- package/superpowers/skills/diagnosing-superpowers/SKILL.md +120 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/analyst-common.md +38 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/cost-and-time.md +28 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/plan-adherence.md +29 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/quality-evidence.md +26 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/repeated-work.md +30 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/request-conflicts.md +20 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/scrub-audit.md +33 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/scrub.md +29 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/similar-session.md +38 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/skill-timeline.md +30 -0
- package/superpowers/skills/diagnosing-superpowers/prompts/stumbles.md +28 -0
- package/superpowers/skills/diagnosing-superpowers/references/context-safety.md +22 -0
- package/superpowers/skills/diagnosing-superpowers/references/github-issues.md +47 -0
- package/superpowers/skills/diagnosing-superpowers/references/redaction-policy.md +34 -0
- package/superpowers/skills/diagnosing-superpowers/references/session-discovery.md +31 -0
- package/superpowers/skills/diagnosing-superpowers/templates/bundle-README.md +77 -0
- package/superpowers/skills/diagnosing-superpowers/templates/case.md +64 -0
- package/superpowers/skills/diagnosing-superpowers/templates/issue.md +51 -0
- package/superpowers/skills/diagnosing-superpowers/templates/report.md +82 -0
- package/superpowers/skills/executing-plans/SKILL.md +350 -41
- package/superpowers/skills/executing-plans/scripts/task-done +52 -0
- package/superpowers/skills/executing-plans/scripts/task-start +28 -0
- package/superpowers/skills/requesting-code-review/SKILL.md +1 -1
- package/superpowers/skills/requesting-code-review/code-reviewer.md +17 -0
- package/superpowers/skills/subagent-driven-development/SKILL.md +18 -18
- package/superpowers/skills/subagent-driven-development/re-review-prompt.md +1 -1
- package/superpowers/skills/subagent-driven-development/scripts/review-package +53 -46
- package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +82 -40
- package/superpowers/skills/subagent-driven-development/scripts/task-brief +43 -41
- package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +2 -2
- package/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
- package/superpowers/skills/test-driven-development/SKILL.md +10 -0
- package/superpowers/skills/using-superpowers/SKILL.md +2 -0
- package/superpowers/skills/using-superpowers/references/claude-code-tools.md +29 -0
- package/superpowers/skills/using-superpowers/references/muse-tools.md +35 -0
- package/superpowers/skills/writing-plans/SKILL.md +62 -29
- package/superpowers/skills/writing-skills/SKILL.md +4 -2
- package/superpowers/skills/writing-skills/graphviz-conventions.dot +171 -171
- package/cbb/worktrees/commands/worktree-close.md +0 -64
- package/cbb/worktrees/commands/worktree-init.md +0 -51
- package/cbb/worktrees/commands/worktree-push.md +0 -42
- package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
package/cbb/lib/install/init.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* OpenSpec Shared Standards 安装脚本
|
|
5
5
|
*
|
|
6
6
|
* 将AI 编码规范与工作流套件安装到目标项目中。
|
|
7
|
-
* 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)
|
|
7
|
+
* 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)、
|
|
8
|
+
* WorkBuddy (.workbuddy/rules/)、TraeWork/TraeCode 中国版 (.trae/rules/,用户级 hook 在 ~/.trae-cn)、
|
|
9
|
+
* MiniMax Code (.minimax/rules/,规则经 AGENTS.md 托管导航加载,命令转 skill,hook 走用户级本地插件)
|
|
8
10
|
*
|
|
9
11
|
* 使用方式:
|
|
10
12
|
* npm install -g @jspg-ai/coding-bb # 首次使用
|
|
@@ -39,6 +41,7 @@ function execCmd(file, args, opts = {}) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
|
|
44
|
+
const setupDecision = require('./setup-decision');
|
|
42
45
|
|
|
43
46
|
// 目标项目根目录:默认取当前 cwd,init() 可在调用前覆盖。
|
|
44
47
|
// 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
|
|
@@ -51,14 +54,18 @@ const MANIFEST_FILE = path.join(MANIFEST_DIR, '.managed-by-cbb');
|
|
|
51
54
|
const LAST_TOOLS_FILE = path.join(MANIFEST_DIR, '.last-tools');
|
|
52
55
|
|
|
53
56
|
// 适配器
|
|
57
|
+
// 适配器(数组顺序 = 工具选择面板的显示顺序)
|
|
54
58
|
const adapters = [
|
|
55
|
-
require('./claude-code'),
|
|
56
|
-
require('./qoder'),
|
|
57
59
|
require('./opencode'),
|
|
60
|
+
require('./workbuddy'),
|
|
61
|
+
require('./trae'),
|
|
62
|
+
require('./qoder'),
|
|
63
|
+
require('./claude-code'),
|
|
64
|
+
require('./minimax'),
|
|
58
65
|
];
|
|
59
66
|
|
|
60
|
-
// superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量
|
|
61
|
-
// 供
|
|
67
|
+
// superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 15 个,
|
|
68
|
+
// 供 cbbm update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
|
|
62
69
|
const SUPERPOWERS_WHITELIST = [
|
|
63
70
|
'writing-plans', // openspec apply:生成 plan.md(schema.yaml 引用)
|
|
64
71
|
'subagent-driven-development', // openspec apply:执行 plan.md(schema.yaml 引用)
|
|
@@ -68,14 +75,20 @@ const SUPERPOWERS_WHITELIST = [
|
|
|
68
75
|
'test-driven-development', // TDD 计划结构 + TDD Evidence 方法论基底
|
|
69
76
|
];
|
|
70
77
|
|
|
71
|
-
// worktree
|
|
72
|
-
//
|
|
73
|
-
|
|
78
|
+
// worktree 管理 skill 清单:init / close / push 三件套(SKILL.md + scripts)。
|
|
79
|
+
// 按 skill 形态部署到各工具的 skillsDir,使这三个能力可被 AI 在对话中自动触发
|
|
80
|
+
// (description 驱动),同时工具也会把它们暴露为斜杠命令供用户手动指定。
|
|
81
|
+
const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push', 'cbb-local-mode'];
|
|
74
82
|
|
|
75
83
|
// 装到空间的 tools skills:当前不分发。
|
|
76
84
|
// cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
|
|
77
85
|
const TOOLS_SKILLS = [];
|
|
78
86
|
|
|
87
|
+
// external 外部依赖 skills 白名单:第三方上游 pin vendor 的 skill(源在 external/<name>/,
|
|
88
|
+
// 各子目录自带 .version pin,供 cbbm update --upstream-external 维护)。
|
|
89
|
+
// 非 skill 类外部依赖不走部署通道,不会进本清单。
|
|
90
|
+
const EXTERNAL_SKILLS = ['frontend-design'];
|
|
91
|
+
|
|
79
92
|
// 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
|
|
80
93
|
// onboard 为新手引导教程,团队入门已由文档覆盖;sync 是 archive 流程承重件,保留分发。
|
|
81
94
|
const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
@@ -83,12 +96,56 @@ const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
|
83
96
|
// settings.json hook 工具
|
|
84
97
|
const settingsManager = require('../utils/settings');
|
|
85
98
|
|
|
99
|
+
// MiniMax 本地插件 hook(无 settings.json hooks 的适配器走插件形态)
|
|
100
|
+
const pluginHookManager = require('../utils/plugin-hook');
|
|
101
|
+
|
|
102
|
+
// AGENTS.md 托管导航块(agentsNav 适配器的项目级规则注入通道)
|
|
103
|
+
const agentsNav = require('./agents-nav');
|
|
104
|
+
|
|
105
|
+
// 命令 → skill 形态转换(commandsAsSkills 适配器,当前仅 MiniMax Code)
|
|
106
|
+
const commandSkill = require('./command-skill');
|
|
107
|
+
|
|
108
|
+
// 工具选择策略(退市工具不在面板提供 + 存量粘性保留)
|
|
109
|
+
const toolSelection = require('./tool-selection');
|
|
110
|
+
|
|
111
|
+
/** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
|
|
112
|
+
function hookOpts(adapter) {
|
|
113
|
+
return adapter.hookDialect ? { dialect: adapter.hookDialect } : {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 工具历史名迁移:beta.16 及以前为 'codebuddy'(CodeBuddy 目录模型),
|
|
117
|
+
// beta.17 起统一为 'workbuddy'。存量 .last-tools / 清单里的旧名读取时归一,
|
|
118
|
+
// 下次安装由 saveLastTools 以新名落盘。
|
|
119
|
+
const LEGACY_TOOL_NAMES = { codebuddy: 'workbuddy' };
|
|
120
|
+
|
|
121
|
+
/** 工具名集合归一(旧名 → 新名);null 透传 */
|
|
122
|
+
function normalizeToolSet(set) {
|
|
123
|
+
if (!set) return set;
|
|
124
|
+
return new Set([...set].map(t => LEGACY_TOOL_NAMES[t] || t));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 迁移清理:移除本包写在旧用户级 settings 里的 hook(如 WorkBuddy 适配修正:
|
|
129
|
+
* ~/.codebuddy/settings.json → ~/.workbuddy/settings.json)。
|
|
130
|
+
* removeHooks 只删 _managedBy: cbb 标记的条目,文件清空则删除。返回 true 表示有改动。
|
|
131
|
+
*/
|
|
132
|
+
function cleanLegacySettings(adapter) {
|
|
133
|
+
if (!adapter.legacySettingsFile || !adapter.hookEvents) return false;
|
|
134
|
+
return settingsManager.removeHooks(adapter.legacySettingsFile, adapter.hookEvents, hookOpts(adapter));
|
|
135
|
+
}
|
|
136
|
+
|
|
86
137
|
// 清单驱动的文件清理决策(保护用户文档 CLAUDE.md / AGENTS.md 等)
|
|
87
138
|
const { pathsToDelete, protectedKeptPaths } = require('./cleanup');
|
|
88
139
|
|
|
140
|
+
// 规则 frontmatter 改写(Trae/WorkBuddy 需 alwaysApply: true)
|
|
141
|
+
const { applyRuleRewrites } = require('./rule-rewrite');
|
|
142
|
+
|
|
89
143
|
// .gitignore 条目决策(纯函数:检测/写入/卸载移除)
|
|
90
144
|
const gitignore = require('../utils/gitignore');
|
|
91
145
|
|
|
146
|
+
// git pre-commit hook:分支感知提交防线(ADR 0002,install/uninstall 调用)
|
|
147
|
+
const githook = require('../utils/githook');
|
|
148
|
+
|
|
92
149
|
// 业务空间配置(cbb setup 命令专用)
|
|
93
150
|
const workspaces = require('./workspaces');
|
|
94
151
|
|
|
@@ -104,6 +161,9 @@ const superpowers = require('../superpowers');
|
|
|
104
161
|
// OpenSpec 工作流管理(内置集成 + cbb update --upstream-openspec)
|
|
105
162
|
const openspec = require('../openspec');
|
|
106
163
|
|
|
164
|
+
// External 外部依赖管理(内置集成 + cbbm update --upstream-external)
|
|
165
|
+
const external = require('../external');
|
|
166
|
+
|
|
107
167
|
// ── 颜色与输出辅助 ──────────────────────────────────────
|
|
108
168
|
|
|
109
169
|
const { COLOR, COLOR_ENABLED, c, log, section, step, padVisual, progress, LOG_STYLES } = require('../utils/output');
|
|
@@ -114,13 +174,15 @@ function detectExistingInstall() {
|
|
|
114
174
|
}
|
|
115
175
|
|
|
116
176
|
/**
|
|
117
|
-
* 判定目标目录是否为业务空间根:workspace-config.json
|
|
118
|
-
*
|
|
177
|
+
* 判定目标目录是否为业务空间根:CBB-WORKSPACE.json(或旧名 workspace-config.json)存在且 .git 是目录。
|
|
178
|
+
* 需求工作树内也有该文件(tracked 副本),但其 .git 是文件(gitdir 指针),
|
|
119
179
|
* 必须以 .git 形态排除——避免「在 worktree 里误跑 cbb update」被当作空间根
|
|
120
180
|
* (误装 worktree 管理入口、在错误位置报旧导航提示)。
|
|
181
|
+
* 双名识别用于改名过渡期:存量空间(旧名)跑 update 时仍能识别并装上新版 hook。
|
|
121
182
|
*/
|
|
122
183
|
function detectWorkspaceRoot() {
|
|
123
|
-
if (!fs.existsSync(path.join(TARGET_DIR, '
|
|
184
|
+
if (!fs.existsSync(path.join(TARGET_DIR, 'CBB-WORKSPACE.json'))
|
|
185
|
+
&& !fs.existsSync(path.join(TARGET_DIR, 'workspace-config.json'))) return false;
|
|
124
186
|
const gitPath = path.join(TARGET_DIR, '.git');
|
|
125
187
|
return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
|
|
126
188
|
}
|
|
@@ -164,8 +226,13 @@ function writeManifest(paths, selectedAdapters) {
|
|
|
164
226
|
});
|
|
165
227
|
}
|
|
166
228
|
|
|
167
|
-
//
|
|
168
|
-
|
|
229
|
+
// 合并:选中的适配器写入新版本;未选中的(工具仍在包内)保持不变;
|
|
230
|
+
// 已从包中移除的工具(如 codebuddy)记录清理,避免清单里留下永久无效项
|
|
231
|
+
const knownNames = new Set(adapters.map(a => a.name));
|
|
232
|
+
const adapterVersions = {};
|
|
233
|
+
for (const [name, ver] of Object.entries(existingAdapterVersions)) {
|
|
234
|
+
if (knownNames.has(name)) adapterVersions[name] = ver;
|
|
235
|
+
}
|
|
169
236
|
selectedAdapters.forEach(adapter => {
|
|
170
237
|
adapterVersions[adapter.name] = pkg.version;
|
|
171
238
|
});
|
|
@@ -184,7 +251,7 @@ function readLastTools() {
|
|
|
184
251
|
if (!fs.existsSync(filePath)) return null;
|
|
185
252
|
try {
|
|
186
253
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
187
|
-
return new Set(data.tools || []);
|
|
254
|
+
return normalizeToolSet(new Set(data.tools || []));
|
|
188
255
|
} catch (_) {
|
|
189
256
|
return null;
|
|
190
257
|
}
|
|
@@ -209,6 +276,39 @@ function removePath(targetPath) {
|
|
|
209
276
|
}
|
|
210
277
|
}
|
|
211
278
|
|
|
279
|
+
/**
|
|
280
|
+
* 清理因删除文件/目录而变空的祖先目录(从深到浅,止于 TARGET_DIR)。
|
|
281
|
+
*
|
|
282
|
+
* 删除清单路径只删到该路径本身,其父目录会留空(如改名后残留的空 `.cbb/skills/`)。
|
|
283
|
+
* 此处统一收尾,升级与卸载共用。仅删除「存在且为空」的目录,非空目录(含用户文件)
|
|
284
|
+
* 一律保留。
|
|
285
|
+
*
|
|
286
|
+
* @param {Iterable<string>} relativePaths 已删除的相对路径(正斜杠)
|
|
287
|
+
* @returns {string[]} 实际删除的空目录相对路径
|
|
288
|
+
*/
|
|
289
|
+
function cleanupEmptyParentDirs(relativePaths) {
|
|
290
|
+
const dirsToCheck = new Set();
|
|
291
|
+
for (const relativePath of relativePaths) {
|
|
292
|
+
let dir = path.dirname(relativePath);
|
|
293
|
+
while (dir && dir !== '.') {
|
|
294
|
+
dirsToCheck.add(dir);
|
|
295
|
+
dir = path.dirname(dir);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const removed = [];
|
|
300
|
+
[...dirsToCheck]
|
|
301
|
+
.sort((a, b) => b.split('/').length - a.split('/').length)
|
|
302
|
+
.forEach(dir => {
|
|
303
|
+
const fullPath = path.join(TARGET_DIR, dir);
|
|
304
|
+
if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
|
|
305
|
+
removePath(fullPath);
|
|
306
|
+
removed.push(dir);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
return removed;
|
|
310
|
+
}
|
|
311
|
+
|
|
212
312
|
/**
|
|
213
313
|
* 安全删除目标路径(文件或目录)
|
|
214
314
|
* 在清单中 → 直接删除(本包旧版本)
|
|
@@ -252,19 +352,22 @@ function ask(question) {
|
|
|
252
352
|
async function selectTools(isUpgrade) {
|
|
253
353
|
const action = isUpgrade ? '升级' : '安装';
|
|
254
354
|
|
|
355
|
+
// 面板可选项 = 未退市适配器(Claude Code / MiniMax Code 已退市,见 tool-selection.js)
|
|
356
|
+
const selectable = toolSelection.selectableAdapters(adapters);
|
|
357
|
+
|
|
255
358
|
// 读取上次选择的工具:有记录则预选上次选择,首次安装则全部不选
|
|
256
359
|
const lastTools = readLastTools();
|
|
257
360
|
const preSelected = lastTools
|
|
258
|
-
?
|
|
361
|
+
? selectable.map((a, i) => lastTools.has(a.name) ? i : -1).filter(i => i >= 0)
|
|
259
362
|
: [];
|
|
260
363
|
|
|
261
364
|
// checkbox 多选:
|
|
262
365
|
// - 不切换 → 确认当前选择
|
|
263
366
|
// - 取消某项 → 只装剩下的
|
|
264
|
-
// - 全部不选 →
|
|
367
|
+
// - 全部不选 → 提示后回退全部可选项
|
|
265
368
|
const selectedIdx = await checkbox.promptCheckbox({
|
|
266
369
|
header: `🔧 选择要${action}的 AI 工具(空格切换,回车确认)`,
|
|
267
|
-
items:
|
|
370
|
+
items: selectable.map(a => ({
|
|
268
371
|
label: c('cyan', a.displayName),
|
|
269
372
|
})),
|
|
270
373
|
preSelected,
|
|
@@ -273,15 +376,18 @@ async function selectTools(isUpgrade) {
|
|
|
273
376
|
let result;
|
|
274
377
|
if (selectedIdx.size === 0) {
|
|
275
378
|
log(`未选择任何工具,默认全部${action}`, 'warn');
|
|
276
|
-
result =
|
|
379
|
+
result = selectable;
|
|
277
380
|
} else {
|
|
278
381
|
// 转成有序适配器数组(按原始顺序)
|
|
279
382
|
result = [...selectedIdx]
|
|
280
383
|
.sort((a, b) => a - b)
|
|
281
|
-
.map(i =>
|
|
384
|
+
.map(i => selectable[i]);
|
|
282
385
|
}
|
|
283
386
|
|
|
284
|
-
//
|
|
387
|
+
// 退市但该空间旧记录里有的工具:粘性保留(继续维护,不因退市清删其内容)
|
|
388
|
+
result = toolSelection.carryOverRetired(adapters, result, lastTools);
|
|
389
|
+
|
|
390
|
+
// 保存本次选择(含粘性保留项,避免记录丢失导致下次被清理),供下次运行时预选
|
|
285
391
|
saveLastTools(result);
|
|
286
392
|
return result;
|
|
287
393
|
}
|
|
@@ -306,7 +412,8 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
306
412
|
// 同名文件存在时直接覆盖
|
|
307
413
|
removePath(destPath);
|
|
308
414
|
|
|
309
|
-
|
|
415
|
+
const content = applyRuleRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
|
|
416
|
+
fs.writeFileSync(destPath, content);
|
|
310
417
|
}
|
|
311
418
|
});
|
|
312
419
|
|
|
@@ -560,7 +667,7 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
|
|
|
560
667
|
const srcPath = path.join(srcDir, f);
|
|
561
668
|
const destPath = path.join(destDir, prefix + f);
|
|
562
669
|
removePath(destPath);
|
|
563
|
-
const content = sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8')));
|
|
670
|
+
const content = rewriteCommandName(adapter, commandSkill.sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
|
|
564
671
|
fs.writeFileSync(destPath, content);
|
|
565
672
|
});
|
|
566
673
|
|
|
@@ -568,41 +675,60 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
|
|
|
568
675
|
}
|
|
569
676
|
|
|
570
677
|
/**
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* 会导致 opencode 拒绝加载整个项目配置。此处仅在部署副本的 frontmatter 块内
|
|
574
|
-
* 移除空的 description 行(undefined 合法),上游源文件不动(CLI 接管区)。
|
|
678
|
+
* opencode 命令显示名取自 frontmatter name,上游为大写空格风格("OPSX: Propose"),
|
|
679
|
+
* 与文档写法(/opsx-propose)不一致;部署副本统一重写为 kebab-case。
|
|
575
680
|
*/
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
if (end === -1) return content;
|
|
584
|
-
let changed = false;
|
|
585
|
-
for (let i = 1; i < end; i++) {
|
|
586
|
-
if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
|
|
587
|
-
lines[i] = null;
|
|
588
|
-
changed = true;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return changed ? lines.filter(l => l !== null).join('\n') : content;
|
|
681
|
+
function rewriteCommandName(adapter, content) {
|
|
682
|
+
if (!adapter.commandNameKebab) return content;
|
|
683
|
+
return content.replace(/^name:\s*"?([A-Za-z]+):\s*([^"\r\n]+?)"?\s*\r?$/m, (m0, ns, rest) => {
|
|
684
|
+
const kebab = (ns + '-' + rest).toLowerCase().replace(/\s+/g, '-');
|
|
685
|
+
return `name: "${kebab}"`;
|
|
686
|
+
});
|
|
592
687
|
}
|
|
593
688
|
|
|
594
|
-
/**
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
689
|
+
/**
|
|
690
|
+
* MiniMax:无项目级 commands 目录,命令转 skill 形态(skill 即其斜杠命令),
|
|
691
|
+
* 每个命令落为 <skillsDir>/<prefix><name>/SKILL.md,目录名 = frontmatter name。
|
|
692
|
+
* @returns {{count:number, names:string[], destDir:string|null}} destDir 为清单登记用相对目录
|
|
693
|
+
*/
|
|
694
|
+
function deployCommandSkills(adapter, srcDir, filter) {
|
|
695
|
+
if (!fs.existsSync(srcDir)) return { count: 0, names: [], destDir: null };
|
|
696
|
+
const destDir = adapter.skillsDir;
|
|
697
|
+
const skillsRoot = path.join(TARGET_DIR, destDir);
|
|
698
|
+
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
699
|
+
|
|
700
|
+
// 与 skill 同构的扁平 .md 源文件(命令不是子目录)
|
|
701
|
+
const commandFiles = fs.readdirSync(srcDir)
|
|
702
|
+
.filter(f => f.endsWith('.md') && fs.statSync(path.join(srcDir, f)).isFile())
|
|
703
|
+
.filter(f => !filter || filter.includes(f));
|
|
704
|
+
|
|
705
|
+
const names = [];
|
|
706
|
+
commandFiles.forEach(f => {
|
|
707
|
+
const skillName = commandSkill.skillNameFor(f, adapter.commandPrefix || '');
|
|
708
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
709
|
+
removePath(skillDir);
|
|
710
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
711
|
+
const content = commandSkill.convertCommandToSkill(
|
|
712
|
+
applyContentRewrites(adapter, fs.readFileSync(path.join(srcDir, f), 'utf-8')),
|
|
713
|
+
skillName);
|
|
714
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
715
|
+
names.push(skillName);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
return { count: commandFiles.length, names, destDir };
|
|
599
719
|
}
|
|
600
720
|
|
|
601
|
-
/**
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
721
|
+
/**
|
|
722
|
+
* 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir;
|
|
723
|
+
* commandsAsSkills 适配器转 skill 形态 → adapter.skillsDir)
|
|
724
|
+
* @returns {{count:number, names:string[], destDir:string|null}}
|
|
725
|
+
*/
|
|
726
|
+
function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
|
|
727
|
+
if (adapter.commandsAsSkills) return deployCommandSkills(adapter, srcDir, filter);
|
|
728
|
+
if (!adapter.commandsDir) return { count: 0, names: [], destDir: null };
|
|
729
|
+
const result = deployCommandFiles(
|
|
730
|
+
adapter, srcDir, path.join(TARGET_DIR, adapter.commandsDir), adapter.commandPrefix || '', filter);
|
|
731
|
+
return { ...result, destDir: adapter.commandsDir };
|
|
606
732
|
}
|
|
607
733
|
|
|
608
734
|
// ── 设置 OpenSpec 工作流 ──────────────────────────────────
|
|
@@ -922,22 +1048,37 @@ async function init(targetDir, options = {}) {
|
|
|
922
1048
|
// 选择优先级:调用方预设(cbb setup)> .last-tools(非交互沿用)> 交互选择;无记录才回退全选
|
|
923
1049
|
const isNonInteractive = yes || !process.stdin.isTTY;
|
|
924
1050
|
let selectedAdapters;
|
|
1051
|
+
let fromRecord = false; // 选择来自预设或 .last-tools 记录(需落盘归一化后的新工具名)
|
|
925
1052
|
if (presetAdapters && presetAdapters.length > 0) {
|
|
926
1053
|
selectedAdapters = presetAdapters;
|
|
1054
|
+
fromRecord = true;
|
|
927
1055
|
} else if (isNonInteractive) {
|
|
928
1056
|
const lastTools = readLastTools();
|
|
929
1057
|
const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
|
|
930
|
-
selectedAdapters = reused.length > 0 ? reused : adapters;
|
|
1058
|
+
selectedAdapters = reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters);
|
|
1059
|
+
fromRecord = reused.length > 0;
|
|
931
1060
|
if (yes && process.stdin.isTTY) {
|
|
932
1061
|
log(reused.length > 0
|
|
933
1062
|
? `--yes 非交互模式,沿用上次工具选择:${reused.map(a => a.displayName).join('、')}`
|
|
934
|
-
: '--yes
|
|
1063
|
+
: '--yes 非交互模式,无上次选择记录,已默认选中全部可选工具', 'info');
|
|
935
1064
|
}
|
|
936
1065
|
} else {
|
|
937
1066
|
selectedAdapters = await selectTools(isUpgrade);
|
|
938
1067
|
}
|
|
939
|
-
|
|
940
|
-
|
|
1068
|
+
|
|
1069
|
+
// 退市工具粘性保留(面板不出现,但本空间旧记录里有 → 继续维护,内容不被清理)。
|
|
1070
|
+
// 覆盖面板外入口:setup 预设(presetAdapters)与非交互记录沿用;交互面板已在 selectTools 内并过。
|
|
1071
|
+
const lastToolsForDir = readLastTools();
|
|
1072
|
+
const withRetired = toolSelection.carryOverRetired(adapters, selectedAdapters, lastToolsForDir);
|
|
1073
|
+
const retiredKept = toolSelection.retiredNames(withRetired);
|
|
1074
|
+
if (retiredKept.length > 0) {
|
|
1075
|
+
log(`保留上次选择的退市工具:${retiredKept.join('、')}(当前不在安装面板提供,内容继续维护)`, 'dim');
|
|
1076
|
+
}
|
|
1077
|
+
selectedAdapters = withRetired;
|
|
1078
|
+
|
|
1079
|
+
// 业务空间根记录本次选择,供 cbb update 等非交互场景沿用;
|
|
1080
|
+
// 沿用旧记录时同样落盘,使历史工具名(codebuddy → workbuddy)归一化自愈
|
|
1081
|
+
if (isWorkspaceRoot && fromRecord) {
|
|
941
1082
|
saveLastTools(selectedAdapters);
|
|
942
1083
|
}
|
|
943
1084
|
|
|
@@ -969,11 +1110,10 @@ async function init(targetDir, options = {}) {
|
|
|
969
1110
|
if (skillsAdapters.length > 0) {
|
|
970
1111
|
console.log(`\n 🔧 ${action}按需加载 Skills...`);
|
|
971
1112
|
const standardsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'dev-standards', 'skills');
|
|
972
|
-
const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
|
|
973
1113
|
const extendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
|
|
974
1114
|
|
|
975
1115
|
const skillsDests = skillsAdapters.map(a => a.skillsDir);
|
|
976
|
-
let stdCount = 0,
|
|
1116
|
+
let stdCount = 0, extCount = 0;
|
|
977
1117
|
skillsAdapters.forEach(adapter => {
|
|
978
1118
|
// 通用编码规范 Skills(cbb/dev-standards/skills)
|
|
979
1119
|
const stdResult = deploySkills(adapter, isUpgrade, managedPaths, standardsSkillsSrc);
|
|
@@ -983,12 +1123,6 @@ async function init(targetDir, options = {}) {
|
|
|
983
1123
|
}
|
|
984
1124
|
// 官方 OpenSpec Skills(openspec/skills/)不部署——工作流入口统一为 /opsx:* 斜杠命令
|
|
985
1125
|
//(openspec/commands/ 在下方 Commands 段部署),避免双形态重复
|
|
986
|
-
// worktree 扩展 Skills:init / close / push 三件套(单层安装,空间根是唯一安装点)
|
|
987
|
-
const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, WORKTREE_SKILLS);
|
|
988
|
-
if (osExtResult.count > 0) {
|
|
989
|
-
osCount = osExtResult.count;
|
|
990
|
-
osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
991
|
-
}
|
|
992
1126
|
// 工具 Skills:当前不分发(恢复方式见 TOOLS_SKILLS 注释)
|
|
993
1127
|
const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, TOOLS_SKILLS);
|
|
994
1128
|
if (extResult.count > 0) {
|
|
@@ -997,9 +1131,22 @@ async function init(targetDir, options = {}) {
|
|
|
997
1131
|
}
|
|
998
1132
|
});
|
|
999
1133
|
if (stdCount > 0) log(`规范 Skills → ${skillsDests.join(', ')} (${stdCount} 个)`, 'success');
|
|
1000
|
-
if (osCount > 0) log(`worktree Skills → ${skillsDests.join(', ')} (${osCount} 个)`, 'success');
|
|
1001
1134
|
if (extCount > 0) log(`扩展 Skills → ${skillsDests.join(', ')} (${extCount} 个)`, 'success');
|
|
1002
1135
|
|
|
1136
|
+
// 部署 worktree 管理 Skills(init / close / push;skill 形态,可被对话自动触发)
|
|
1137
|
+
const worktreeSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
|
|
1138
|
+
let wtCount = 0;
|
|
1139
|
+
if (fs.existsSync(worktreeSkillsSrc)) {
|
|
1140
|
+
skillsAdapters.forEach(adapter => {
|
|
1141
|
+
const wtResult = deploySkills(adapter, isUpgrade, managedPaths, worktreeSkillsSrc, WORKTREE_SKILLS);
|
|
1142
|
+
if (wtResult.count > 0) {
|
|
1143
|
+
wtCount = wtResult.count;
|
|
1144
|
+
wtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
if (wtCount > 0) log(`Worktree Skills → ${skillsDests.join(', ')} (${wtCount} 个)`, 'success');
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1003
1150
|
// 部署 Superpowers Skills(白名单:openspec 工作流依赖 + 方法论基底)
|
|
1004
1151
|
const superpowersSkillsSrc = superpowers.SUPERPOWERS_SKILLS_DIR;
|
|
1005
1152
|
let spCount = 0;
|
|
@@ -1017,22 +1164,36 @@ async function init(targetDir, options = {}) {
|
|
|
1017
1164
|
log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
|
|
1018
1165
|
}
|
|
1019
1166
|
|
|
1167
|
+
// 部署 external 外部依赖 Skills(第三方上游 pin vendor,白名单控制)
|
|
1168
|
+
const externalSkillsSrc = external.EXTERNAL_DIR;
|
|
1169
|
+
let exCount = 0;
|
|
1170
|
+
if (fs.existsSync(externalSkillsSrc)) {
|
|
1171
|
+
skillsAdapters.forEach(adapter => {
|
|
1172
|
+
const exResult = deploySkills(adapter, isUpgrade, managedPaths, externalSkillsSrc, EXTERNAL_SKILLS);
|
|
1173
|
+
if (exResult.count > 0) {
|
|
1174
|
+
exCount = exResult.count;
|
|
1175
|
+
exResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
if (exCount > 0) log(`External Skills → ${skillsDests.join(', ')} (${exCount}/${EXTERNAL_SKILLS.length} 个,白名单)`, 'success');
|
|
1179
|
+
} else {
|
|
1180
|
+
log('external/ 目录不存在,跳过 External Skills 部署', 'warn');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1020
1183
|
// 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
|
|
1021
1184
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
1022
1185
|
}
|
|
1023
1186
|
|
|
1024
|
-
// 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发;
|
|
1025
|
-
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
|
|
1187
|
+
// 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发;commandsAsSkills 适配器转 skill 形态)
|
|
1188
|
+
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir || a.commandsAsSkills);
|
|
1026
1189
|
if (commandsAdapters.length > 0) {
|
|
1027
1190
|
console.log(`\n ⚡ ${action} Commands...`);
|
|
1028
1191
|
const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
|
|
1029
|
-
const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
|
|
1030
|
-
const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
|
|
1031
1192
|
const openspecCmdInclude = fs.readdirSync(commandsBaseSrc)
|
|
1032
1193
|
.filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
|
|
1033
1194
|
|
|
1034
1195
|
const cmdDests = [...new Set(commandsAdapters
|
|
1035
|
-
.
|
|
1196
|
+
.map(a => a.commandsDir || (a.commandsAsSkills ? `${a.skillsDir}(转 skill)` : null))
|
|
1036
1197
|
.filter(Boolean))];
|
|
1037
1198
|
let cmdCount = 0;
|
|
1038
1199
|
commandsAdapters.forEach(adapter => {
|
|
@@ -1041,13 +1202,7 @@ async function init(targetDir, options = {}) {
|
|
|
1041
1202
|
const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc, openspecCmdInclude);
|
|
1042
1203
|
if (baseResult.count > 0) {
|
|
1043
1204
|
adapterCmdCount += baseResult.count;
|
|
1044
|
-
baseResult.names.forEach(name => newPaths.add(`${
|
|
1045
|
-
}
|
|
1046
|
-
// worktree 管理 Commands(cbb/worktrees/commands/)
|
|
1047
|
-
const extResult = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
|
|
1048
|
-
if (extResult.count > 0) {
|
|
1049
|
-
adapterCmdCount += extResult.count;
|
|
1050
|
-
extResult.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
|
|
1205
|
+
baseResult.names.forEach(name => newPaths.add(`${baseResult.destDir}/${name}`));
|
|
1051
1206
|
}
|
|
1052
1207
|
cmdCount = Math.max(cmdCount, adapterCmdCount);
|
|
1053
1208
|
});
|
|
@@ -1077,9 +1232,27 @@ async function init(targetDir, options = {}) {
|
|
|
1077
1232
|
}
|
|
1078
1233
|
}
|
|
1079
1234
|
|
|
1080
|
-
//
|
|
1235
|
+
// AGENTS.md 规则导航块(声明 agentsNav 的适配器;MiniMax 唯一项目级规则注入通道)。
|
|
1236
|
+
// 选中 → 注入/更新托管块(块外用户内容不动);未选中(本次 update 取消勾选)→ 精确摘除。
|
|
1237
|
+
let agentsNavState = null;
|
|
1238
|
+
let agentsNavFile = null;
|
|
1239
|
+
adapters.filter(a => a.agentsNav).forEach(adapter => {
|
|
1240
|
+
const target = path.join(TARGET_DIR, adapter.agentsNav.file);
|
|
1241
|
+
if (selectedAdapters.some(sa => sa.name === adapter.name)) {
|
|
1242
|
+
const state = agentsNav.inject(target, adapter.name, adapter.agentsNav.text);
|
|
1243
|
+
if (state !== 'unchanged') { agentsNavState = state; agentsNavFile = adapter.agentsNav.file; }
|
|
1244
|
+
} else if (agentsNav.remove(target, adapter.name)) {
|
|
1245
|
+
agentsNavState = 'removed';
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
if (agentsNavState === 'created' || agentsNavState === 'updated') {
|
|
1249
|
+
log(`AGENTS.md 规则导航 → ${agentsNavFile}`, 'success');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// 写入自动版本检查 hook(settings.json 方言 + 本地插件方言)
|
|
1081
1253
|
const hookAdapters = selectedAdapters.filter(a => a.settingsFile && a.hookEvents);
|
|
1082
|
-
|
|
1254
|
+
const pluginHookAdapters = selectedAdapters.filter(a => a.pluginHook);
|
|
1255
|
+
if (hookAdapters.length > 0 || pluginHookAdapters.length > 0) {
|
|
1083
1256
|
console.log(`\n 🔔 配置自动版本检查...`);
|
|
1084
1257
|
const command = settingsManager.getCheckUpdateCommand();
|
|
1085
1258
|
hookAdapters.forEach(adapter => {
|
|
@@ -1087,14 +1260,33 @@ async function init(targetDir, options = {}) {
|
|
|
1087
1260
|
const settingsPath = adapter.settingsFileIsUserLevel
|
|
1088
1261
|
? adapter.settingsFile
|
|
1089
1262
|
: path.join(TARGET_DIR, adapter.settingsFile);
|
|
1090
|
-
settingsManager.addHooks(settingsPath, command, adapter.hookEvents);
|
|
1263
|
+
settingsManager.addHooks(settingsPath, command, adapter.hookEvents, hookOpts(adapter));
|
|
1091
1264
|
// 清理旧版本遗留的 SessionStart hook(现已仅安装 UserPromptSubmit)
|
|
1092
|
-
if (settingsManager.removeLegacyHooks(settingsPath)) {
|
|
1265
|
+
if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
|
|
1093
1266
|
log(`清理 ${adapter.displayName} 遗留的 SessionStart hook`, 'success');
|
|
1094
1267
|
}
|
|
1268
|
+
// 迁移:清理旧用户级 settings(如 ~/.codebuddy → ~/.workbuddy)
|
|
1269
|
+
if (cleanLegacySettings(adapter)) {
|
|
1270
|
+
log(`迁移清理旧用户级配置 ${adapter.legacySettingsFile}`, 'success');
|
|
1271
|
+
}
|
|
1095
1272
|
if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
|
|
1096
1273
|
});
|
|
1097
|
-
|
|
1274
|
+
if (hookAdapters.length > 0) {
|
|
1275
|
+
log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')} (${hookAdapters[0].hookEvents.join(', ')})`, 'success');
|
|
1276
|
+
}
|
|
1277
|
+
// 本地插件方言(MiniMax:无 settings.json hooks)——用户级,不写入项目清单
|
|
1278
|
+
if (pluginHookAdapters.length > 0) {
|
|
1279
|
+
const pkgVersion = require(path.join(SHARED_STANDARDS_DIR, 'package.json')).version;
|
|
1280
|
+
pluginHookAdapters.forEach(adapter => {
|
|
1281
|
+
const info = pluginHookManager.install({
|
|
1282
|
+
dataDir: adapter.resolveDataDir(),
|
|
1283
|
+
...adapter.pluginHook,
|
|
1284
|
+
command,
|
|
1285
|
+
version: pkgVersion,
|
|
1286
|
+
});
|
|
1287
|
+
log(`${adapter.displayName} 版本检查插件 → ${info.dir}`, 'success');
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1098
1290
|
}
|
|
1099
1291
|
|
|
1100
1292
|
// 升级时清理旧路径(在旧清单中但不在新路径集中)。
|
|
@@ -1114,6 +1306,8 @@ async function init(targetDir, options = {}) {
|
|
|
1114
1306
|
if (oldPaths.length > MAX_DISPLAY) {
|
|
1115
1307
|
log(` ... 还有 ${oldPaths.length - MAX_DISPLAY} 个`, 'dim');
|
|
1116
1308
|
}
|
|
1309
|
+
// 收尾:清掉因此变空的祖先目录(如改名后残留的空 .cbb/skills/)
|
|
1310
|
+
cleanupEmptyParentDirs(oldPaths).forEach(dir => log(` 清理空目录 ${dir}/`, 'dim'));
|
|
1117
1311
|
}
|
|
1118
1312
|
|
|
1119
1313
|
const kept = protectedKeptPaths(managedPaths, newPaths);
|
|
@@ -1140,6 +1334,14 @@ async function init(targetDir, options = {}) {
|
|
|
1140
1334
|
// 更新 .gitignore
|
|
1141
1335
|
updateGitignore();
|
|
1142
1336
|
|
|
1337
|
+
// 安装分支感知提交防线(ADR 0002,仅业务空间根:空间根仓库 + .codespace/ 各应用)
|
|
1338
|
+
let hookSummary = null;
|
|
1339
|
+
if (isWorkspaceRoot) {
|
|
1340
|
+
hookSummary = githook.installGitHooksForWorkspace(TARGET_DIR);
|
|
1341
|
+
const appDone = hookSummary.apps.length;
|
|
1342
|
+
log(`提交防线 pre-commit hook → 空间根 + ${appDone} 个应用仓库(${hookSummary.root})`, 'success');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1143
1345
|
// 完成摘要
|
|
1144
1346
|
console.log(`\n ✅ ${action}完成!\n`);
|
|
1145
1347
|
|
|
@@ -1168,13 +1370,15 @@ async function init(targetDir, options = {}) {
|
|
|
1168
1370
|
|
|
1169
1371
|
// 附加组件(单行)
|
|
1170
1372
|
const hasSkills = selectedAdapters.some(a => a.skillsDir);
|
|
1171
|
-
const hasCommands = selectedAdapters.some(a => a.commandsDir);
|
|
1172
|
-
const hasHooks = selectedAdapters.some(a => a.settingsFile && a.hookEvents);
|
|
1373
|
+
const hasCommands = selectedAdapters.some(a => a.commandsDir || a.commandsAsSkills);
|
|
1374
|
+
const hasHooks = selectedAdapters.some(a => (a.settingsFile && a.hookEvents) || a.pluginHook);
|
|
1173
1375
|
const extras = [];
|
|
1174
1376
|
if (hasSkills) extras.push('Skills');
|
|
1175
1377
|
if (hasCommands) extras.push('Commands');
|
|
1176
1378
|
if (hasHooks) extras.push('版本检查 hook');
|
|
1379
|
+
if (hookSummary) extras.push('提交防线 pre-commit hook');
|
|
1177
1380
|
if (agentsMdState === 'created') extras.push('AGENTS.md(空间导航)');
|
|
1381
|
+
if (agentsNavState === 'created' || agentsNavState === 'updated') extras.push('AGENTS.md(规则导航)');
|
|
1178
1382
|
if (extras.length > 0) {
|
|
1179
1383
|
console.log(` ${extras.join(' / ')} 已配置`);
|
|
1180
1384
|
}
|
|
@@ -1184,38 +1388,51 @@ async function init(targetDir, options = {}) {
|
|
|
1184
1388
|
// ── cbb setup 命令 ──────────────────────────────────────
|
|
1185
1389
|
|
|
1186
1390
|
/**
|
|
1187
|
-
* 若 targetDir
|
|
1188
|
-
*
|
|
1189
|
-
* - 版本不同 →
|
|
1190
|
-
* -
|
|
1391
|
+
* 若 targetDir 已安装,比较「已装版本 vs 当前包版本」与「本次工具选择 vs .last-tools」
|
|
1392
|
+
* 决定是否继续安装(判定纯逻辑见 setup-decision.js)。
|
|
1393
|
+
* - 版本不同 → 'install'(setup 已整体确认过,直接升级到当前包版本,不再二次询问)
|
|
1394
|
+
* - 版本一致但本次选择含 .last-tools 之外的新工具(或无记录)→ 'install'
|
|
1395
|
+
* (beta.14 及以前的 bug:版本一致即整体跳过,新工具的空间级目录永不安装)
|
|
1396
|
+
* - 版本一致且所选 ⊆ .last-tools → 'skip'(不询问、不重复安装)
|
|
1191
1397
|
*
|
|
1192
1398
|
* 注:只比较「已装版本 vs 当前包版本」——安装动作只能落盘当前包的产物,
|
|
1193
1399
|
* registry 是否有更新由用户级版本检查 hook 提醒,不在此处联网查询。
|
|
1400
|
+
* @param {string} targetDir
|
|
1401
|
+
* @param {Array<{name:string}>} [selectedAdapters] 本次 setup 选中的工具
|
|
1194
1402
|
* @returns {'install'|'skip'}
|
|
1195
1403
|
*/
|
|
1196
|
-
function decideUpgradeIfInstalled(targetDir) {
|
|
1404
|
+
function decideUpgradeIfInstalled(targetDir, selectedAdapters) {
|
|
1197
1405
|
const installed = version.readInstalledVersion(targetDir);
|
|
1198
1406
|
if (!installed) return 'install'; // 未安装:不提示,直接继续
|
|
1199
1407
|
|
|
1200
1408
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
1409
|
+
const decision = setupDecision.decideSetupAction({
|
|
1410
|
+
sameVersion: version.compareVersions(installed, pkg.version) === 0,
|
|
1411
|
+
lastTools: normalizeToolSet(setupDecision.readLastToolsFrom(targetDir)),
|
|
1412
|
+
selectedNames: (selectedAdapters || []).map(a => a.name),
|
|
1413
|
+
});
|
|
1201
1414
|
|
|
1202
|
-
|
|
1203
|
-
if (version.compareVersions(installed, pkg.version) === 0) {
|
|
1415
|
+
if (decision.action === 'skip') {
|
|
1204
1416
|
step('✓', '版本', `v${installed}(已是最新,跳过)`);
|
|
1205
1417
|
return 'skip';
|
|
1206
1418
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1419
|
+
if (decision.reason === 'new-tools') {
|
|
1420
|
+
step('✓', '版本', `v${installed}(版本一致,新增工具 ${decision.newTools.join('、')} → 继续安装)`);
|
|
1421
|
+
} else if (decision.reason === 'no-tools-record') {
|
|
1422
|
+
step('✓', '版本', `v${installed}(无工具选择记录 → 继续安装)`);
|
|
1423
|
+
} else {
|
|
1424
|
+
step('✓', '版本', `v${installed} → v${pkg.version}`);
|
|
1425
|
+
}
|
|
1209
1426
|
return 'install';
|
|
1210
1427
|
}
|
|
1211
1428
|
|
|
1212
1429
|
/**
|
|
1213
|
-
* 检查业务空间根目录下是否有
|
|
1214
|
-
* 不存在时,将包内样例文件复制为
|
|
1430
|
+
* 检查业务空间根目录下是否有 CBB-WORKSPACE.json(init-worktree 技能依赖)。
|
|
1431
|
+
* 不存在时,将包内样例文件复制为 CBB-WORKSPACE.json。
|
|
1215
1432
|
* 返回 'exists' | 'created'(不打印,由调用方汇总输出)。
|
|
1216
1433
|
*/
|
|
1217
1434
|
function ensureWorkspaceConfig(targetDir) {
|
|
1218
|
-
const configPath = path.join(targetDir, '
|
|
1435
|
+
const configPath = path.join(targetDir, 'CBB-WORKSPACE.json');
|
|
1219
1436
|
if (fs.existsSync(configPath)) return 'exists';
|
|
1220
1437
|
|
|
1221
1438
|
const samplePath = path.join(SHARED_STANDARDS_DIR, workspaces.SAMPLE_CONFIG);
|
|
@@ -1224,12 +1441,12 @@ function ensureWorkspaceConfig(targetDir) {
|
|
|
1224
1441
|
}
|
|
1225
1442
|
|
|
1226
1443
|
/**
|
|
1227
|
-
* 读取
|
|
1444
|
+
* 读取 CBB-WORKSPACE.json 中的应用列表,将关联应用代码 clone/fetch 到 .codespace/ 目录。
|
|
1228
1445
|
* 首次 clone(耗时较长),后续 fetch --all(秒级更新)。
|
|
1229
1446
|
* 这是基准代码,init-worktree 技能后续只做轻量 fetch 检查。
|
|
1230
1447
|
*/
|
|
1231
1448
|
/**
|
|
1232
|
-
* 读取
|
|
1449
|
+
* 读取 CBB-WORKSPACE.json,将关联应用代码 clone/fetch 到 .codespace/ 目录。
|
|
1233
1450
|
* 不打印逐条日志,返回汇总对象由调用方渲染:
|
|
1234
1451
|
* { ok, fail, updated[], cloned[], fetchFailed[], sampleApps[], invalid[] }
|
|
1235
1452
|
* 长耗时(首次 clone),通过 onProgress 回调实时提示当前正在处理哪个应用。
|
|
@@ -1238,7 +1455,7 @@ function ensureWorkspaceConfig(targetDir) {
|
|
|
1238
1455
|
*/
|
|
1239
1456
|
async function syncAppRepos(targetDir, onProgress = () => {}) {
|
|
1240
1457
|
const summary = { ok: 0, fail: 0, updated: [], cloned: [], fetchFailed: [], sampleApps: [], invalid: [] };
|
|
1241
|
-
const configPath = path.join(targetDir, '
|
|
1458
|
+
const configPath = path.join(targetDir, 'CBB-WORKSPACE.json');
|
|
1242
1459
|
if (!fs.existsSync(configPath)) return summary;
|
|
1243
1460
|
|
|
1244
1461
|
let config;
|
|
@@ -1392,7 +1609,7 @@ async function syncWorkspaceRepo(targetDir, onProgress = () => {}) {
|
|
|
1392
1609
|
}
|
|
1393
1610
|
|
|
1394
1611
|
/**
|
|
1395
|
-
* 处理单个业务空间:计算子目录 → 检测/克隆 →
|
|
1612
|
+
* 处理单个业务空间:计算子目录 → 检测/克隆 → CBB-WORKSPACE.json → 拉取应用代码 → 升级询问 → 安装 AI 配置。
|
|
1396
1613
|
* 输出为紧凑步进行(✓ 标签 值),失败时不抛出也不退出,返回结果对象供调用方汇总。
|
|
1397
1614
|
* @param {object} [opts] - { selectedAdapters } cbb setup 上层选定的适配器(透传给空间级安装)
|
|
1398
1615
|
* @returns {Promise<{ok: boolean, error?: string, message?: string, subdir?: string, skipped?: boolean}>}
|
|
@@ -1454,28 +1671,28 @@ async function initSingleWorkspace(w, opts = {}) {
|
|
|
1454
1671
|
step('✓', '仓库', `已克隆 ${subdirName}/`);
|
|
1455
1672
|
}
|
|
1456
1673
|
|
|
1457
|
-
// 3.
|
|
1674
|
+
// 3. CBB-WORKSPACE.json:缺失则从样例生成,随后校验格式
|
|
1458
1675
|
const cfgState = ensureWorkspaceConfig(targetDir);
|
|
1459
|
-
const configPath = path.join(targetDir, '
|
|
1676
|
+
const configPath = path.join(targetDir, 'CBB-WORKSPACE.json');
|
|
1460
1677
|
const configValidation = workspaces.validateWorkspaceConfig(configPath);
|
|
1461
1678
|
if (!configValidation.valid) {
|
|
1462
|
-
step('✗', '配置', `
|
|
1679
|
+
step('✗', '配置', `CBB-WORKSPACE.json 校验失败:${configValidation.message}`,
|
|
1463
1680
|
{ iconColor: 'red', labelColor: 'red', valueColor: 'red' });
|
|
1464
|
-
// 撤销本次克隆,使工作空间回到初始状态,用户配好
|
|
1681
|
+
// 撤销本次克隆,使工作空间回到初始状态,用户配好 CBB-WORKSPACE.json 后可重新执行
|
|
1465
1682
|
if (wasCloned && fs.existsSync(targetDir)) {
|
|
1466
1683
|
removePath(targetDir);
|
|
1467
|
-
log(`已撤销本次克隆(${subdirName}/),请配好
|
|
1684
|
+
log(`已撤销本次克隆(${subdirName}/),请配好 CBB-WORKSPACE.json 后重新执行 cbb setup`, 'warn');
|
|
1468
1685
|
} else {
|
|
1469
|
-
log('请修正
|
|
1686
|
+
log('请修正 CBB-WORKSPACE.json 后重新执行 cbb setup', 'warn');
|
|
1470
1687
|
}
|
|
1471
1688
|
log(`参考格式:${path.join(SHARED_STANDARDS_DIR, workspaces.SAMPLE_CONFIG)}`, 'info');
|
|
1472
1689
|
return { ok: false, error: 'CONFIG_INVALID', message: configValidation.message };
|
|
1473
1690
|
}
|
|
1474
1691
|
if (cfgState === 'created') {
|
|
1475
|
-
step('!', '配置', '
|
|
1692
|
+
step('!', '配置', 'CBB-WORKSPACE.json 已从样例生成,请编辑 apps 后重跑 cbb setup',
|
|
1476
1693
|
{ iconColor: 'yellow', valueColor: 'yellow' });
|
|
1477
1694
|
} else {
|
|
1478
|
-
step('✓', '配置', `
|
|
1695
|
+
step('✓', '配置', `CBB-WORKSPACE.json(${configValidation.config.apps.length} 个应用)`);
|
|
1479
1696
|
}
|
|
1480
1697
|
|
|
1481
1698
|
// 4. 拉取关联应用代码到 .codespace/(基准代码,首次 clone 较慢,实时提示当前应用)
|
|
@@ -1493,11 +1710,11 @@ async function initSingleWorkspace(w, opts = {}) {
|
|
|
1493
1710
|
step(appWarn ? '!' : '✓', '应用', appParts.join(' · '), { iconColor: appWarn ? 'yellow' : 'green' });
|
|
1494
1711
|
if (apps.sampleApps.length > 0) {
|
|
1495
1712
|
const names = apps.sampleApps.map(a => a.name).join('、');
|
|
1496
|
-
console.log(` ${c('gray', `${names} → 编辑
|
|
1713
|
+
console.log(` ${c('gray', `${names} → 编辑 CBB-WORKSPACE.json 填入真实仓库地址`)}`);
|
|
1497
1714
|
}
|
|
1498
1715
|
|
|
1499
|
-
// 5.
|
|
1500
|
-
const decision = decideUpgradeIfInstalled(targetDir);
|
|
1716
|
+
// 5. 若已安装:版本一致且无新增工具才跳过;否则继续安装/升级(setup 已整体确认过,不再二次询问)
|
|
1717
|
+
const decision = decideUpgradeIfInstalled(targetDir, opts.selectedAdapters);
|
|
1501
1718
|
if (decision === 'skip') {
|
|
1502
1719
|
return { ok: true, subdir: subdirName, upToDate: true };
|
|
1503
1720
|
}
|
|
@@ -1519,7 +1736,7 @@ function describeBatchError(r) {
|
|
|
1519
1736
|
case 'DIR_MISMATCH': return '子目录已存在但不是该业务空间的仓库';
|
|
1520
1737
|
case 'CLONE_FAILED': return `git clone 失败:${r.message || ''}`.trim();
|
|
1521
1738
|
case 'PULL_FAILED': return `工作空间仓库同步失败:${r.message || ''}`.trim();
|
|
1522
|
-
case 'CONFIG_INVALID': return `
|
|
1739
|
+
case 'CONFIG_INVALID': return `CBB-WORKSPACE.json 不合法:${r.message || ''}`.trim();
|
|
1523
1740
|
case 'INIT_FAILED': return `初始化失败:${r.message || ''}`.trim();
|
|
1524
1741
|
default: return r.error || '未知错误';
|
|
1525
1742
|
}
|
|
@@ -1592,20 +1809,21 @@ async function setupWorkspace() {
|
|
|
1592
1809
|
.sort((a, b) => a - b)
|
|
1593
1810
|
.map(i => list[i]);
|
|
1594
1811
|
|
|
1595
|
-
// 2.5 选择 AI
|
|
1812
|
+
// 2.5 选择 AI 工具适配器(一次选择,作用于用户级组件与全部空间;非交互默认全选可选工具)
|
|
1813
|
+
const selectable = toolSelection.selectableAdapters(adapters);
|
|
1596
1814
|
let selectedAdapters;
|
|
1597
1815
|
if (!process.stdin.isTTY) {
|
|
1598
|
-
selectedAdapters =
|
|
1816
|
+
selectedAdapters = selectable;
|
|
1599
1817
|
} else {
|
|
1600
1818
|
const toolIdx = await checkbox.promptCheckbox({
|
|
1601
1819
|
header: '🔧 选择要安装的 AI 工具(空格切换,回车确认)',
|
|
1602
|
-
items:
|
|
1820
|
+
items: selectable.map(a => ({ label: c('cyan', a.displayName) })),
|
|
1603
1821
|
// 默认只预选 opencode(团队当前主用),可空格加选其他工具
|
|
1604
|
-
preSelected:
|
|
1822
|
+
preSelected: selectable.reduce((acc, a, i) => (a.name === 'opencode' ? acc.concat(i) : acc), []),
|
|
1605
1823
|
});
|
|
1606
1824
|
selectedAdapters = toolIdx.size === 0
|
|
1607
|
-
?
|
|
1608
|
-
: [...toolIdx].sort((a, b) => a - b).map(i =>
|
|
1825
|
+
? selectable
|
|
1826
|
+
: [...toolIdx].sort((a, b) => a - b).map(i => selectable[i]);
|
|
1609
1827
|
}
|
|
1610
1828
|
|
|
1611
1829
|
// 3. 配置摘要(业务空间 / 目标目录 / AI 工具)
|
|
@@ -1661,7 +1879,7 @@ const USER_MANIFEST_FILE = path.join(os.homedir(), '.cbb', '.user-components');
|
|
|
1661
1879
|
// 当前为空:cbb-wiki-ops 保留在源仓库(cbb/tools/)但不分发,有用户需要时再加入此清单。
|
|
1662
1880
|
const USER_LEVEL_SKILLS = [];
|
|
1663
1881
|
|
|
1664
|
-
async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
1882
|
+
async function setupUserLevelComponents(targetAdapters = toolSelection.selectableAdapters(adapters)) {
|
|
1665
1883
|
const home = os.homedir();
|
|
1666
1884
|
const srcDir = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
|
|
1667
1885
|
if (!fs.existsSync(srcDir)) return;
|
|
@@ -1698,12 +1916,29 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
|
1698
1916
|
if (hookAdapters.length > 0) {
|
|
1699
1917
|
const command = settingsManager.getCheckUpdateCommand();
|
|
1700
1918
|
hookAdapters.forEach(adapter => {
|
|
1701
|
-
settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents);
|
|
1702
|
-
settingsManager.removeLegacyHooks(adapter.settingsFile);
|
|
1919
|
+
settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents, hookOpts(adapter));
|
|
1920
|
+
settingsManager.removeLegacyHooks(adapter.settingsFile, hookOpts(adapter));
|
|
1921
|
+
cleanLegacySettings(adapter);
|
|
1703
1922
|
});
|
|
1704
1923
|
summaryParts.push(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join('、')}`);
|
|
1705
1924
|
}
|
|
1706
1925
|
|
|
1926
|
+
// 本地插件 hook(MiniMax:无 settings.json hooks,走 <dataDir>/plugins 本地插件,幂等覆盖)
|
|
1927
|
+
const pluginHookAdapters = targetAdapters.filter(a => a.pluginHook);
|
|
1928
|
+
if (pluginHookAdapters.length > 0) {
|
|
1929
|
+
const command = settingsManager.getCheckUpdateCommand();
|
|
1930
|
+
const pkgVersion = require(path.join(SHARED_STANDARDS_DIR, 'package.json')).version;
|
|
1931
|
+
pluginHookAdapters.forEach(adapter => {
|
|
1932
|
+
pluginHookManager.install({
|
|
1933
|
+
dataDir: adapter.resolveDataDir(),
|
|
1934
|
+
...adapter.pluginHook,
|
|
1935
|
+
command,
|
|
1936
|
+
version: pkgVersion,
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
summaryParts.push(`版本检查插件 → ${pluginHookAdapters.map(a => a.displayName).join('、')}`);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1707
1942
|
if (summaryParts.length > 0) step('✓', '用户级组件', summaryParts.join(' · '));
|
|
1708
1943
|
|
|
1709
1944
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
@@ -1716,14 +1951,14 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
|
1716
1951
|
/**
|
|
1717
1952
|
* cbb update:就地升级当前目录内容 + 用户级组件。
|
|
1718
1953
|
*
|
|
1719
|
-
* 单层安装:统一安装器按空间根判定(
|
|
1954
|
+
* 单层安装:统一安装器按空间根判定(CBB-WORKSPACE.json + .git 目录)补齐
|
|
1720
1955
|
* worktree 管理入口与空间导航;旧子集清单升级时由清单差集机制自动补齐全量内容。
|
|
1721
1956
|
*/
|
|
1722
1957
|
async function updateInstalled() {
|
|
1723
1958
|
// 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量
|
|
1724
1959
|
const lastTools = readLastTools();
|
|
1725
1960
|
const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
|
|
1726
|
-
await setupUserLevelComponents(reused.length > 0 ? reused : adapters);
|
|
1961
|
+
await setupUserLevelComponents(reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters));
|
|
1727
1962
|
|
|
1728
1963
|
const manifest = readManifest();
|
|
1729
1964
|
if (manifest.size === 0) {
|
|
@@ -1815,9 +2050,12 @@ async function uninstallUserLevelComponents() {
|
|
|
1815
2050
|
// 移除版本检查 hook(保留用户其余配置)
|
|
1816
2051
|
adapters.forEach(adapter => {
|
|
1817
2052
|
if (!adapter.settingsFile || !adapter.hookEvents) return;
|
|
1818
|
-
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents)) {
|
|
2053
|
+
if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) {
|
|
1819
2054
|
log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
|
|
1820
2055
|
}
|
|
2056
|
+
if (cleanLegacySettings(adapter)) {
|
|
2057
|
+
log(`${adapter.legacySettingsFile}: 移除版本检查 hook(旧路径),保留用户配置`, 'success');
|
|
2058
|
+
}
|
|
1821
2059
|
});
|
|
1822
2060
|
|
|
1823
2061
|
removePath(USER_MANIFEST_FILE);
|
|
@@ -1846,7 +2084,7 @@ async function uninstall() {
|
|
|
1846
2084
|
// 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
|
|
1847
2085
|
let removeUserLevel = false;
|
|
1848
2086
|
if (fs.existsSync(USER_MANIFEST_FILE)) {
|
|
1849
|
-
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder 的版本检查 hook
|
|
2087
|
+
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.workbuddy、~/.trae-cn 的版本检查 hook、MiniMax Code 本地插件)?(y/N): ');
|
|
1850
2088
|
removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
|
|
1851
2089
|
}
|
|
1852
2090
|
|
|
@@ -1865,16 +2103,35 @@ async function uninstall() {
|
|
|
1865
2103
|
: path.join(TARGET_DIR, adapter.settingsFile);
|
|
1866
2104
|
// 项目级:需在清单中才处理
|
|
1867
2105
|
if (!adapter.settingsFileIsUserLevel && !managedPaths.has(adapter.settingsFile)) return;
|
|
1868
|
-
if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents)) {
|
|
2106
|
+
if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents, hookOpts(adapter))) {
|
|
1869
2107
|
log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
|
|
1870
2108
|
}
|
|
1871
2109
|
// 清理旧版本遗留的 SessionStart hook(无版本差异,无条件清理)
|
|
1872
|
-
if (settingsManager.removeLegacyHooks(settingsPath)) {
|
|
2110
|
+
if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
|
|
1873
2111
|
log(`${adapter.settingsFile}: 清理遗留 SessionStart hook`, 'success');
|
|
1874
2112
|
}
|
|
1875
2113
|
skippedPaths.add(adapter.settingsFile);
|
|
1876
2114
|
});
|
|
1877
2115
|
|
|
2116
|
+
// 本地插件 hook(MiniMax,用户级组件):仅确认卸载用户级时删除插件目录
|
|
2117
|
+
if (removeUserLevel) {
|
|
2118
|
+
adapters.forEach(adapter => {
|
|
2119
|
+
if (!adapter.pluginHook) return;
|
|
2120
|
+
if (pluginHookManager.remove({ dataDir: adapter.resolveDataDir(), ...adapter.pluginHook })) {
|
|
2121
|
+
log(`移除 ${adapter.displayName} 版本检查插件(${adapter.resolveDataDir()})`, 'success');
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// AGENTS.md 规则导航块:本包托管块精确摘除(块外用户内容不动;文件归用户所有不进清单)
|
|
2127
|
+
adapters.forEach(adapter => {
|
|
2128
|
+
if (!adapter.agentsNav) return;
|
|
2129
|
+
const target = path.join(TARGET_DIR, adapter.agentsNav.file);
|
|
2130
|
+
if (agentsNav.remove(target, adapter.name)) {
|
|
2131
|
+
log(`移除 ${adapter.agentsNav.file} 规则导航块`, 'success');
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
|
|
1878
2135
|
// opencode.json:移除本包登记的 instructions pattern(cbb 创建的整体文件由清单删除兜底)
|
|
1879
2136
|
removeInstructionsRegistrations();
|
|
1880
2137
|
|
|
@@ -1894,22 +2151,7 @@ async function uninstall() {
|
|
|
1894
2151
|
});
|
|
1895
2152
|
|
|
1896
2153
|
// 清理可能变空的父目录(从深到浅)
|
|
1897
|
-
|
|
1898
|
-
managedPaths.forEach(relativePath => {
|
|
1899
|
-
let dir = path.dirname(relativePath);
|
|
1900
|
-
while (dir && dir !== '.') {
|
|
1901
|
-
dirsToCheck.add(dir);
|
|
1902
|
-
dir = path.dirname(dir);
|
|
1903
|
-
}
|
|
1904
|
-
});
|
|
1905
|
-
// 按深度排序,深层先处理
|
|
1906
|
-
[...dirsToCheck].sort((a, b) => b.split('/').length - a.split('/').length).forEach(dir => {
|
|
1907
|
-
const fullPath = path.join(TARGET_DIR, dir);
|
|
1908
|
-
if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
|
|
1909
|
-
removePath(fullPath);
|
|
1910
|
-
log(`清理空目录 ${dir}/`, 'success');
|
|
1911
|
-
}
|
|
1912
|
-
});
|
|
2154
|
+
cleanupEmptyParentDirs(managedPaths).forEach(dir => log(`清理空目录 ${dir}/`, 'success'));
|
|
1913
2155
|
|
|
1914
2156
|
// 删除清单文件和 .va 目录
|
|
1915
2157
|
removePath(manifestDir);
|
|
@@ -1918,6 +2160,22 @@ async function uninstall() {
|
|
|
1918
2160
|
// 清理 .gitignore
|
|
1919
2161
|
cleanupGitignore();
|
|
1920
2162
|
|
|
2163
|
+
// 移除分支感知提交防线(ADR 0002):空间根 + .codespace/ 各应用(业务空间根专属)
|
|
2164
|
+
if (detectWorkspaceRoot()) {
|
|
2165
|
+
const results = [githook.uninstallGitHook(TARGET_DIR)];
|
|
2166
|
+
const codeDir = path.join(TARGET_DIR, '.codespace');
|
|
2167
|
+
if (fs.existsSync(codeDir)) {
|
|
2168
|
+
for (const name of fs.readdirSync(codeDir)) {
|
|
2169
|
+
const appDir = path.join(codeDir, name);
|
|
2170
|
+
if (fs.existsSync(path.join(appDir, '.git'))) {
|
|
2171
|
+
results.push(githook.uninstallGitHook(appDir));
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const removedHooks = results.filter(r => r !== 'noop').length;
|
|
2176
|
+
if (removedHooks > 0) log(`移除提交防线 pre-commit hook(${removedHooks} 个仓库)`, 'success');
|
|
2177
|
+
}
|
|
2178
|
+
|
|
1921
2179
|
// 卸载用户级组件(如已确认)
|
|
1922
2180
|
if (removeUserLevel) {
|
|
1923
2181
|
console.log('');
|
|
@@ -1927,12 +2185,68 @@ async function uninstall() {
|
|
|
1927
2185
|
console.log(`\n✅ 卸载完成!共移除 ${removed} 个文件/目录\n`);
|
|
1928
2186
|
}
|
|
1929
2187
|
|
|
2188
|
+
// ── 上游版本检查 ──────────────────────────────────────────────
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* 检查 openspec / superpowers / external 是否有上游新版本(只读,不改动任何文件)。
|
|
2192
|
+
* 供 cbbm check-upstream 调用。
|
|
2193
|
+
*/
|
|
2194
|
+
async function checkUpstream() {
|
|
2195
|
+
const results = await Promise.allSettled([
|
|
2196
|
+
openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
|
|
2197
|
+
superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
|
|
2198
|
+
external.checkUpstream().then(list => ({ name: 'external', list })),
|
|
2199
|
+
]);
|
|
2200
|
+
|
|
2201
|
+
let outdated = false;
|
|
2202
|
+
for (const r of results) {
|
|
2203
|
+
if (r.status === 'rejected') {
|
|
2204
|
+
log(`检查失败: ${r.reason.message}`, 'error');
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
// external 是多依赖聚合结果,逐依赖渲染
|
|
2208
|
+
if (r.value.name === 'external') {
|
|
2209
|
+
for (const item of r.value.list) {
|
|
2210
|
+
if (item.error) {
|
|
2211
|
+
log(`external/${item.name}: 检查失败: ${item.error}`, 'error');
|
|
2212
|
+
} else if (!item.latest) {
|
|
2213
|
+
log(`external/${item.name}: 无法获取上游版本`, 'warn');
|
|
2214
|
+
} else if (item.outdated) {
|
|
2215
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} → ${item.latest.slice(0, 12)}(上游有新提交)`, 'warn');
|
|
2216
|
+
outdated = true;
|
|
2217
|
+
} else {
|
|
2218
|
+
log(`external/${item.name}: ${item.current.slice(0, 12)} 已是最新`, 'success');
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const { name, current, latest, outdated: isOutdated } = r.value;
|
|
2224
|
+
if (!latest) {
|
|
2225
|
+
log(`${name}: 无法获取上游版本`, 'warn');
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (!current) {
|
|
2229
|
+
log(`${name}: 本地无 pin(上游最新 ${latest})`, 'warn');
|
|
2230
|
+
outdated = true;
|
|
2231
|
+
} else if (isOutdated) {
|
|
2232
|
+
log(`${name}: ${current} → ${latest}(有新版本)`, 'warn');
|
|
2233
|
+
outdated = true;
|
|
2234
|
+
} else {
|
|
2235
|
+
log(`${name}: ${current} 已是最新`, 'success');
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
if (outdated) {
|
|
2240
|
+
log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers / --upstream-external', 'info');
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
1930
2244
|
// ── CLI 入口 ──────────────────────────────────────────────
|
|
1931
2245
|
|
|
1932
2246
|
// 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
|
|
1933
2247
|
const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
|
|
1934
|
-
const INTERNAL_COMMANDS = ['check-update', 'wiki', 'wp'];
|
|
1935
|
-
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
|
|
2248
|
+
const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
|
|
2249
|
+
const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec', '--upstream-external'];
|
|
1936
2250
|
|
|
1937
2251
|
const args = process.argv.slice(2);
|
|
1938
2252
|
const command = args[0];
|
|
@@ -1940,11 +2254,11 @@ const restArgs = args.slice(1);
|
|
|
1940
2254
|
const hasUpstreamFlag = restArgs.some(a => UPSTREAM_FLAGS.includes(a));
|
|
1941
2255
|
|
|
1942
2256
|
if (ENTRY === 'cbb' && command === 'update' && hasUpstreamFlag) {
|
|
1943
|
-
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec', 'error');
|
|
2257
|
+
log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec / --upstream-external', 'error');
|
|
1944
2258
|
process.exit(1);
|
|
1945
2259
|
}
|
|
1946
2260
|
if (ENTRY === 'cbbm' && command === 'update' && !hasUpstreamFlag) {
|
|
1947
|
-
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec)', 'error');
|
|
2261
|
+
log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec / --upstream-external)', 'error');
|
|
1948
2262
|
log('升级已安装内容请使用 cbb update', 'warn');
|
|
1949
2263
|
process.exit(1);
|
|
1950
2264
|
}
|
|
@@ -1969,12 +2283,13 @@ switch (command) {
|
|
|
1969
2283
|
case 'update': {
|
|
1970
2284
|
const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
|
|
1971
2285
|
const upstreamOpenspec = restArgs.includes('--upstream-openspec');
|
|
1972
|
-
|
|
1973
|
-
|
|
2286
|
+
const upstreamExternal = restArgs.includes('--upstream-external');
|
|
2287
|
+
if (upstreamSuperpowers || upstreamOpenspec || upstreamExternal) {
|
|
2288
|
+
// 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
|
|
1974
2289
|
if (upstreamSuperpowers) {
|
|
1975
2290
|
superpowers.fetchLatestSkills().then(result => {
|
|
1976
2291
|
if (result.skipped) {
|
|
1977
|
-
log(`superpowers skills 已是最新 (${result.
|
|
2292
|
+
log(`superpowers skills 已是最新 (${result.tag})`, 'success');
|
|
1978
2293
|
}
|
|
1979
2294
|
const validation = superpowers.validateSkills();
|
|
1980
2295
|
if (!validation.valid) {
|
|
@@ -1990,22 +2305,33 @@ switch (command) {
|
|
|
1990
2305
|
}
|
|
1991
2306
|
if (upstreamOpenspec) {
|
|
1992
2307
|
openspec.fetchLatestOpenSpec().then(result => {
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
}
|
|
1999
|
-
log(`openspec 工作流完整性校验通过`, 'success');
|
|
2000
|
-
} else {
|
|
2001
|
-
const validation = openspec.validateDeployed();
|
|
2002
|
-
if (!validation.valid) {
|
|
2003
|
-
log(`本地 openspec 工作流不完整,请重新运行 cbb update --upstream-openspec:`, 'error');
|
|
2004
|
-
for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
|
|
2005
|
-
process.exit(1);
|
|
2006
|
-
}
|
|
2007
|
-
log(`openspec 工作流完整性校验通过`, 'success');
|
|
2308
|
+
const validation = openspec.validateDeployed();
|
|
2309
|
+
if (!validation.valid) {
|
|
2310
|
+
log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
|
|
2311
|
+
for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
|
|
2312
|
+
process.exit(1);
|
|
2008
2313
|
}
|
|
2314
|
+
log(`openspec 工作流完整性校验通过`, 'success');
|
|
2315
|
+
}).catch(err => {
|
|
2316
|
+
log(`更新失败: ${err.message}`, 'error');
|
|
2317
|
+
process.exit(1);
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
if (upstreamExternal) {
|
|
2321
|
+
// 可选依赖名参数:cbbm update --upstream-external [name](省略 = 全部)
|
|
2322
|
+
const extNames = [];
|
|
2323
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
2324
|
+
if (restArgs[i] === '--upstream-external' && restArgs[i + 1] && !restArgs[i + 1].startsWith('-')) {
|
|
2325
|
+
extNames.push(restArgs[i + 1]);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
external.fetchDeps(extNames).then(results => {
|
|
2329
|
+
const failed = results.filter(r => r.error);
|
|
2330
|
+
if (failed.length > 0) {
|
|
2331
|
+
failed.forEach(r => log(`external/${r.name}: 更新失败: ${r.error}`, 'error'));
|
|
2332
|
+
process.exit(1);
|
|
2333
|
+
}
|
|
2334
|
+
log('external 外部依赖完整性校验通过', 'success');
|
|
2009
2335
|
}).catch(err => {
|
|
2010
2336
|
log(`更新失败: ${err.message}`, 'error');
|
|
2011
2337
|
process.exit(1);
|
|
@@ -2028,6 +2354,12 @@ switch (command) {
|
|
|
2028
2354
|
case 'check-update':
|
|
2029
2355
|
require('../utils/check-update.js');
|
|
2030
2356
|
break;
|
|
2357
|
+
case 'check-upstream':
|
|
2358
|
+
checkUpstream().catch(err => {
|
|
2359
|
+
log(err.message, 'error');
|
|
2360
|
+
process.exit(1);
|
|
2361
|
+
});
|
|
2362
|
+
break;
|
|
2031
2363
|
case 'wp':
|
|
2032
2364
|
require('../wiki/index.js').publish(restArgs);
|
|
2033
2365
|
break;
|
|
@@ -2059,11 +2391,13 @@ function showHelp() {
|
|
|
2059
2391
|
@jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
|
|
2060
2392
|
|
|
2061
2393
|
cbbm check-update 版本检查(用户级 hook 每日自动触发)
|
|
2394
|
+
cbbm check-upstream 检查 openspec / superpowers / external 是否有上游新版本
|
|
2062
2395
|
cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
|
|
2063
2396
|
|
|
2064
|
-
|
|
2065
|
-
cbbm update --upstream-superpowers
|
|
2066
|
-
cbbm update --upstream-openspec
|
|
2397
|
+
维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
|
|
2398
|
+
cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
|
|
2399
|
+
cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
|
|
2400
|
+
cbbm update --upstream-external [名] 按 pin sha 拉取 external 外部依赖(省略名 = 全部,走镜像)
|
|
2067
2401
|
`);
|
|
2068
2402
|
return;
|
|
2069
2403
|
}
|