@colin4k1024/tsp 2.5.0 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/commands/dashboard.md +105 -0
- package/commands/goal.md +142 -0
- package/commands/heartbeat.md +129 -0
- package/commands/triage.md +108 -0
- package/hooks/README.md +1 -1
- package/hooks/harness-context-monitor.js +4 -0
- package/hooks/hooks.json +27 -26
- package/hooks/strategic-compact/README.md +7 -5
- package/hooks/strategic-compact/suggest-compact.js +9 -174
- package/marketplace.json +7 -8
- package/package.json +1 -1
- package/schemas/goal.schema.json +172 -0
- package/scripts/harness-audit.js +7 -4
- package/scripts/hooks/session-start-goal-resume.js +95 -0
- package/scripts/hooks/suggest-compact.js +392 -62
- package/scripts/install-platform.js +68 -85
- package/scripts/lib/blame-attribution.js +210 -0
- package/scripts/lib/completion-oracle.js +351 -0
- package/scripts/lib/heartbeat-scheduler.js +265 -0
- package/scripts/lib/opencode/convert-agents.js +273 -0
- package/scripts/lib/opencode/convert-hooks.js +286 -0
- package/scripts/lib/opencode/generate-agents-md.js +361 -0
- package/scripts/lib/wave-cost-advisor.js +155 -0
- package/scripts/test-opencode-install.js +151 -0
- package/skills/goal-convergence/SKILL.md +150 -0
- package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
- package/skills/loop-heartbeat/SKILL.md +120 -0
- package/skills/mcp-connector-bridge/SKILL.md +132 -0
- package/skills/repo-scan/SKILL.md +63 -63
- package/skills/rework-loop/SKILL.md +131 -0
- package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
- package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
- package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
- package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
- package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TSP hook 事件到 OpenCode 事件的映射
|
|
7
|
+
*/
|
|
8
|
+
const EVENT_MAPPING = {
|
|
9
|
+
'PreToolUse': 'tool.execute.before',
|
|
10
|
+
'PostToolUse': 'tool.execute.after',
|
|
11
|
+
'SessionStart': 'session.created',
|
|
12
|
+
'Stop': 'session.idle',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 工具名称映射
|
|
17
|
+
*/
|
|
18
|
+
const TOOL_MAPPING = {
|
|
19
|
+
'Bash': 'bash',
|
|
20
|
+
'Write': 'write',
|
|
21
|
+
'Edit': 'edit',
|
|
22
|
+
'MultiEdit': 'edit',
|
|
23
|
+
'Read': 'read',
|
|
24
|
+
'Grep': 'grep',
|
|
25
|
+
'Glob': 'glob',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 读取 hooks.json 配置
|
|
30
|
+
*/
|
|
31
|
+
function readHooksConfig(hooksDir) {
|
|
32
|
+
const hooksConfigPath = path.join(hooksDir, 'hooks.json');
|
|
33
|
+
if (!fs.existsSync(hooksConfigPath)) {
|
|
34
|
+
return { hooks: {} };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(hooksConfigPath, 'utf8'));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(`Warning: Could not parse hooks.json: ${error.message}`);
|
|
41
|
+
return { hooks: {} };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 转换单个 hook 配置
|
|
47
|
+
*/
|
|
48
|
+
function convertHookEntry(hookEntry, pluginRoot) {
|
|
49
|
+
if (!hookEntry || typeof hookEntry !== 'object') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { matcher, hooks, description, id } = hookEntry;
|
|
54
|
+
|
|
55
|
+
if (!Array.isArray(hooks) || hooks.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 转换 matcher 为 OpenCode 工具过滤器
|
|
60
|
+
const toolFilter = matcher === '*' ? null : matcher.split('|').map(tool => TOOL_MAPPING[tool] || tool.toLowerCase());
|
|
61
|
+
|
|
62
|
+
// 转换 hooks 为 OpenCode 格式
|
|
63
|
+
const convertedHooks = hooks.map(hook => {
|
|
64
|
+
if (!hook || typeof hook !== 'object') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { type, command, timeout, async: isAsync } = hook;
|
|
69
|
+
|
|
70
|
+
if (type !== 'command' || !command) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 替换变量
|
|
75
|
+
const convertedCommand = command
|
|
76
|
+
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginRoot)
|
|
77
|
+
.replace(/\$\{OPENCODE_PLUGIN_ROOT\}/g, pluginRoot);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
command: convertedCommand,
|
|
81
|
+
timeout: timeout || 30,
|
|
82
|
+
async: isAsync || false,
|
|
83
|
+
};
|
|
84
|
+
}).filter(Boolean);
|
|
85
|
+
|
|
86
|
+
if (convertedHooks.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: id || `hook-${Date.now()}`,
|
|
92
|
+
description: description || '',
|
|
93
|
+
tools: toolFilter,
|
|
94
|
+
hooks: convertedHooks,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 生成 OpenCode 插件代码
|
|
100
|
+
*/
|
|
101
|
+
function generatePluginCode(hooksConfig, pluginRoot) {
|
|
102
|
+
const lines = [];
|
|
103
|
+
|
|
104
|
+
lines.push('// TSP Hooks Plugin for OpenCode');
|
|
105
|
+
lines.push('// Auto-generated by convert-hooks.js');
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('module.exports = function tspHooks({ project, directory, worktree, client, $ }) {');
|
|
108
|
+
lines.push(' return {');
|
|
109
|
+
|
|
110
|
+
// 转换 PreToolUse hooks
|
|
111
|
+
const preToolUseHooks = (hooksConfig.hooks.PreToolUse || [])
|
|
112
|
+
.map(entry => convertHookEntry(entry, pluginRoot))
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
|
|
115
|
+
if (preToolUseHooks.length > 0) {
|
|
116
|
+
lines.push(" 'tool.execute.before': async (input, output) => {");
|
|
117
|
+
lines.push(' const toolName = input.tool;');
|
|
118
|
+
lines.push(' const args = input.args;');
|
|
119
|
+
lines.push('');
|
|
120
|
+
|
|
121
|
+
for (const hook of preToolUseHooks) {
|
|
122
|
+
const toolCondition = hook.tools
|
|
123
|
+
? ` && [${hook.tools.map(t => `'${t}'`).join(', ')}].includes(toolName)`
|
|
124
|
+
: '';
|
|
125
|
+
|
|
126
|
+
lines.push(` // ${hook.description || hook.id}`);
|
|
127
|
+
lines.push(` if (true${toolCondition}) {`);
|
|
128
|
+
lines.push(` try {`);
|
|
129
|
+
lines.push(` const { execSync } = require('child_process');`);
|
|
130
|
+
lines.push(` const command = \`${hook.hooks[0].command.replace(/`/g, '\\`')}\`;`);
|
|
131
|
+
lines.push(` execSync(command, { timeout: ${hook.hooks[0].timeout * 1000}, cwd: directory });`);
|
|
132
|
+
lines.push(` } catch (error) {`);
|
|
133
|
+
lines.push(` // Hook failed, continue execution`);
|
|
134
|
+
lines.push(` }`);
|
|
135
|
+
lines.push(` }`);
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push(' },');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 转换 PostToolUse hooks
|
|
143
|
+
const postToolUseHooks = (hooksConfig.hooks.PostToolUse || [])
|
|
144
|
+
.map(entry => convertHookEntry(entry, pluginRoot))
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
|
|
147
|
+
if (postToolUseHooks.length > 0) {
|
|
148
|
+
lines.push(" 'tool.execute.after': async (input, output) => {");
|
|
149
|
+
lines.push(' const toolName = input.tool;');
|
|
150
|
+
lines.push(' const args = input.args;');
|
|
151
|
+
lines.push(' const result = output;');
|
|
152
|
+
lines.push('');
|
|
153
|
+
|
|
154
|
+
for (const hook of postToolUseHooks) {
|
|
155
|
+
const toolCondition = hook.tools
|
|
156
|
+
? ` && [${hook.tools.map(t => `'${t}'`).join(', ')}].includes(toolName)`
|
|
157
|
+
: '';
|
|
158
|
+
|
|
159
|
+
lines.push(` // ${hook.description || hook.id}`);
|
|
160
|
+
lines.push(` if (true${toolCondition}) {`);
|
|
161
|
+
lines.push(` try {`);
|
|
162
|
+
lines.push(` const { execSync } = require('child_process');`);
|
|
163
|
+
lines.push(` const command = \`${hook.hooks[0].command.replace(/`/g, '\\`')}\`;`);
|
|
164
|
+
lines.push(` execSync(command, { timeout: ${hook.hooks[0].timeout * 1000}, cwd: directory });`);
|
|
165
|
+
lines.push(` } catch (error) {`);
|
|
166
|
+
lines.push(` // Hook failed, continue execution`);
|
|
167
|
+
lines.push(` }`);
|
|
168
|
+
lines.push(` }`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.push(' },');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 转换 SessionStart hooks
|
|
176
|
+
const sessionStartHooks = (hooksConfig.hooks.SessionStart || [])
|
|
177
|
+
.map(entry => convertHookEntry(entry, pluginRoot))
|
|
178
|
+
.filter(Boolean);
|
|
179
|
+
|
|
180
|
+
if (sessionStartHooks.length > 0) {
|
|
181
|
+
lines.push(" 'session.created': async (input, output) => {");
|
|
182
|
+
lines.push('');
|
|
183
|
+
|
|
184
|
+
for (const hook of sessionStartHooks) {
|
|
185
|
+
lines.push(` // ${hook.description || hook.id}`);
|
|
186
|
+
lines.push(` try {`);
|
|
187
|
+
lines.push(` const { execSync } = require('child_process');`);
|
|
188
|
+
lines.push(` const command = \`${hook.hooks[0].command.replace(/`/g, '\\`')}\`;`);
|
|
189
|
+
lines.push(` execSync(command, { timeout: ${hook.hooks[0].timeout * 1000}, cwd: directory });`);
|
|
190
|
+
lines.push(` } catch (error) {`);
|
|
191
|
+
lines.push(` // Hook failed, continue execution`);
|
|
192
|
+
lines.push(` }`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
lines.push(' },');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 转换 Stop hooks
|
|
200
|
+
const stopHooks = (hooksConfig.hooks.Stop || [])
|
|
201
|
+
.map(entry => convertHookEntry(entry, pluginRoot))
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
|
|
204
|
+
if (stopHooks.length > 0) {
|
|
205
|
+
lines.push(" 'session.idle': async (input, output) => {");
|
|
206
|
+
lines.push('');
|
|
207
|
+
|
|
208
|
+
for (const hook of stopHooks) {
|
|
209
|
+
lines.push(` // ${hook.description || hook.id}`);
|
|
210
|
+
lines.push(` try {`);
|
|
211
|
+
lines.push(` const { execSync } = require('child_process');`);
|
|
212
|
+
lines.push(` const command = \`${hook.hooks[0].command.replace(/`/g, '\\`')}\`;`);
|
|
213
|
+
lines.push(` execSync(command, { timeout: ${hook.hooks[0].timeout * 1000}, cwd: directory });`);
|
|
214
|
+
lines.push(` } catch (error) {`);
|
|
215
|
+
lines.push(` // Hook failed, continue execution`);
|
|
216
|
+
lines.push(` }`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
lines.push(' },');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
lines.push(' };');
|
|
224
|
+
lines.push('};');
|
|
225
|
+
|
|
226
|
+
return lines.join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 转换 hooks 插件(供 install-platform.js 调用)
|
|
231
|
+
*/
|
|
232
|
+
function convertHooksPlugin(root, opencodeHome) {
|
|
233
|
+
const pluginDir = path.join(opencodeHome, 'plugins', 'team-skills-platform');
|
|
234
|
+
|
|
235
|
+
// 读取 hooks 配置
|
|
236
|
+
const hooksDir = path.join(root, 'hooks');
|
|
237
|
+
const hooksConfig = readHooksConfig(hooksDir);
|
|
238
|
+
|
|
239
|
+
// 生成插件代码
|
|
240
|
+
const pluginCode = generatePluginCode(hooksConfig, pluginDir);
|
|
241
|
+
|
|
242
|
+
// 写入插件文件
|
|
243
|
+
const pluginsDir = path.join(opencodeHome, 'plugins');
|
|
244
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
245
|
+
const pluginPath = path.join(pluginsDir, 'tsp-hooks.js');
|
|
246
|
+
fs.writeFileSync(pluginPath, pluginCode, 'utf8');
|
|
247
|
+
|
|
248
|
+
console.log(`Generated OpenCode hooks plugin at ${pluginPath}`);
|
|
249
|
+
console.log(` Hooks converted: ${Object.values(hooksConfig.hooks).flat().length}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 主函数
|
|
254
|
+
*/
|
|
255
|
+
function main() {
|
|
256
|
+
const root = path.join(__dirname, '../../..');
|
|
257
|
+
const opencodeHome = process.argv[2] || path.join(require('os').homedir(), '.config', 'opencode');
|
|
258
|
+
|
|
259
|
+
console.log('Converting hooks for OpenCode...');
|
|
260
|
+
console.log(`Root: ${root}`);
|
|
261
|
+
console.log(`Target: ${opencodeHome}`);
|
|
262
|
+
|
|
263
|
+
convertHooksPlugin(root, opencodeHome);
|
|
264
|
+
|
|
265
|
+
console.log('✅ Hooks conversion completed');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 导出函数供其他脚本使用
|
|
269
|
+
module.exports = {
|
|
270
|
+
convertHookEntry,
|
|
271
|
+
generatePluginCode,
|
|
272
|
+
readHooksConfig,
|
|
273
|
+
convertHooksPlugin,
|
|
274
|
+
EVENT_MAPPING,
|
|
275
|
+
TOOL_MAPPING,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// 如果直接运行此脚本
|
|
279
|
+
if (require.main === module) {
|
|
280
|
+
try {
|
|
281
|
+
main();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('Error converting hooks:', error.message);
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const PLUGIN_NAME = require('../team-skills-data.json').plugin.name;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 读取 Markdown 文件内容
|
|
9
|
+
*/
|
|
10
|
+
function readMarkdownFile(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.warn(`Warning: Could not read ${filePath}: ${error.message}`);
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 扫描目录中的所有 .md 文件
|
|
21
|
+
*/
|
|
22
|
+
function scanMarkdownFiles(dirPath, prefix = '') {
|
|
23
|
+
const files = [];
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(dirPath)) {
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
33
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
34
|
+
|
|
35
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
36
|
+
files.push({
|
|
37
|
+
path: relativePath,
|
|
38
|
+
fullPath,
|
|
39
|
+
name: entry.name,
|
|
40
|
+
});
|
|
41
|
+
} else if (entry.isDirectory()) {
|
|
42
|
+
files.push(...scanMarkdownFiles(fullPath, relativePath));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 生成规则索引部分
|
|
51
|
+
*/
|
|
52
|
+
function generateRulesIndex(rulesDir) {
|
|
53
|
+
const lines = [];
|
|
54
|
+
const commonDir = path.join(rulesDir, 'common');
|
|
55
|
+
const zhDir = path.join(rulesDir, 'zh');
|
|
56
|
+
|
|
57
|
+
lines.push('## 规则索引');
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push('本项目包含以下规则文件,可通过 `@path` 语法引用:');
|
|
60
|
+
lines.push('');
|
|
61
|
+
|
|
62
|
+
// 通用规则
|
|
63
|
+
if (fs.existsSync(commonDir)) {
|
|
64
|
+
lines.push('### 通用规则 (rules/common/)');
|
|
65
|
+
lines.push('');
|
|
66
|
+
const commonFiles = scanMarkdownFiles(commonDir, 'rules/common');
|
|
67
|
+
for (const file of commonFiles) {
|
|
68
|
+
const content = readMarkdownFile(file.fullPath);
|
|
69
|
+
const title = extractTitle(content, file.name);
|
|
70
|
+
lines.push(`- **${title}**: \`@${file.path}\``);
|
|
71
|
+
}
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 中文规则
|
|
76
|
+
if (fs.existsSync(zhDir)) {
|
|
77
|
+
lines.push('### 中文规则 (rules/zh/)');
|
|
78
|
+
lines.push('');
|
|
79
|
+
const zhFiles = scanMarkdownFiles(zhDir, 'rules/zh');
|
|
80
|
+
for (const file of zhFiles) {
|
|
81
|
+
const content = readMarkdownFile(file.fullPath);
|
|
82
|
+
const title = extractTitle(content, file.name);
|
|
83
|
+
lines.push(`- **${title}**: \`@${file.path}\``);
|
|
84
|
+
}
|
|
85
|
+
lines.push('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 语言特定规则
|
|
89
|
+
const languageDirs = ['typescript', 'python', 'golang', 'java', 'kotlin', 'rust', 'swift', 'cpp', 'csharp', 'php', 'perl'];
|
|
90
|
+
for (const lang of languageDirs) {
|
|
91
|
+
const langDir = path.join(rulesDir, lang);
|
|
92
|
+
if (fs.existsSync(langDir)) {
|
|
93
|
+
lines.push(`### ${lang.charAt(0).toUpperCase() + lang.slice(1)} 规则 (rules/${lang}/)`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
const langFiles = scanMarkdownFiles(langDir, `rules/${lang}`);
|
|
96
|
+
for (const file of langFiles) {
|
|
97
|
+
const content = readMarkdownFile(file.fullPath);
|
|
98
|
+
const title = extractTitle(content, file.name);
|
|
99
|
+
lines.push(`- **${title}**: \`@${file.path}\``);
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 从 Markdown 内容中提取标题
|
|
110
|
+
*/
|
|
111
|
+
function extractTitle(content, fallback) {
|
|
112
|
+
for (const line of content.split(/\r?\n/)) {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (trimmed.startsWith('# ')) {
|
|
115
|
+
return trimmed.slice(2).trim();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return fallback.replace('.md', '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 生成角色索引部分
|
|
123
|
+
*/
|
|
124
|
+
function generateRolesIndex(agentsDir) {
|
|
125
|
+
const lines = [];
|
|
126
|
+
const rolesDir = path.join(agentsDir, 'roles');
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(rolesDir)) {
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push('## 可用角色');
|
|
133
|
+
lines.push('');
|
|
134
|
+
|
|
135
|
+
const roleDisplay = {
|
|
136
|
+
'tech-lead': 'Tech Lead(技术负责人)',
|
|
137
|
+
'product-manager': 'Product Manager(产品经理)',
|
|
138
|
+
'project-manager': 'Project Manager(项目管理)',
|
|
139
|
+
'architect': 'Architect(架构师)',
|
|
140
|
+
'frontend-engineer': 'Frontend Engineer(前端开发)',
|
|
141
|
+
'backend-engineer': 'Backend Engineer(后端开发)',
|
|
142
|
+
'qa-engineer': 'QA Engineer(测试工程师)',
|
|
143
|
+
'devops-engineer': 'DevOps Engineer(运维工程师)',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const roleFiles = fs.readdirSync(rolesDir)
|
|
147
|
+
.filter(name => name.endsWith('.md'))
|
|
148
|
+
.sort();
|
|
149
|
+
|
|
150
|
+
for (const roleFile of roleFiles) {
|
|
151
|
+
const roleName = path.parse(roleFile).name;
|
|
152
|
+
const displayName = roleDisplay[roleName] || roleName;
|
|
153
|
+
lines.push(`- **${displayName}**: \`plugins/${PLUGIN_NAME}/agents/roles/${roleFile}\``);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push('');
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 生成命令索引部分
|
|
162
|
+
*/
|
|
163
|
+
function generateCommandsIndex(commandsDir) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
|
|
166
|
+
if (!fs.existsSync(commandsDir)) {
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lines.push('## 核心团队命令');
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push('| 命令 | 用途 |');
|
|
173
|
+
lines.push('|------|------|');
|
|
174
|
+
|
|
175
|
+
const commandFiles = fs.readdirSync(commandsDir)
|
|
176
|
+
.filter(name => name.endsWith('.md'))
|
|
177
|
+
.sort();
|
|
178
|
+
|
|
179
|
+
const commandDescriptions = {
|
|
180
|
+
'team-help': '根据当前阶段、artifacts 与阻塞项推荐下一步主链命令',
|
|
181
|
+
'team-intake': '接收需求并锁定目标、范围、约束',
|
|
182
|
+
'team-plan': '拆解任务、角色分工、依赖与里程碑',
|
|
183
|
+
'team-execute': '驱动研发角色在边界内实施',
|
|
184
|
+
'team-review': '做方案、质量、测试和放行评审',
|
|
185
|
+
'team-release': '做发布准备、上线检查与回滚保障',
|
|
186
|
+
'team-closeout': '在观察窗口结束后做最终收口与 backlog 回写',
|
|
187
|
+
'handoff': '在角色间做结构化交接',
|
|
188
|
+
'plan': '创建实现计划',
|
|
189
|
+
'tdd': '测试驱动开发',
|
|
190
|
+
'code-review': '代码审查',
|
|
191
|
+
'build-fix': '修复构建错误',
|
|
192
|
+
'verify': '验证实现',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
for (const commandFile of commandFiles) {
|
|
196
|
+
const commandName = path.parse(commandFile).name;
|
|
197
|
+
const description = commandDescriptions[commandName] || '团队命令';
|
|
198
|
+
lines.push(`| \`/${commandName}\` | ${description} |`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
lines.push('');
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 生成技能索引部分
|
|
207
|
+
*/
|
|
208
|
+
function generateSkillsIndex(skillsDir) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(skillsDir)) {
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
lines.push('## 可用技能');
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push('以下技能可通过 `skill` 工具加载:');
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
221
|
+
.filter(entry => entry.isDirectory())
|
|
222
|
+
.map(entry => entry.name)
|
|
223
|
+
.sort();
|
|
224
|
+
|
|
225
|
+
for (const skillDir of skillDirs) {
|
|
226
|
+
const skillFile = path.join(skillsDir, skillDir, 'SKILL.md');
|
|
227
|
+
if (fs.existsSync(skillFile)) {
|
|
228
|
+
const content = readMarkdownFile(skillFile);
|
|
229
|
+
const title = extractTitle(content, skillDir);
|
|
230
|
+
lines.push(`- **${title}**: \`skills/${skillDir}/SKILL.md\``);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
lines.push('');
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 生成完整的 AGENTS.md 内容
|
|
240
|
+
*/
|
|
241
|
+
function generateAgentsMd(root) {
|
|
242
|
+
const lines = [];
|
|
243
|
+
|
|
244
|
+
// 头部标记
|
|
245
|
+
lines.push('<!-- team-skills-platform -->');
|
|
246
|
+
lines.push('# Team Skills Platform — OpenCode 配置');
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push('本文件由安装脚本自动生成,包含 TSP 平台的完整配置和规则。');
|
|
249
|
+
lines.push('');
|
|
250
|
+
|
|
251
|
+
// 插件根路径
|
|
252
|
+
lines.push('## 插件路径');
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(`- 插件根目录: \`~/.config/opencode/plugins/${PLUGIN_NAME}/\``);
|
|
255
|
+
lines.push(`- 规则目录: \`~/.config/opencode/plugins/${PLUGIN_NAME}/rules/\``);
|
|
256
|
+
lines.push(`- 技能目录: \`~/.config/opencode/plugins/${PLUGIN_NAME}/skills/\``);
|
|
257
|
+
lines.push(`- 命令目录: \`~/.config/opencode/command/\``);
|
|
258
|
+
lines.push(`- Agent 目录: \`~/.config/opencode/agents/\``);
|
|
259
|
+
lines.push('');
|
|
260
|
+
|
|
261
|
+
// 规则索引
|
|
262
|
+
const rulesDir = path.join(root, 'rules');
|
|
263
|
+
lines.push(...generateRulesIndex(rulesDir));
|
|
264
|
+
|
|
265
|
+
// 角色索引
|
|
266
|
+
const agentsDir = path.join(root, 'agents');
|
|
267
|
+
lines.push(...generateRolesIndex(agentsDir));
|
|
268
|
+
|
|
269
|
+
// 命令索引
|
|
270
|
+
const commandsDir = path.join(root, 'commands');
|
|
271
|
+
lines.push(...generateCommandsIndex(commandsDir));
|
|
272
|
+
|
|
273
|
+
// 技能索引
|
|
274
|
+
const skillsDir = path.join(root, 'skills');
|
|
275
|
+
lines.push(...generateSkillsIndex(skillsDir));
|
|
276
|
+
|
|
277
|
+
// 使用说明
|
|
278
|
+
lines.push('## 使用说明');
|
|
279
|
+
lines.push('');
|
|
280
|
+
lines.push('1. **引用规则**: 使用 `@rules/common/coding-style.md` 语法引用特定规则');
|
|
281
|
+
lines.push('2. **加载技能**: 使用 `skill` 工具加载 `SKILL.md` 文件');
|
|
282
|
+
lines.push('3. **执行命令**: 使用 `/team-intake` 等命令执行团队工作流');
|
|
283
|
+
lines.push('4. **切换角色**: 使用 `@tech-lead` 等方式切换到特定角色');
|
|
284
|
+
lines.push('');
|
|
285
|
+
|
|
286
|
+
// 尾部标记
|
|
287
|
+
lines.push(`<!-- end ${PLUGIN_NAME} -->`);
|
|
288
|
+
|
|
289
|
+
return `${lines.join('\n')}\n`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 合并 AGENTS.md 内容
|
|
294
|
+
*/
|
|
295
|
+
function mergeAgentsMd(targetPath, newContent) {
|
|
296
|
+
const markerEnd = `<!-- end ${PLUGIN_NAME} -->`;
|
|
297
|
+
|
|
298
|
+
if (!fs.existsSync(targetPath)) {
|
|
299
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
300
|
+
fs.writeFileSync(targetPath, newContent, 'utf8');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
305
|
+
if (existing.includes('<!-- team-skills-platform -->')) {
|
|
306
|
+
const startIdx = existing.indexOf('<!-- team-skills-platform -->');
|
|
307
|
+
let endIdx = existing.indexOf(markerEnd, startIdx);
|
|
308
|
+
if (endIdx !== -1) {
|
|
309
|
+
endIdx += markerEnd.length;
|
|
310
|
+
if (existing[endIdx] === '\n') {
|
|
311
|
+
endIdx += 1;
|
|
312
|
+
}
|
|
313
|
+
fs.writeFileSync(targetPath, `${existing.slice(0, startIdx)}${newContent}`, 'utf8');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
319
|
+
fs.writeFileSync(targetPath, `${existing}${separator}${newContent}`, 'utf8');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 主函数
|
|
324
|
+
*/
|
|
325
|
+
function main() {
|
|
326
|
+
const root = path.join(__dirname, '../../..');
|
|
327
|
+
const opencodeHome = process.argv[2] || path.join(require('os').homedir(), '.config', 'opencode');
|
|
328
|
+
|
|
329
|
+
console.log('Generating OpenCode AGENTS.md...');
|
|
330
|
+
console.log(`Root: ${root}`);
|
|
331
|
+
console.log(`Target: ${opencodeHome}`);
|
|
332
|
+
|
|
333
|
+
const content = generateAgentsMd(root);
|
|
334
|
+
const targetPath = path.join(opencodeHome, 'AGENTS.md');
|
|
335
|
+
|
|
336
|
+
mergeAgentsMd(targetPath, content);
|
|
337
|
+
|
|
338
|
+
console.log(`✅ Generated AGENTS.md at ${targetPath}`);
|
|
339
|
+
console.log(` Size: ${content.length} characters`);
|
|
340
|
+
console.log(` Lines: ${content.split('\n').length}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 导出函数供其他脚本使用
|
|
344
|
+
module.exports = {
|
|
345
|
+
generateAgentsMd,
|
|
346
|
+
mergeAgentsMd,
|
|
347
|
+
generateRulesIndex,
|
|
348
|
+
generateRolesIndex,
|
|
349
|
+
generateCommandsIndex,
|
|
350
|
+
generateSkillsIndex,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// 如果直接运行此脚本
|
|
354
|
+
if (require.main === module) {
|
|
355
|
+
try {
|
|
356
|
+
main();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Error generating AGENTS.md:', error.message);
|
|
359
|
+
process.exitCode = 1;
|
|
360
|
+
}
|
|
361
|
+
}
|