@colin4k1024/tsp 2.5.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * 角色配置映射
7
+ */
8
+ const ROLE_CONFIGS = {
9
+ 'tech-lead': {
10
+ description: 'Tech Lead(技术负责人)- 负责需求 intake、任务拆解、角色分派、冲突决策与最终交付收口',
11
+ tools: { '*': true },
12
+ color: '#3B82F6',
13
+ },
14
+ 'product-manager': {
15
+ description: 'Product Manager(产品经理)- 负责需求澄清、PRD 编写、用户故事和验收标准定义',
16
+ tools: { read: true, write: true, grep: true, glob: true, webfetch: true },
17
+ color: '#10B981',
18
+ },
19
+ 'project-manager': {
20
+ description: 'Project Manager(项目管理)- 负责排期、依赖管理、里程碑跟踪和风险推进',
21
+ tools: { read: true, write: true, grep: true, glob: true },
22
+ color: '#F59E0B',
23
+ },
24
+ 'architect': {
25
+ description: 'Architect(架构师)- 负责 ADR、系统边界、接口与数据契约设计',
26
+ tools: { read: true, write: true, grep: true, glob: true, webfetch: true },
27
+ color: '#8B5CF6',
28
+ },
29
+ 'frontend-engineer': {
30
+ description: 'Frontend Engineer(前端开发)- 负责前端实现与自测',
31
+ tools: { edit: true, write: true, bash: true, read: true, grep: true, glob: true },
32
+ color: '#EC4899',
33
+ },
34
+ 'backend-engineer': {
35
+ description: 'Backend Engineer(后端开发)- 负责后端实现与自测',
36
+ tools: { edit: true, write: true, bash: true, read: true, grep: true, glob: true },
37
+ color: '#06B6D4',
38
+ },
39
+ 'qa-engineer': {
40
+ description: 'QA Engineer(测试工程师)- 负责测试计划、回归验证和放行建议',
41
+ tools: { read: true, grep: true, glob: true, bash: true },
42
+ color: '#F97316',
43
+ },
44
+ 'devops-engineer': {
45
+ description: 'DevOps Engineer(运维工程师)- 负责发布、监控、回滚与运行保障',
46
+ tools: { edit: true, write: true, bash: true, read: true, grep: true, glob: true },
47
+ color: '#6366F1',
48
+ },
49
+ };
50
+
51
+ /**
52
+ * Specialist 配置映射
53
+ */
54
+ const SPECIALIST_CONFIGS = {
55
+ 'planner': {
56
+ description: 'Planner - 实现规划专家',
57
+ tools: { read: true, grep: true, glob: true },
58
+ hidden: true,
59
+ },
60
+ 'architect': {
61
+ description: 'Architect - 系统设计专家',
62
+ tools: { read: true, grep: true, glob: true },
63
+ hidden: true,
64
+ },
65
+ 'tdd-guide': {
66
+ description: 'TDD Guide - 测试驱动开发专家',
67
+ tools: { read: true, write: true, edit: true, bash: true, grep: true },
68
+ hidden: true,
69
+ },
70
+ 'code-reviewer': {
71
+ description: 'Code Reviewer - 代码审查专家',
72
+ tools: { read: true, grep: true, glob: true, bash: true },
73
+ hidden: true,
74
+ },
75
+ 'security-reviewer': {
76
+ description: 'Security Reviewer - 安全审查专家',
77
+ tools: { read: true, grep: true, glob: true },
78
+ hidden: true,
79
+ },
80
+ 'build-error-resolver': {
81
+ description: 'Build Error Resolver - 构建错误修复专家',
82
+ tools: { read: true, write: true, edit: true, bash: true, grep: true, glob: true },
83
+ hidden: true,
84
+ },
85
+ };
86
+
87
+ /**
88
+ * 从 Markdown 内容中提取描述
89
+ */
90
+ function extractDescription(content, fallback) {
91
+ for (const line of content.split(/\r?\n/)) {
92
+ const trimmed = line.trim();
93
+ if (trimmed.startsWith('# ')) {
94
+ return trimmed.slice(2).trim();
95
+ }
96
+ }
97
+ return fallback;
98
+ }
99
+
100
+ /**
101
+ * 为 Agent 文件添加 YAML front matter
102
+ */
103
+ function addFrontMatter(filePath, config, isSpecialist = false) {
104
+ const content = fs.readFileSync(filePath, 'utf8');
105
+ const fileName = path.parse(filePath).name;
106
+
107
+ // 检查是否已经有 front matter
108
+ if (content.startsWith('---')) {
109
+ console.log(`Skipping ${fileName} - already has front matter`);
110
+ return content;
111
+ }
112
+
113
+ const description = config.description || extractDescription(content, fileName);
114
+ const tools = config.tools || { '*': true };
115
+
116
+ const frontMatter = [
117
+ '---',
118
+ `description: "${description.replace(/"/g, '\\"')}"`,
119
+ `tools:`,
120
+ ];
121
+
122
+ // Format tools as YAML object
123
+ for (const [tool, enabled] of Object.entries(tools)) {
124
+ frontMatter.push(` ${tool}: ${enabled}`);
125
+ }
126
+
127
+ if (config.color) {
128
+ frontMatter.push(`color: "${config.color}"`);
129
+ }
130
+
131
+ if (config.hidden) {
132
+ frontMatter.push(`hidden: true`);
133
+ }
134
+
135
+ frontMatter.push('---', '');
136
+
137
+ return `${frontMatter.join('\n')}${content}`;
138
+ }
139
+
140
+ /**
141
+ * 转换角色 agents
142
+ */
143
+ function convertRoleAgents(sourceDir, targetDir) {
144
+ if (!fs.existsSync(sourceDir)) {
145
+ console.log(`Source directory not found: ${sourceDir}`);
146
+ return;
147
+ }
148
+
149
+ fs.mkdirSync(targetDir, { recursive: true });
150
+
151
+ const files = fs.readdirSync(sourceDir)
152
+ .filter(file => file.endsWith('.md'))
153
+ .sort();
154
+
155
+ for (const file of files) {
156
+ const sourcePath = path.join(sourceDir, file);
157
+ const targetPath = path.join(targetDir, file);
158
+ const roleName = path.parse(file).name;
159
+
160
+ const config = ROLE_CONFIGS[roleName] || {
161
+ description: extractDescription(fs.readFileSync(sourcePath, 'utf8'), roleName),
162
+ tools: { '*': true },
163
+ };
164
+
165
+ const content = addFrontMatter(sourcePath, config, false);
166
+ fs.writeFileSync(targetPath, content, 'utf8');
167
+ console.log(`Converted role agent: ${file}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 转换 specialist agents
173
+ */
174
+ function convertSpecialistAgents(sourceDir, targetDir) {
175
+ if (!fs.existsSync(sourceDir)) {
176
+ console.log(`Source directory not found: ${sourceDir}`);
177
+ return;
178
+ }
179
+
180
+ fs.mkdirSync(targetDir, { recursive: true });
181
+
182
+ const files = fs.readdirSync(sourceDir)
183
+ .filter(file => file.endsWith('.md'))
184
+ .sort();
185
+
186
+ for (const file of files) {
187
+ const sourcePath = path.join(sourceDir, file);
188
+ const targetPath = path.join(targetDir, file);
189
+ const specialistName = path.parse(file).name;
190
+
191
+ const config = SPECIALIST_CONFIGS[specialistName] || {
192
+ description: extractDescription(fs.readFileSync(sourcePath, 'utf8'), specialistName),
193
+ tools: { '*': true },
194
+ hidden: true,
195
+ };
196
+
197
+ const content = addFrontMatter(sourcePath, config, true);
198
+ fs.writeFileSync(targetPath, content, 'utf8');
199
+ console.log(`Converted specialist agent: ${file}`);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * 主函数
205
+ */
206
+ function main() {
207
+ const root = path.join(__dirname, '../../..');
208
+ const opencodeHome = process.argv[2] || path.join(require('os').homedir(), '.config', 'opencode');
209
+ const pluginDir = path.join(opencodeHome, 'plugins', 'team-skills-platform');
210
+
211
+ console.log('Converting agents for OpenCode...');
212
+ console.log(`Root: ${root}`);
213
+ console.log(`Target: ${opencodeHome}`);
214
+
215
+ // 转换角色 agents
216
+ const rolesSourceDir = path.join(root, 'agents', 'roles');
217
+ const rolesTargetDir = path.join(pluginDir, 'agents', 'roles');
218
+ convertRoleAgents(rolesSourceDir, rolesTargetDir);
219
+
220
+ // 转换 specialist agents
221
+ const specialistsSourceDir = path.join(root, 'agents', 'specialists');
222
+ const specialistsTargetDir = path.join(pluginDir, 'agents', 'specialists');
223
+ convertSpecialistAgents(specialistsSourceDir, specialistsTargetDir);
224
+
225
+ // 同时复制到 opencodeHome/agents/ 目录
226
+ const agentsTargetDir = path.join(opencodeHome, 'agents');
227
+ fs.mkdirSync(agentsTargetDir, { recursive: true });
228
+
229
+ // 复制角色 agents
230
+ if (fs.existsSync(rolesTargetDir)) {
231
+ for (const file of fs.readdirSync(rolesTargetDir)) {
232
+ if (file.endsWith('.md')) {
233
+ fs.copyFileSync(
234
+ path.join(rolesTargetDir, file),
235
+ path.join(agentsTargetDir, file)
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ // 复制 specialist agents
242
+ if (fs.existsSync(specialistsTargetDir)) {
243
+ for (const file of fs.readdirSync(specialistsTargetDir)) {
244
+ if (file.endsWith('.md')) {
245
+ fs.copyFileSync(
246
+ path.join(specialistsTargetDir, file),
247
+ path.join(agentsTargetDir, `specialist-${file}`)
248
+ );
249
+ }
250
+ }
251
+ }
252
+
253
+ console.log('✅ Agent conversion completed');
254
+ }
255
+
256
+ // 导出函数供其他脚本使用
257
+ module.exports = {
258
+ addFrontMatter,
259
+ convertRoleAgents,
260
+ convertSpecialistAgents,
261
+ ROLE_CONFIGS,
262
+ SPECIALIST_CONFIGS,
263
+ };
264
+
265
+ // 如果直接运行此脚本
266
+ if (require.main === module) {
267
+ try {
268
+ main();
269
+ } catch (error) {
270
+ console.error('Error converting agents:', error.message);
271
+ process.exitCode = 1;
272
+ }
273
+ }
@@ -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
+ }