@jspg-ai/coding-bb 0.0.3-beta.27 → 0.0.3-beta.29
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 +1 -1
- package/cbb/lib/install/agents-nav.js +92 -0
- package/cbb/lib/install/command-skill.js +105 -0
- package/cbb/lib/install/init.js +133 -49
- package/cbb/lib/install/minimax.js +53 -0
- package/cbb/lib/utils/check-update.js +6 -2
- package/cbb/lib/utils/gitignore.js +2 -0
- package/cbb/lib/utils/plugin-hook.js +92 -0
- package/cbb/worktrees/_shared/scripts/junction-ai-config.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +10 -8
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +9 -6
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +1 -1
- package/package.json +2 -2
|
@@ -112,7 +112,7 @@ trigger: always_on
|
|
|
112
112
|
**模式二 · worktree 模式(并行 / 隔离):**
|
|
113
113
|
|
|
114
114
|
- `cbb-worktree-init` 建隔离 worktree(AI 配置内容子目录已 junction 就位),**开发会话开进 `.worktrees/worktree-<需求名>/`**,可多需求并行;app 检出收拢在 `<wt>/.codespace/<app>/`——**镜像空间根布局**(`.codespace/<app>/...` 相对路径两模式通用,ADR 0003)
|
|
115
|
-
- 根会话保留作控制面(worktree 生灭、`cbb update`
|
|
115
|
+
- 根会话保留作控制面(worktree 生灭、`cbb update` 等空间级操作);**会话形态按工具定,能自动则 agent 亲自做**:OpenCode 桌面端 = agent 自动把会话切进/切出 worktree(`session_move`,上下文保留、不列入用户待办,空间根路径在案供回切);无会话搬移能力的桌面端 = 用户手动切根;CLI/TUI = 新开会话进 worktree
|
|
116
116
|
|
|
117
117
|
**基准与防线(两模式共同,ADR 0002):**
|
|
118
118
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AGENTS.md 托管导航块(agentsNav)。
|
|
5
|
+
*
|
|
6
|
+
* MiniMax Code 不读取 .minimax/rules(源码无该逻辑),其项目级规则注入通道
|
|
7
|
+
* 只有工作区根 AGENTS.md(local-runtime/src/project/instructions.ts,32KB 预算)。
|
|
8
|
+
* 安装时在 AGENTS.md 注入一句托管导航,引导 AI 先读取规则;块形如:
|
|
9
|
+
*
|
|
10
|
+
* <!-- cbb:agents-nav:minimax begin -->
|
|
11
|
+
* ⚠️ 规则优先:……
|
|
12
|
+
* <!-- cbb:agents-nav:minimax end -->
|
|
13
|
+
*
|
|
14
|
+
* 约束:
|
|
15
|
+
* - 幂等:块内容随安装更新,块外用户内容永不改动;
|
|
16
|
+
* - 标记带适配器名,多适配器共存不互撞;
|
|
17
|
+
* - 卸载只摘除本包块;摘除后文件为空(本包创建的情形)则删除文件。
|
|
18
|
+
*
|
|
19
|
+
* AGENTS.md 本身不进清单(cleanup.js 的 PROTECTED_FROM_CLEANUP 同样保护它),
|
|
20
|
+
* 托管块的注入与摘除由 init() 安装流程 / uninstall() 按适配器显式处理。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
/** 转义正则元字符(适配器名入正则上下文) */
|
|
27
|
+
function escapeRe(value) {
|
|
28
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function markersFor(adapterName) {
|
|
32
|
+
const safe = escapeRe(adapterName);
|
|
33
|
+
return {
|
|
34
|
+
begin: `<!-- cbb:agents-nav:${adapterName} begin -->`,
|
|
35
|
+
end: `<!-- cbb:agents-nav:${adapterName} end -->`,
|
|
36
|
+
// [\s\S]*? 非贪婪:块内可含任意内容(含 \r),遇最近的 end 标记即止
|
|
37
|
+
blockRe: new RegExp(`<!-- cbb:agents-nav:${safe} begin -->[\\s\\S]*?<!-- cbb:agents-nav:${safe} end -->`, 'm'),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 注入(或更新)导航块。
|
|
43
|
+
* @param {string} filePath 目标 AGENTS.md 绝对路径
|
|
44
|
+
* @param {string} adapterName 适配器名(标记命名空间)
|
|
45
|
+
* @param {string} text 导航句
|
|
46
|
+
* @returns {'created'|'updated'|'unchanged'} created = 文件原不存在或为空白
|
|
47
|
+
*/
|
|
48
|
+
function inject(filePath, adapterName, text) {
|
|
49
|
+
const { begin, end, blockRe } = markersFor(adapterName);
|
|
50
|
+
const existed = fs.existsSync(filePath);
|
|
51
|
+
const content = existed ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
52
|
+
// 行尾与既有文件保持一致(Windows 生成的 AGENTS.md 多为 CRLF)
|
|
53
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
54
|
+
const block = `${begin}${eol}${text}${eol}${end}`;
|
|
55
|
+
|
|
56
|
+
if (blockRe.test(content)) {
|
|
57
|
+
const next = content.replace(blockRe, block);
|
|
58
|
+
if (next === content) return 'unchanged';
|
|
59
|
+
fs.writeFileSync(filePath, next);
|
|
60
|
+
return 'updated';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const wasBlank = content.trim() === '';
|
|
64
|
+
const base = content.replace(/\n*$/, ''); // 收敛尾部空行,块另起段落
|
|
65
|
+
const next = wasBlank ? `${block}${eol}` : `${base}${eol}${eol}${block}${eol}`;
|
|
66
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
67
|
+
fs.writeFileSync(filePath, next);
|
|
68
|
+
return wasBlank && !existed ? 'created' : wasBlank ? 'updated' : 'created';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 精确摘除导航块(块外内容不动)。
|
|
73
|
+
* @returns {boolean} 是否有改动(块不存在 → false)
|
|
74
|
+
*/
|
|
75
|
+
function remove(filePath, adapterName) {
|
|
76
|
+
if (!fs.existsSync(filePath)) return false;
|
|
77
|
+
const { blockRe } = markersFor(adapterName);
|
|
78
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
79
|
+
if (!blockRe.test(content)) return false;
|
|
80
|
+
|
|
81
|
+
// 只删块本身;随后收敛尾部空白(块移除后的残留空行)
|
|
82
|
+
const next = content.replace(blockRe, '').replace(/\s+$/, '');
|
|
83
|
+
if (next.trim() === '') {
|
|
84
|
+
// 文件只剩本包内容(由本包创建的情形)→ 连文件一起删除
|
|
85
|
+
fs.unlinkSync(filePath);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(filePath, next + '\n');
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { inject, remove };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openspec 命令 → skill 形态转换(commandsAsSkills 适配器用,当前仅 MiniMax Code)。
|
|
5
|
+
*
|
|
6
|
+
* MiniMax Code 无项目级 commands 目录,skill 即其命令形态:每个 openspec 命令
|
|
7
|
+
* 落为 <skillsDir>/<prefix><name>/SKILL.md,目录名 = frontmatter name(Agent
|
|
8
|
+
* Skills 约定),/opsx-propose 斜杠照常可用。
|
|
9
|
+
*
|
|
10
|
+
* 同时承接命令 frontmatter 消毒(sanitizeCommandFrontmatter):opencode 新版
|
|
11
|
+
* 校验命令配置(description 必须为 string | undefined),上游个别命令
|
|
12
|
+
* (openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
|
|
13
|
+
* 会拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述(超过 120 字
|
|
14
|
+
* 截断),上游源文件不动(CLI 接管区)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 命令文件 → skill 目录名(文件名基准,保证目录名稳定)。
|
|
21
|
+
* @param {string} fileName 如 propose.md
|
|
22
|
+
* @param {string} prefix 如 opsx-
|
|
23
|
+
* @returns {string} 如 opsx-propose
|
|
24
|
+
*/
|
|
25
|
+
function skillNameFor(fileName, prefix) {
|
|
26
|
+
return (prefix || '') + path.basename(fileName, '.md');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 取 frontmatter(首个 --- 到闭合 --- 之间);无 frontmatter 返回 -1 */
|
|
30
|
+
function findFrontmatterEnd(lines) {
|
|
31
|
+
if ((lines[0] || '').trim() !== '---') return -1;
|
|
32
|
+
for (let i = 1; i < lines.length; i++) {
|
|
33
|
+
if ((lines[i] || '').trim() === '---') return i;
|
|
34
|
+
}
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 命令 frontmatter 消毒(见文件头注释)。无 frontmatter 或无需改动时原样返回。
|
|
40
|
+
*/
|
|
41
|
+
function sanitizeCommandFrontmatter(content) {
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
if ((lines[0] || '').trim() !== '---') return content;
|
|
44
|
+
const end = findFrontmatterEnd(lines);
|
|
45
|
+
if (end === -1) return content;
|
|
46
|
+
let sentence = '';
|
|
47
|
+
for (let i = end + 1; i < lines.length; i++) {
|
|
48
|
+
const t = (lines[i] || '').trim();
|
|
49
|
+
if (t) { sentence = t; break; }
|
|
50
|
+
}
|
|
51
|
+
let changed = false;
|
|
52
|
+
for (let i = 1; i < end; i++) {
|
|
53
|
+
if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
|
|
54
|
+
if (sentence) {
|
|
55
|
+
if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
|
|
56
|
+
lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
|
|
57
|
+
} else {
|
|
58
|
+
lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
|
|
59
|
+
}
|
|
60
|
+
changed = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return changed ? lines.filter(l => l !== null).join('\n') : content;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 把命令内容转换为 skill 的 SKILL.md 内容:
|
|
68
|
+
* 1. frontmatter description 消毒(null → 回填正文首句);
|
|
69
|
+
* 2. frontmatter name 强制为 skillName —— 目录名 = name 是 Agent Skills 约定,
|
|
70
|
+
* 也规避上游标题与文件名不一致(如 ff.md 的 "OPSX: Fast Forward")。
|
|
71
|
+
*
|
|
72
|
+
* 上游 frontmatter 的其余字段(category / tags 等)原样保留。
|
|
73
|
+
*
|
|
74
|
+
* @param {string} content 命令原文(调用方已应用 contentRewrites)
|
|
75
|
+
* @param {string} skillName 目标 skill 名(= 目录名)
|
|
76
|
+
* @returns {string} SKILL.md 内容
|
|
77
|
+
*/
|
|
78
|
+
function convertCommandToSkill(content, skillName) {
|
|
79
|
+
const sanitized = sanitizeCommandFrontmatter(content);
|
|
80
|
+
const lines = sanitized.split('\n');
|
|
81
|
+
|
|
82
|
+
// 无 frontmatter:补建(name 必填;description 回填正文首句)
|
|
83
|
+
if ((lines[0] || '').trim() !== '---') {
|
|
84
|
+
const sentence = (lines.find(l => l.trim()) || '').trim().slice(0, 120);
|
|
85
|
+
const fm = ['---', `name: "${skillName}"`];
|
|
86
|
+
if (sentence) fm.push(`description: ${JSON.stringify(sentence)}`);
|
|
87
|
+
fm.push('---', '');
|
|
88
|
+
return fm.concat(lines).join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const end = findFrontmatterEnd(lines);
|
|
92
|
+
if (end === -1) return sanitized; // 开了没闭合的异常 frontmatter:不动
|
|
93
|
+
|
|
94
|
+
let nameLine = -1;
|
|
95
|
+
for (let i = 1; i < end; i++) {
|
|
96
|
+
if (/^name:/.test(lines[i])) { nameLine = i; break; }
|
|
97
|
+
}
|
|
98
|
+
const nameValue = `name: "${skillName}"`;
|
|
99
|
+
if (nameLine === -1) lines.splice(1, 0, nameValue); // 缺 name:紧跟开标记插入
|
|
100
|
+
else lines[nameLine] = nameValue + (lines[nameLine].endsWith('\r') ? '\r' : '');
|
|
101
|
+
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { skillNameFor, sanitizeCommandFrontmatter, convertCommandToSkill };
|
package/cbb/lib/install/init.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 将AI 编码规范与工作流套件安装到目标项目中。
|
|
7
7
|
* 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)、
|
|
8
|
-
* WorkBuddy (.workbuddy/rules/)、TraeWork/TraeCode 中国版 (.trae/rules/,用户级 hook 在 ~/.trae-cn)
|
|
8
|
+
* WorkBuddy (.workbuddy/rules/)、TraeWork/TraeCode 中国版 (.trae/rules/,用户级 hook 在 ~/.trae-cn)、
|
|
9
|
+
* MiniMax Code (.minimax/rules/,规则经 AGENTS.md 托管导航加载,命令转 skill,hook 走用户级本地插件)
|
|
9
10
|
*
|
|
10
11
|
* 使用方式:
|
|
11
12
|
* npm install -g @jspg-ai/coding-bb # 首次使用
|
|
@@ -60,6 +61,7 @@ const adapters = [
|
|
|
60
61
|
require('./trae'),
|
|
61
62
|
require('./qoder'),
|
|
62
63
|
require('./claude-code'),
|
|
64
|
+
require('./minimax'),
|
|
63
65
|
];
|
|
64
66
|
|
|
65
67
|
// superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 15 个,
|
|
@@ -89,6 +91,15 @@ const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
|
|
|
89
91
|
// settings.json hook 工具
|
|
90
92
|
const settingsManager = require('../utils/settings');
|
|
91
93
|
|
|
94
|
+
// MiniMax 本地插件 hook(无 settings.json hooks 的适配器走插件形态)
|
|
95
|
+
const pluginHookManager = require('../utils/plugin-hook');
|
|
96
|
+
|
|
97
|
+
// AGENTS.md 托管导航块(agentsNav 适配器的项目级规则注入通道)
|
|
98
|
+
const agentsNav = require('./agents-nav');
|
|
99
|
+
|
|
100
|
+
// 命令 → skill 形态转换(commandsAsSkills 适配器,当前仅 MiniMax Code)
|
|
101
|
+
const commandSkill = require('./command-skill');
|
|
102
|
+
|
|
92
103
|
/** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
|
|
93
104
|
function hookOpts(adapter) {
|
|
94
105
|
return adapter.hookDialect ? { dialect: adapter.hookDialect } : {};
|
|
@@ -639,47 +650,13 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
|
|
|
639
650
|
const srcPath = path.join(srcDir, f);
|
|
640
651
|
const destPath = path.join(destDir, prefix + f);
|
|
641
652
|
removePath(destPath);
|
|
642
|
-
const content = rewriteCommandName(adapter, sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
|
|
653
|
+
const content = rewriteCommandName(adapter, commandSkill.sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
|
|
643
654
|
fs.writeFileSync(destPath, content);
|
|
644
655
|
});
|
|
645
656
|
|
|
646
657
|
return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
|
|
647
658
|
}
|
|
648
659
|
|
|
649
|
-
/**
|
|
650
|
-
* 命令 frontmatter 消毒:opencode 新版校验命令配置(description 必须为 string | undefined),
|
|
651
|
-
* 上游个别命令(openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
|
|
652
|
-
* 会导致 opencode 拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述
|
|
653
|
-
* (超过 120 字截断),上游源文件不动(CLI 接管区)。
|
|
654
|
-
*/
|
|
655
|
-
function sanitizeCommandFrontmatter(content) {
|
|
656
|
-
const lines = content.split('\n');
|
|
657
|
-
if ((lines[0] || '').trim() !== '---') return content;
|
|
658
|
-
let end = -1;
|
|
659
|
-
for (let i = 1; i < lines.length; i++) {
|
|
660
|
-
if ((lines[i] || '').trim() === '---') { end = i; break; }
|
|
661
|
-
}
|
|
662
|
-
if (end === -1) return content;
|
|
663
|
-
let sentence = '';
|
|
664
|
-
for (let i = end + 1; i < lines.length; i++) {
|
|
665
|
-
const t = (lines[i] || '').trim();
|
|
666
|
-
if (t) { sentence = t; break; }
|
|
667
|
-
}
|
|
668
|
-
let changed = false;
|
|
669
|
-
for (let i = 1; i < end; i++) {
|
|
670
|
-
if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
|
|
671
|
-
if (sentence) {
|
|
672
|
-
if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
|
|
673
|
-
lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
|
|
674
|
-
} else {
|
|
675
|
-
lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
|
|
676
|
-
}
|
|
677
|
-
changed = true;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return changed ? lines.filter(l => l !== null).join('\n') : content;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
660
|
/**
|
|
684
661
|
* opencode 命令显示名取自 frontmatter name,上游为大写空格风格("OPSX: Propose"),
|
|
685
662
|
* 与文档写法(/opsx-propose)不一致;部署副本统一重写为 kebab-case。
|
|
@@ -692,11 +669,49 @@ function rewriteCommandName(adapter, content) {
|
|
|
692
669
|
});
|
|
693
670
|
}
|
|
694
671
|
|
|
695
|
-
/**
|
|
672
|
+
/**
|
|
673
|
+
* MiniMax:无项目级 commands 目录,命令转 skill 形态(skill 即其斜杠命令),
|
|
674
|
+
* 每个命令落为 <skillsDir>/<prefix><name>/SKILL.md,目录名 = frontmatter name。
|
|
675
|
+
* @returns {{count:number, names:string[], destDir:string|null}} destDir 为清单登记用相对目录
|
|
676
|
+
*/
|
|
677
|
+
function deployCommandSkills(adapter, srcDir, filter) {
|
|
678
|
+
if (!fs.existsSync(srcDir)) return { count: 0, names: [], destDir: null };
|
|
679
|
+
const destDir = adapter.skillsDir;
|
|
680
|
+
const skillsRoot = path.join(TARGET_DIR, destDir);
|
|
681
|
+
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
682
|
+
|
|
683
|
+
// 与 skill 同构的扁平 .md 源文件(命令不是子目录)
|
|
684
|
+
const commandFiles = fs.readdirSync(srcDir)
|
|
685
|
+
.filter(f => f.endsWith('.md') && fs.statSync(path.join(srcDir, f)).isFile())
|
|
686
|
+
.filter(f => !filter || filter.includes(f));
|
|
687
|
+
|
|
688
|
+
const names = [];
|
|
689
|
+
commandFiles.forEach(f => {
|
|
690
|
+
const skillName = commandSkill.skillNameFor(f, adapter.commandPrefix || '');
|
|
691
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
692
|
+
removePath(skillDir);
|
|
693
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
694
|
+
const content = commandSkill.convertCommandToSkill(
|
|
695
|
+
applyContentRewrites(adapter, fs.readFileSync(path.join(srcDir, f), 'utf-8')),
|
|
696
|
+
skillName);
|
|
697
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
698
|
+
names.push(skillName);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
return { count: commandFiles.length, names, destDir };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir;
|
|
706
|
+
* commandsAsSkills 适配器转 skill 形态 → adapter.skillsDir)
|
|
707
|
+
* @returns {{count:number, names:string[], destDir:string|null}}
|
|
708
|
+
*/
|
|
696
709
|
function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
|
|
697
|
-
if (
|
|
698
|
-
return
|
|
710
|
+
if (adapter.commandsAsSkills) return deployCommandSkills(adapter, srcDir, filter);
|
|
711
|
+
if (!adapter.commandsDir) return { count: 0, names: [], destDir: null };
|
|
712
|
+
const result = deployCommandFiles(
|
|
699
713
|
adapter, srcDir, path.join(TARGET_DIR, adapter.commandsDir), adapter.commandPrefix || '', filter);
|
|
714
|
+
return { ...result, destDir: adapter.commandsDir };
|
|
700
715
|
}
|
|
701
716
|
|
|
702
717
|
// ── 设置 OpenSpec 工作流 ──────────────────────────────────
|
|
@@ -1125,8 +1140,8 @@ async function init(targetDir, options = {}) {
|
|
|
1125
1140
|
await detectUserLevelSuperpowers(selectedAdapters, yes);
|
|
1126
1141
|
}
|
|
1127
1142
|
|
|
1128
|
-
// 安装 Commands(官方 11 个 → /opsx:*,onboard
|
|
1129
|
-
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
|
|
1143
|
+
// 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发;commandsAsSkills 适配器转 skill 形态)
|
|
1144
|
+
const commandsAdapters = selectedAdapters.filter(a => a.commandsDir || a.commandsAsSkills);
|
|
1130
1145
|
if (commandsAdapters.length > 0) {
|
|
1131
1146
|
console.log(`\n ⚡ ${action} Commands...`);
|
|
1132
1147
|
const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
|
|
@@ -1134,7 +1149,7 @@ async function init(targetDir, options = {}) {
|
|
|
1134
1149
|
.filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
|
|
1135
1150
|
|
|
1136
1151
|
const cmdDests = [...new Set(commandsAdapters
|
|
1137
|
-
.map(a => a.commandsDir)
|
|
1152
|
+
.map(a => a.commandsDir || (a.commandsAsSkills ? `${a.skillsDir}(转 skill)` : null))
|
|
1138
1153
|
.filter(Boolean))];
|
|
1139
1154
|
let cmdCount = 0;
|
|
1140
1155
|
commandsAdapters.forEach(adapter => {
|
|
@@ -1143,7 +1158,7 @@ async function init(targetDir, options = {}) {
|
|
|
1143
1158
|
const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc, openspecCmdInclude);
|
|
1144
1159
|
if (baseResult.count > 0) {
|
|
1145
1160
|
adapterCmdCount += baseResult.count;
|
|
1146
|
-
baseResult.names.forEach(name => newPaths.add(`${
|
|
1161
|
+
baseResult.names.forEach(name => newPaths.add(`${baseResult.destDir}/${name}`));
|
|
1147
1162
|
}
|
|
1148
1163
|
cmdCount = Math.max(cmdCount, adapterCmdCount);
|
|
1149
1164
|
});
|
|
@@ -1173,9 +1188,27 @@ async function init(targetDir, options = {}) {
|
|
|
1173
1188
|
}
|
|
1174
1189
|
}
|
|
1175
1190
|
|
|
1176
|
-
//
|
|
1191
|
+
// AGENTS.md 规则导航块(声明 agentsNav 的适配器;MiniMax 唯一项目级规则注入通道)。
|
|
1192
|
+
// 选中 → 注入/更新托管块(块外用户内容不动);未选中(本次 update 取消勾选)→ 精确摘除。
|
|
1193
|
+
let agentsNavState = null;
|
|
1194
|
+
let agentsNavFile = null;
|
|
1195
|
+
adapters.filter(a => a.agentsNav).forEach(adapter => {
|
|
1196
|
+
const target = path.join(TARGET_DIR, adapter.agentsNav.file);
|
|
1197
|
+
if (selectedAdapters.some(sa => sa.name === adapter.name)) {
|
|
1198
|
+
const state = agentsNav.inject(target, adapter.name, adapter.agentsNav.text);
|
|
1199
|
+
if (state !== 'unchanged') { agentsNavState = state; agentsNavFile = adapter.agentsNav.file; }
|
|
1200
|
+
} else if (agentsNav.remove(target, adapter.name)) {
|
|
1201
|
+
agentsNavState = 'removed';
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
if (agentsNavState === 'created' || agentsNavState === 'updated') {
|
|
1205
|
+
log(`AGENTS.md 规则导航 → ${agentsNavFile}`, 'success');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// 写入自动版本检查 hook(settings.json 方言 + 本地插件方言)
|
|
1177
1209
|
const hookAdapters = selectedAdapters.filter(a => a.settingsFile && a.hookEvents);
|
|
1178
|
-
|
|
1210
|
+
const pluginHookAdapters = selectedAdapters.filter(a => a.pluginHook);
|
|
1211
|
+
if (hookAdapters.length > 0 || pluginHookAdapters.length > 0) {
|
|
1179
1212
|
console.log(`\n 🔔 配置自动版本检查...`);
|
|
1180
1213
|
const command = settingsManager.getCheckUpdateCommand();
|
|
1181
1214
|
hookAdapters.forEach(adapter => {
|
|
@@ -1194,7 +1227,22 @@ async function init(targetDir, options = {}) {
|
|
|
1194
1227
|
}
|
|
1195
1228
|
if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
|
|
1196
1229
|
});
|
|
1197
|
-
|
|
1230
|
+
if (hookAdapters.length > 0) {
|
|
1231
|
+
log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')} (${hookAdapters[0].hookEvents.join(', ')})`, 'success');
|
|
1232
|
+
}
|
|
1233
|
+
// 本地插件方言(MiniMax:无 settings.json hooks)——用户级,不写入项目清单
|
|
1234
|
+
if (pluginHookAdapters.length > 0) {
|
|
1235
|
+
const pkgVersion = require(path.join(SHARED_STANDARDS_DIR, 'package.json')).version;
|
|
1236
|
+
pluginHookAdapters.forEach(adapter => {
|
|
1237
|
+
const info = pluginHookManager.install({
|
|
1238
|
+
dataDir: adapter.resolveDataDir(),
|
|
1239
|
+
...adapter.pluginHook,
|
|
1240
|
+
command,
|
|
1241
|
+
version: pkgVersion,
|
|
1242
|
+
});
|
|
1243
|
+
log(`${adapter.displayName} 版本检查插件 → ${info.dir}`, 'success');
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1198
1246
|
}
|
|
1199
1247
|
|
|
1200
1248
|
// 升级时清理旧路径(在旧清单中但不在新路径集中)。
|
|
@@ -1278,14 +1326,15 @@ async function init(targetDir, options = {}) {
|
|
|
1278
1326
|
|
|
1279
1327
|
// 附加组件(单行)
|
|
1280
1328
|
const hasSkills = selectedAdapters.some(a => a.skillsDir);
|
|
1281
|
-
const hasCommands = selectedAdapters.some(a => a.commandsDir);
|
|
1282
|
-
const hasHooks = selectedAdapters.some(a => a.settingsFile && a.hookEvents);
|
|
1329
|
+
const hasCommands = selectedAdapters.some(a => a.commandsDir || a.commandsAsSkills);
|
|
1330
|
+
const hasHooks = selectedAdapters.some(a => (a.settingsFile && a.hookEvents) || a.pluginHook);
|
|
1283
1331
|
const extras = [];
|
|
1284
1332
|
if (hasSkills) extras.push('Skills');
|
|
1285
1333
|
if (hasCommands) extras.push('Commands');
|
|
1286
1334
|
if (hasHooks) extras.push('版本检查 hook');
|
|
1287
1335
|
if (hookSummary) extras.push('提交防线 pre-commit hook');
|
|
1288
1336
|
if (agentsMdState === 'created') extras.push('AGENTS.md(空间导航)');
|
|
1337
|
+
if (agentsNavState === 'created' || agentsNavState === 'updated') extras.push('AGENTS.md(规则导航)');
|
|
1289
1338
|
if (extras.length > 0) {
|
|
1290
1339
|
console.log(` ${extras.join(' / ')} 已配置`);
|
|
1291
1340
|
}
|
|
@@ -1829,6 +1878,22 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
|
|
|
1829
1878
|
summaryParts.push(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join('、')}`);
|
|
1830
1879
|
}
|
|
1831
1880
|
|
|
1881
|
+
// 本地插件 hook(MiniMax:无 settings.json hooks,走 <dataDir>/plugins 本地插件,幂等覆盖)
|
|
1882
|
+
const pluginHookAdapters = targetAdapters.filter(a => a.pluginHook);
|
|
1883
|
+
if (pluginHookAdapters.length > 0) {
|
|
1884
|
+
const command = settingsManager.getCheckUpdateCommand();
|
|
1885
|
+
const pkgVersion = require(path.join(SHARED_STANDARDS_DIR, 'package.json')).version;
|
|
1886
|
+
pluginHookAdapters.forEach(adapter => {
|
|
1887
|
+
pluginHookManager.install({
|
|
1888
|
+
dataDir: adapter.resolveDataDir(),
|
|
1889
|
+
...adapter.pluginHook,
|
|
1890
|
+
command,
|
|
1891
|
+
version: pkgVersion,
|
|
1892
|
+
});
|
|
1893
|
+
});
|
|
1894
|
+
summaryParts.push(`版本检查插件 → ${pluginHookAdapters.map(a => a.displayName).join('、')}`);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1832
1897
|
if (summaryParts.length > 0) step('✓', '用户级组件', summaryParts.join(' · '));
|
|
1833
1898
|
|
|
1834
1899
|
const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
|
|
@@ -1974,7 +2039,7 @@ async function uninstall() {
|
|
|
1974
2039
|
// 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
|
|
1975
2040
|
let removeUserLevel = false;
|
|
1976
2041
|
if (fs.existsSync(USER_MANIFEST_FILE)) {
|
|
1977
|
-
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.workbuddy、~/.trae-cn 的版本检查 hook
|
|
2042
|
+
const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.workbuddy、~/.trae-cn 的版本检查 hook、MiniMax Code 本地插件)?(y/N): ');
|
|
1978
2043
|
removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
|
|
1979
2044
|
}
|
|
1980
2045
|
|
|
@@ -2003,6 +2068,25 @@ async function uninstall() {
|
|
|
2003
2068
|
skippedPaths.add(adapter.settingsFile);
|
|
2004
2069
|
});
|
|
2005
2070
|
|
|
2071
|
+
// 本地插件 hook(MiniMax,用户级组件):仅确认卸载用户级时删除插件目录
|
|
2072
|
+
if (removeUserLevel) {
|
|
2073
|
+
adapters.forEach(adapter => {
|
|
2074
|
+
if (!adapter.pluginHook) return;
|
|
2075
|
+
if (pluginHookManager.remove({ dataDir: adapter.resolveDataDir(), ...adapter.pluginHook })) {
|
|
2076
|
+
log(`移除 ${adapter.displayName} 版本检查插件(${adapter.resolveDataDir()})`, 'success');
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// AGENTS.md 规则导航块:本包托管块精确摘除(块外用户内容不动;文件归用户所有不进清单)
|
|
2082
|
+
adapters.forEach(adapter => {
|
|
2083
|
+
if (!adapter.agentsNav) return;
|
|
2084
|
+
const target = path.join(TARGET_DIR, adapter.agentsNav.file);
|
|
2085
|
+
if (agentsNav.remove(target, adapter.name)) {
|
|
2086
|
+
log(`移除 ${adapter.agentsNav.file} 规则导航块`, 'success');
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2006
2090
|
// opencode.json:移除本包登记的 instructions pattern(cbb 创建的整体文件由清单删除兜底)
|
|
2007
2091
|
removeInstructionsRegistrations();
|
|
2008
2092
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MiniMax Code(mcode CLI)适配器
|
|
3
|
+
*
|
|
4
|
+
* 官方源码考证结论(agent.minimaxi.com 文档站 + 源码浅克隆):
|
|
5
|
+
* - rules:MiniMax 无原生 rules 自动读取逻辑(源码无 .minimax/rules 读取),
|
|
6
|
+
* 规则照常分发到 .minimax/rules/,靠 AGENTS.md 托管导航块(agentsNav)引导
|
|
7
|
+
* AI 主动读取——AGENTS.md 是 MiniMax 唯一的项目级规则注入通道
|
|
8
|
+
* (local-runtime/src/project/instructions.ts,32KB 预算)。
|
|
9
|
+
* - skills:.minimax/skills 为原生 workspace skill 根(skills/roots.ts),
|
|
10
|
+
* 标准 Agent Skills 格式(目录名 = frontmatter name)零改写。
|
|
11
|
+
* - commands:MiniMax 无项目级 commands 目录,openspec 命令转 skill 形态分发
|
|
12
|
+
* (skill 即其命令形态,/opsx-propose 斜杠照常可用),commandsAsSkills: true。
|
|
13
|
+
* - hooks:无 Claude 式 settings.json hooks(hooks 仅插件作用域),版本检查改用
|
|
14
|
+
* 用户级本地插件(<data-dir>/plugins/cbb-check-update,声明 UserPromptSubmit)。
|
|
15
|
+
*
|
|
16
|
+
* 数据目录:MINIMAX_DATA_DIR(优先)> MAVIS_DATA_DIR > ~/.minimax,与 mcode
|
|
17
|
+
* 运行时一致;用户若改过数据目录,需重跑 cbb update 以重装插件到新位置。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
|
|
23
|
+
/** 解析 mcode 数据目录(调用时求值,便于环境变量覆盖与测试) */
|
|
24
|
+
function resolveDataDir() {
|
|
25
|
+
return process.env.MINIMAX_DATA_DIR
|
|
26
|
+
|| process.env.MAVIS_DATA_DIR
|
|
27
|
+
|| path.join(os.homedir(), '.minimax');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
name: 'minimax',
|
|
32
|
+
displayName: 'MiniMax Code',
|
|
33
|
+
rulesDir: '.minimax/rules',
|
|
34
|
+
skillsDir: '.minimax/skills',
|
|
35
|
+
// 无项目级 commands 目录:openspec 命令转 <skillsDir>/<prefix><name>/SKILL.md
|
|
36
|
+
commandsAsSkills: true,
|
|
37
|
+
commandPrefix: 'opsx-',
|
|
38
|
+
// 部署副本中把 /opsx: 与 /cbb: 引用改写为 skill 斜杠风格(/opsx-propose)
|
|
39
|
+
contentRewrites: [['/opsx:', '/opsx-'], ['/cbb:', '/cbb-']],
|
|
40
|
+
// AGENTS.md 托管导航块(幂等注入 / 卸载精确摘除,不触碰块外用户内容)
|
|
41
|
+
agentsNav: {
|
|
42
|
+
file: 'AGENTS.md',
|
|
43
|
+
text: '⚠️ 规则优先:在当前目录下执行的任何任务(不限于编码任务)开始前,必须先读取并遵循 `.minimax/rules/` 中的相关规则文件。',
|
|
44
|
+
},
|
|
45
|
+
// 版本检查 hook:用户级本地插件(<dataDir>/plugins/<dirName>,自动发现热加载、默认启用)
|
|
46
|
+
pluginHook: {
|
|
47
|
+
dirName: 'cbb-check-update',
|
|
48
|
+
events: ['UserPromptSubmit'],
|
|
49
|
+
// 解析上限:MiniMax timeout 单位为秒,1–10(超出视为非法回落 5s)
|
|
50
|
+
timeout: 10,
|
|
51
|
+
},
|
|
52
|
+
resolveDataDir,
|
|
53
|
+
};
|
|
@@ -28,18 +28,22 @@ function getProjectDir() {
|
|
|
28
28
|
|| process.env.TRAE_CN_PROJECT_DIR
|
|
29
29
|
|| process.env.TRAE_PROJECT_DIR
|
|
30
30
|
|| process.env.QODER_PROJECT_DIR
|
|
31
|
+
// MiniMax 同时注入 MINIMAX_PROJECT_DIR 与 CLAUDE_PROJECT_DIR,专属变量优先
|
|
32
|
+
|| process.env.MINIMAX_PROJECT_DIR
|
|
31
33
|
|| process.env.CLAUDE_PROJECT_DIR
|
|
32
34
|
|| process.cwd();
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** 通过环境变量检测当前调用方对应的适配器名称 */
|
|
36
38
|
function detectAdapterName() {
|
|
37
|
-
// 兼容变量(CLAUDE_PROJECT_DIR)由 Trae / WorkBuddy 一并注入,故优先判定各自专属变量;
|
|
39
|
+
// 兼容变量(CLAUDE_PROJECT_DIR)由 Trae / WorkBuddy / MiniMax 一并注入,故优先判定各自专属变量;
|
|
38
40
|
// CODEBUDDY_PROJECT_DIR 为 WorkBuddy 沿用的历史变量名,一并识别为 workbuddy;
|
|
39
|
-
// TRAE_CN_PROJECT_DIR / TRAE_PROJECT_DIR 为中国版 Trae 的变量名(不同版本注入不一),均识别为 trae
|
|
41
|
+
// TRAE_CN_PROJECT_DIR / TRAE_PROJECT_DIR 为中国版 Trae 的变量名(不同版本注入不一),均识别为 trae;
|
|
42
|
+
// MINIMAX_PROJECT_DIR 为 MiniMax Code 专属变量(插件 hook 注入,与 CLAUDE_PROJECT_DIR 同时存在)
|
|
40
43
|
if (process.env.WORKBUDDY_PROJECT_DIR || process.env.CODEBUDDY_PROJECT_DIR) return 'workbuddy';
|
|
41
44
|
if (process.env.TRAE_CN_PROJECT_DIR || process.env.TRAE_PROJECT_DIR) return 'trae';
|
|
42
45
|
if (process.env.QODER_PROJECT_DIR) return 'qoder';
|
|
46
|
+
if (process.env.MINIMAX_PROJECT_DIR) return 'minimax';
|
|
43
47
|
if (process.env.CLAUDE_PROJECT_DIR) return 'claude-code';
|
|
44
48
|
return null;
|
|
45
49
|
}
|
|
@@ -30,6 +30,8 @@ const GITIGNORE_RULES = [
|
|
|
30
30
|
// 但卸载/清理时仍移除其 gitignore 行
|
|
31
31
|
{ add: null, accept: ['.trae-cn', '.trae-cn/'] },
|
|
32
32
|
{ add: '.trae', accept: ['.trae', '.trae/'] },
|
|
33
|
+
// .minimax:MiniMax Code 项目级配置根(rules / skills)
|
|
34
|
+
{ add: '.minimax', accept: ['.minimax', '.minimax/'] },
|
|
33
35
|
];
|
|
34
36
|
|
|
35
37
|
/** 将 .gitignore 文本拆为行数组。 */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MiniMax Code 本地插件 hook 的安装 / 卸载工具。
|
|
5
|
+
*
|
|
6
|
+
* MiniMax 无 Claude 式 settings.json hooks(hooks 仅插件作用域),版本检查改由
|
|
7
|
+
* 用户级本地插件承担:
|
|
8
|
+
* <dataDir>/plugins/cbb-check-update/
|
|
9
|
+
* .claude-plugin/plugin.json 兼容 manifest(仅 name 必填,走 CLAUDE 兼容读取器)
|
|
10
|
+
* hooks/hooks.json manifest 未声明 hooks 时的默认路径
|
|
11
|
+
*
|
|
12
|
+
* 机制(源码考证):
|
|
13
|
+
* - 本地插件根为 path.join(dataDir, 'plugins'),由扫描自动发现并热加载
|
|
14
|
+
* (package-storage.ts + watcher),无需重启 mcode;
|
|
15
|
+
* - 本地插件默认启用(不在 pluginDisabledLocalRoots 表即启用);
|
|
16
|
+
* - hook 事件 UserPromptSubmit,stdin 收 session_id JSON(与 Claude 同构),
|
|
17
|
+
* 输出 hookSpecificOutput.additionalContext 注入对话;
|
|
18
|
+
* - timeout 单位为秒(1–10,超限视为非法回落 5s);非 0 退出码(exit 2)会拒绝
|
|
19
|
+
* prompt —— check-update.js 恒 exit 0,绝不阻塞;
|
|
20
|
+
* - Windows 下 CLAUDE 格式命令经 Git Bash / PowerShell 执行(不走 cmd.exe)。
|
|
21
|
+
*
|
|
22
|
+
* 本插件目录整目录归本包管理:安装幂等覆盖本包两个文件,卸载直接删除目录。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
/** 插件目录:dataDir + plugins + dirName */
|
|
29
|
+
function pluginDir({ dataDir, dirName }) {
|
|
30
|
+
return path.join(dataDir, 'plugins', dirName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 幂等写入(内容一致不重写,避免触发插件 digest 重算) */
|
|
34
|
+
function writeIfChanged(file, content) {
|
|
35
|
+
if (fs.existsSync(file) && fs.readFileSync(file, 'utf-8') === content) return false;
|
|
36
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
37
|
+
fs.writeFileSync(file, content);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 安装(幂等)版本检查插件。
|
|
43
|
+
*
|
|
44
|
+
* @param {object} opts
|
|
45
|
+
* @param {string} opts.dataDir mcode 数据目录(由适配器 resolveDataDir 解析)
|
|
46
|
+
* @param {string} opts.dirName 插件目录名(cbb-check-update)
|
|
47
|
+
* @param {string[]} opts.events hook 事件(['UserPromptSubmit'])
|
|
48
|
+
* @param {number} opts.timeout 秒(1–10)
|
|
49
|
+
* @param {string} opts.command hook 命令(node "<check-update.js>")
|
|
50
|
+
* @param {string} opts.version 插件 manifest 版本(= 本包版本)
|
|
51
|
+
* @returns {{dir: string, changed: boolean}}
|
|
52
|
+
*/
|
|
53
|
+
function install({ dataDir, dirName, events, timeout, command, version }) {
|
|
54
|
+
const root = pluginDir({ dataDir, dirName });
|
|
55
|
+
|
|
56
|
+
const manifest = {
|
|
57
|
+
name: dirName,
|
|
58
|
+
description: '@jspg-ai/coding-bb 版本检查 hook(UserPromptSubmit 触发,每日最多请求一次 registry)',
|
|
59
|
+
version: version || '0.0.0',
|
|
60
|
+
author: 'jspg-ai',
|
|
61
|
+
// 刻意不声明 hooks 字段:CLAUDE 兼容读取器对未声明的 hooks 走默认路径
|
|
62
|
+
// hooks/hooks.json(readPluginCapabilities 的 defaultPath),结构最稳。
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const hooksDoc = {
|
|
66
|
+
hooks: Object.fromEntries(events.map(event => [event, [{
|
|
67
|
+
hooks: [{ type: 'command', command, timeout }],
|
|
68
|
+
}]])),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const changedManifest = writeIfChanged(
|
|
72
|
+
path.join(root, '.claude-plugin', 'plugin.json'),
|
|
73
|
+
JSON.stringify(manifest, null, 2) + '\n');
|
|
74
|
+
const changedHooks = writeIfChanged(
|
|
75
|
+
path.join(root, 'hooks', 'hooks.json'),
|
|
76
|
+
JSON.stringify(hooksDoc, null, 2) + '\n');
|
|
77
|
+
|
|
78
|
+
return { dir: root, changed: changedManifest || changedHooks };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 卸载版本检查插件(整目录删除)。
|
|
83
|
+
* @returns {boolean} 是否有删除动作
|
|
84
|
+
*/
|
|
85
|
+
function remove({ dataDir, dirName }) {
|
|
86
|
+
const root = pluginDir({ dataDir, dirName });
|
|
87
|
+
if (!fs.existsSync(root)) return false;
|
|
88
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { pluginDir, install, remove };
|
|
@@ -33,7 +33,7 @@ if (!wsRoot || !wtPath) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// 各工具的项目级配置根(存在才处理)与本包部署的内容子目录
|
|
36
|
-
const CONFIG_ROOTS = ['.claude', '.qoder', '.opencode', '.workbuddy', '.trae'];
|
|
36
|
+
const CONFIG_ROOTS = ['.claude', '.qoder', '.opencode', '.workbuddy', '.trae', '.minimax'];
|
|
37
37
|
const CONTENT_SUBDIRS = ['rules', 'skills', 'commands'];
|
|
38
38
|
// 文件级内容(复制而非 junction):opencode 的 instructions 登记
|
|
39
39
|
const CONTENT_FILES = { '.opencode': ['opencode.json'] };
|
|
@@ -323,21 +323,23 @@ node "<skill_dir>/scripts/remove-worktrees.js "<ws_root>" "<branch>" "<force>"
|
|
|
323
323
|
|
|
324
324
|
所有 worktree 已成功移除后,如果当前 shell 的工作目录位于**已被关闭的 worktree 内**,需要主动让 IDE 窗口切回 `WS_ROOT`,避免 IDE 窗口指向已删除的目录。
|
|
325
325
|
|
|
326
|
-
> ⓘ **本节是 close-worktree 专属逻辑**:与 init-worktree 的 Step 5 不共享、不交叉同步。init-worktree 负责"
|
|
326
|
+
> ⓘ **本节是 close-worktree 专属逻辑**:与 init-worktree 的 Step 5 不共享、不交叉同步。init-worktree 负责"创建后把开发会话开进 worktree",本节负责"关闭后切回根目录",两者职责独立。
|
|
327
327
|
|
|
328
328
|
**AI agent 行为**:
|
|
329
|
-
1.
|
|
330
|
-
2.
|
|
329
|
+
1. 检查当前会话 / IDE 窗口是否指向被关闭的 worktree
|
|
330
|
+
2. **OpenCode 等支持会话搬移的工具**:agent 亲自经 code-mode 调用 `opencode.session_move` 把会话切回 `<ws_root>`——自动完成、上下文保留、**不列入用户待办**,切完核对工作目录
|
|
331
|
+
3. **Qoder / Cursor / VS Code**:调用对应 IDE 的 `--reuse-window` 命令切回 WS_ROOT:
|
|
331
332
|
- Qoder → `qoder --reuse-window "<ws_root>"`
|
|
332
333
|
- Cursor → `cursor --reuse-window "<ws_root>"`
|
|
333
334
|
- VS Code → `code --reuse-window "<ws_root>"`
|
|
334
|
-
|
|
335
|
+
4. **JetBrains 跳过主动切换**——`idea64 <path>` 会启动新进程或提示"打开/在新窗口打开",让用户手动操作
|
|
335
336
|
|
|
336
337
|
**关键点**:
|
|
337
|
-
-
|
|
338
|
+
- **只在会话 / 窗口匹配被关闭的 worktree 时触发**——如果已在 WS_ROOT 内,再切一次是多余的
|
|
339
|
+
- **OpenCode 会话切回是零成本自动动作**——`session_move` 复用当前会话(上下文保留、无新窗口),agent 亲自做,严禁推给用户
|
|
338
340
|
- **仅切 Qoder / Cursor / VS Code**——这三者的 `--reuse-window` 是轻量级"切目录",不重启 IDE
|
|
339
341
|
- **不会产生孤立窗口**——`--reuse-window` 是复用当前窗口切换目录,**不会打开新窗口**。即使 IDE 已开着指向 WS_ROOT 的其他窗口,本步骤也不会重复创建,仅是把当前指向已删除目录的窗口切走
|
|
340
|
-
-
|
|
342
|
+
- **失败不致命**——`session_move` / `--reuse-window` 失败时仅警告,后续分支删除不受影响
|
|
341
343
|
|
|
342
344
|
### 4.4 分支处理(默认跳过,按需手动调用)
|
|
343
345
|
|
|
@@ -384,7 +386,7 @@ git -C "<ws_root>" worktree list
|
|
|
384
386
|
|
|
385
387
|
目标 worktree 应已不在列表中。
|
|
386
388
|
|
|
387
|
-
**工作区回根交接(必须执行)**:若用户的开发会话工作在 worktree
|
|
389
|
+
**工作区回根交接(必须执行)**:若用户的开发会话工作在 worktree 内(当前会话工作目录 = 已删除的 `<wt_path>`),此刻目录已悬空——**OpenCode 等支持会话搬移的工具由 agent 自动 `session_move` 切回 `<ws_root>`,不列入用户待办**;无该能力的工具才提示用户把工作区切回 `<ws_root>`(打开文件夹 / Open Folder / 切换工作目录),并在最终报告中给出该绝对路径。
|
|
388
390
|
|
|
389
391
|
---
|
|
390
392
|
|
|
@@ -404,7 +406,7 @@ git -C "<ws_root>" worktree list
|
|
|
404
406
|
(无未归档 change 时不显示本节)
|
|
405
407
|
分支处理:feature-xxx 已保留(默认行为;如需删除分支,手动调用 scripts/delete-branches.js)
|
|
406
408
|
剩余工作空间 worktree:<列出仍存在的,或 "无" >
|
|
407
|
-
👉
|
|
409
|
+
👉 仅当所用工具不支持自动切根、且你的窗口仍停在已删除的 worktree 时,请手动切回工作空间根:<ws_root 绝对路径>
|
|
408
410
|
Workspace cleaned up.
|
|
409
411
|
```
|
|
410
412
|
|
|
@@ -304,7 +304,7 @@ WT_PATH d:\workspace\xxx\.worktrees\worktree-feature-login
|
|
|
304
304
|
node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
脚本把根的 AI 配置**内容子目录**(`.claude/`、`.qoder/`、`.opencode/`、`.workbuddy/`、`.trae/` 下的 rules / skills / commands)junction 进工作树;`.opencode/opencode.json` 为文件则复制。
|
|
307
|
+
脚本把根的 AI 配置**内容子目录**(`.claude/`、`.qoder/`、`.opencode/`、`.workbuddy/`、`.trae/`、`.minimax/` 下的 rules / skills / commands)junction 进工作树;`.opencode/opencode.json` 为文件则复制。
|
|
308
308
|
|
|
309
309
|
**解读末行 `JUNCTION_RESULT`**:
|
|
310
310
|
- 退出码 0 → linked / copied / skipped(幂等)任一组合均成功
|
|
@@ -320,8 +320,9 @@ node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
|
320
320
|
|
|
321
321
|
不执行脚本。AI agent 向用户输出最终报告(格式见「最终报告」),并完成以下交接声明:
|
|
322
322
|
|
|
323
|
-
1. **开发会话开进 worktree
|
|
324
|
-
-
|
|
323
|
+
1. **开发会话开进 worktree**(按工具形态,**能自动则 agent 亲自做,不推给用户**):
|
|
324
|
+
- **OpenCode 等支持会话搬移的工具(桌面端单窗口工作流)**:由 **agent 自己**把当前会话的工作目录切到 `<wt_path>`(经 code-mode 调用 `opencode.session_move`,`directory = <wt_path>`)——聊天上下文保留、无需新窗口、**不列入用户待办**;AI 配置已 junction 就位(`/opsx:*` 等命令照常可用),`.codespace/<app>/...` 相对路径原样生效
|
|
325
|
+
- **无会话搬移能力的桌面端工具(Claude Code / Qoder 等)**:提示用户把当前窗口的工作区根切到 `<wt_path>`(打开文件夹 / Open Folder / 切换工作目录)
|
|
325
326
|
- **CLI / TUI agent**:新开会话于 `<wt_path>`;根会话保留作控制面——后续创建/关闭 worktree、`cbb update` 等空间级操作在此执行
|
|
326
327
|
- 无论哪种形态,**必须向用户报出空间根绝对路径 `<ws_root>`**(控制面地址:close 关闭 worktree 后回切用)
|
|
327
328
|
2. **目录纪律(worktree 会话内自然成立)**:会话 cwd == 开发目录,上游相对路径原样生效;**分支感知 hook 已覆盖全部仓库**——任何落 main 的载荷提交会被拦截(大声失败是防线,不是故障)
|
|
@@ -332,13 +333,15 @@ node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
|
332
333
|
|
|
333
334
|
## 最终报告
|
|
334
335
|
|
|
336
|
+
> **输出纪律**:严格按下方格式输出,禁止追加规则提醒等额外段落;规则由 rules 系统在触发点呈现,不复述。
|
|
337
|
+
|
|
335
338
|
最终报告格式:
|
|
336
339
|
|
|
337
340
|
```
|
|
338
341
|
✅ worktree 已创建:<wt_path>
|
|
339
342
|
关联应用:<应用名 → .codespace/<应用名> 路径> 列表(全量,或用户显式指定的子集)
|
|
340
343
|
🔗 AI 配置已 junction 就位:rules / skills / commands(cbb update 一处生效)
|
|
341
|
-
📌
|
|
344
|
+
📌 开发会话已开进 <wt_path>(OpenCode:agent 已自动切换会话工作目录;CLI 端:新开会话;无搬移能力的桌面端:已提示用户切根)
|
|
342
345
|
空间根路径(控制面,close 后回切):<ws_root>
|
|
343
346
|
👉 下一步建议:
|
|
344
347
|
1. 开始需求开发(开发会话内,二选一):
|
|
@@ -432,7 +435,7 @@ node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
|
432
435
|
### 完成后开发会话未开进 worktree
|
|
433
436
|
|
|
434
437
|
- **问题:** 流程结束后开发会话仍停留在工作空间根目录(junction 好的 AI 配置没人加载),agent 在根会话里靠路径翻译干活,回到第一代失败模式
|
|
435
|
-
- **修复:** Step 4
|
|
438
|
+
- **修复:** Step 4 交接必须确保"开发会话开在 `<wt_path>`"(OpenCode 等支持会话搬移的工具 = agent 亲自经 `session_move` 切换、不列入用户待办;无该能力的桌面端 = 提示用户切根;CLI = 新开会话);根会话保留作控制面(worktree 生灭、`cbb update`)
|
|
436
439
|
|
|
437
440
|
### 用 `git checkout -b` 在创建 worktree 前切分支
|
|
438
441
|
|
|
@@ -473,7 +476,7 @@ node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
|
|
|
473
476
|
- 严格 Step 3.3 → Step 3.5 顺序(先统一创建分支,再统一创建 worktree;workspace 工作树先建,各应用按序补建)
|
|
474
477
|
- Step 3.6 junction AI 配置(失败不阻塞,但要报告)
|
|
475
478
|
- 所有脚本调用以 `node "<skill_dir>/scripts/<name>.js" "<绝对路径>" "<branch>"` 形式发起(apps 缺省由脚本内部从 CBB-WORKSPACE.json 读取,子集用 `--apps` 传入);**执行时 cwd 保持在工作空间根**(`check-env.js` / `parse-config.js` 按 cwd 定位)
|
|
476
|
-
- Step 4
|
|
479
|
+
- Step 4 交接必须确保"开发会话开在 `<wt_path>`"(支持会话搬移的工具 = agent 自动 `session_move` 切换,**禁止把切根列入用户待办**;其余 = 用户切根 / CLI 新开会话)、空间根路径在案作控制面
|
|
477
480
|
|
|
478
481
|
---
|
|
479
482
|
|
|
@@ -33,7 +33,7 @@ if (!wsRoot || !wtPath) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// 各工具的项目级配置根(存在才处理)与本包部署的内容子目录
|
|
36
|
-
const CONFIG_ROOTS = ['.claude', '.qoder', '.opencode', '.workbuddy', '.trae'];
|
|
36
|
+
const CONFIG_ROOTS = ['.claude', '.qoder', '.opencode', '.workbuddy', '.trae', '.minimax'];
|
|
37
37
|
const CONTENT_SUBDIRS = ['rules', 'skills', 'commands'];
|
|
38
38
|
// 文件级内容(复制而非 junction):opencode 的 instructions 登记
|
|
39
39
|
const CONTENT_FILES = { '.opencode': ['opencode.json'] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jspg-ai/coding-bb",
|
|
3
|
-
"version": "0.0.3-beta.
|
|
3
|
+
"version": "0.0.3-beta.29",
|
|
4
4
|
"description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
|
|
5
5
|
"main": "cbb/lib/install/init.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"sync:shared": "node scripts/sync-shared.js",
|
|
21
21
|
"pretest": "node scripts/sync-shared.js --check",
|
|
22
|
-
"test": "node test/lib/install/cleanup.test.js && node test/lib/utils/settings.test.js && node test/lib/utils/gitignore.test.js && node test/lib/utils/githook.test.js && node test/lib/utils/version.test.js && node test/lib/install/workspaces.test.js && node test/lib/utils/checkbox.test.js && node test/lib/utils/output.test.js && node test/lib/install/cli-help.test.js && node test/lib/install/adapters.test.js && node test/lib/wiki/wiki-publish.test.js && node test/lib/wiki/wiki-parse-args.test.js && node test/lib/wiki/wiki-split.test.js && node test/lib/superpowers/superpowers.test.js && node test/worktrees/push-branches.test.js && node test/worktrees/commit-worktrees.test.js && node test/worktrees/push-worktrees.test.js && node test/worktrees/check-env-deep.test.js && node test/worktrees/create-worktrees.test.js && node test/worktrees/make-mr-urls.test.js && node test/lib/install/setup-decision.test.js && node test/lib/openspec/protected-paths.test.js"
|
|
22
|
+
"test": "node test/lib/install/cleanup.test.js && node test/lib/utils/settings.test.js && node test/lib/utils/gitignore.test.js && node test/lib/utils/githook.test.js && node test/lib/utils/version.test.js && node test/lib/install/workspaces.test.js && node test/lib/utils/checkbox.test.js && node test/lib/utils/output.test.js && node test/lib/install/cli-help.test.js && node test/lib/install/adapters.test.js && node test/lib/install/agents-nav.test.js && node test/lib/install/command-skill.test.js && node test/lib/utils/plugin-hook.test.js && node test/lib/wiki/wiki-publish.test.js && node test/lib/wiki/wiki-parse-args.test.js && node test/lib/wiki/wiki-split.test.js && node test/lib/superpowers/superpowers.test.js && node test/worktrees/push-branches.test.js && node test/worktrees/commit-worktrees.test.js && node test/worktrees/push-worktrees.test.js && node test/worktrees/check-env-deep.test.js && node test/worktrees/create-worktrees.test.js && node test/worktrees/make-mr-urls.test.js && node test/lib/install/setup-decision.test.js && node test/lib/openspec/protected-paths.test.js"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"cbb",
|