coding-tool-x 3.2.0

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.
Files changed (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. package/src/utils/session.js +240 -0
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Commands 服务
3
+ *
4
+ * 管理 Claude/OpenCode 自定义命令的 CRUD 操作
5
+ * 支持从 GitHub 仓库扫描和安装命令
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const { RepoScannerBase } = require('./repo-scanner-base');
12
+ const { NATIVE_PATHS } = require('../../config/paths');
13
+ const {
14
+ parseCommandContent,
15
+ parseFrontmatter
16
+ } = require('./format-converter');
17
+
18
+ // 默认仓库源
19
+ const DEFAULT_REPOS = [];
20
+ const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
21
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
22
+
23
+ const PLATFORM_CONFIG = {
24
+ claude: {
25
+ userCommandsDir: path.join(os.homedir(), '.claude', 'commands'),
26
+ projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
27
+ repoType: 'commands'
28
+ },
29
+ opencode: {
30
+ userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
31
+ legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
32
+ projectCommandsDir: (projectPath) => {
33
+ const modern = path.join(projectPath, '.opencode', 'commands');
34
+ const legacy = path.join(projectPath, '.opencode', 'command');
35
+ if (fs.existsSync(legacy) && !fs.existsSync(modern)) {
36
+ return legacy;
37
+ }
38
+ return modern;
39
+ },
40
+ repoType: 'opencode-commands'
41
+ }
42
+ };
43
+
44
+ function normalizePlatform(platform) {
45
+ return SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
46
+ }
47
+
48
+ /**
49
+ * 确保目录存在
50
+ */
51
+ function ensureDir(dirPath) {
52
+ if (!fs.existsSync(dirPath)) {
53
+ fs.mkdirSync(dirPath, { recursive: true });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 生成 frontmatter 字符串(用于命令创建/更新)
59
+ */
60
+ function generateCommandFrontmatter(data) {
61
+ const lines = ['---'];
62
+
63
+ if (data.description) {
64
+ lines.push(`description: "${data.description}"`);
65
+ }
66
+ if (data['allowed-tools']) {
67
+ lines.push(`allowed-tools: ${data['allowed-tools']}`);
68
+ }
69
+ if (data['argument-hint']) {
70
+ lines.push(`argument-hint: ${data['argument-hint']}`);
71
+ }
72
+ if (data.model) {
73
+ lines.push(`model: ${data.model}`);
74
+ }
75
+ if (data.context) {
76
+ lines.push(`context: ${data.context}`);
77
+ }
78
+ if (data.agent) {
79
+ lines.push(`agent: ${data.agent}`);
80
+ }
81
+ if (typeof data.subtask === 'boolean') {
82
+ lines.push(`subtask: ${data.subtask}`);
83
+ }
84
+
85
+ lines.push('---');
86
+ return lines.join('\n');
87
+ }
88
+
89
+ /**
90
+ * 递归扫描目录获取命令文件
91
+ */
92
+ function scanCommandsDir(dir, basePath, scope) {
93
+ const commands = [];
94
+
95
+ if (!fs.existsSync(dir)) {
96
+ return commands;
97
+ }
98
+
99
+ try {
100
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
101
+
102
+ for (const entry of entries) {
103
+ const fullPath = path.join(dir, entry.name);
104
+
105
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
106
+ // 递归扫描子目录
107
+ const subCommands = scanCommandsDir(fullPath, basePath, scope);
108
+ commands.push(...subCommands);
109
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
110
+ // 解析命令文件
111
+ try {
112
+ const content = fs.readFileSync(fullPath, 'utf-8');
113
+ const { frontmatter, body } = parseFrontmatter(content);
114
+
115
+ // 计算相对路径和命令名
116
+ const relativePath = path.relative(basePath, fullPath);
117
+ const commandName = entry.name.replace(/\.md$/, '');
118
+ const namespace = path.dirname(relativePath);
119
+
120
+ commands.push({
121
+ name: commandName,
122
+ namespace: namespace === '.' ? null : namespace,
123
+ scope,
124
+ path: relativePath,
125
+ fullPath,
126
+ description: frontmatter.description || '',
127
+ allowedTools: frontmatter['allowed-tools'] || '',
128
+ argumentHint: frontmatter['argument-hint'] || '',
129
+ agent: frontmatter.agent || '',
130
+ model: frontmatter.model || '',
131
+ subtask: frontmatter.subtask || '',
132
+ body,
133
+ fullContent: content,
134
+ updatedAt: fs.statSync(fullPath).mtime.getTime()
135
+ });
136
+ } catch (err) {
137
+ console.warn(`[CommandsService] Failed to parse ${fullPath}:`, err.message);
138
+ }
139
+ }
140
+ }
141
+ } catch (err) {
142
+ console.error(`[CommandsService] Failed to scan ${dir}:`, err.message);
143
+ }
144
+
145
+ return commands;
146
+ }
147
+
148
+ /**
149
+ * Commands 仓库扫描器
150
+ */
151
+ class CommandsRepoScanner extends RepoScannerBase {
152
+ constructor(platform, installDir) {
153
+ super({
154
+ type: PLATFORM_CONFIG[platform]?.repoType || 'commands',
155
+ installDir,
156
+ markerFile: null, // 直接扫描 .md 文件
157
+ fileExtension: '.md',
158
+ defaultRepos: DEFAULT_REPOS
159
+ });
160
+ }
161
+
162
+ /**
163
+ * 获取并解析单个命令文件
164
+ */
165
+ async fetchAndParseItem(file, repo, baseDir) {
166
+ try {
167
+ // 计算相对路径
168
+ const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
169
+ const fileName = path.basename(file.path, '.md');
170
+ const namespace = path.dirname(relativePath);
171
+
172
+ // 获取文件内容
173
+ const content = await this.fetchRawContent(repo, file.path);
174
+ const { frontmatter, body } = this.parseFrontmatter(content);
175
+
176
+ return {
177
+ key: `${repo.owner}/${repo.name}:${relativePath}`,
178
+ name: fileName,
179
+ namespace: namespace === '.' ? null : namespace,
180
+ scope: 'remote',
181
+ path: relativePath,
182
+ repoPath: file.path,
183
+ description: frontmatter.description || '',
184
+ allowedTools: frontmatter['allowed-tools'] || '',
185
+ argumentHint: frontmatter['argument-hint'] || '',
186
+ agent: frontmatter.agent || '',
187
+ model: frontmatter.model || '',
188
+ subtask: frontmatter.subtask || '',
189
+ body,
190
+ fullContent: content,
191
+ installed: this.isInstalled(relativePath),
192
+ readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
193
+ repoOwner: repo.owner,
194
+ repoName: repo.name,
195
+ repoBranch: repo.branch,
196
+ repoDirectory: repo.directory || ''
197
+ };
198
+ } catch (err) {
199
+ console.warn(`[CommandsRepoScanner] Parse command ${file.path} error:`, err.message);
200
+ return null;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * 检查命令是否已安装
206
+ */
207
+ isInstalled(relativePath) {
208
+ const fullPath = path.join(this.installDir, relativePath);
209
+ return fs.existsSync(fullPath);
210
+ }
211
+
212
+ /**
213
+ * 获取去重 key
214
+ */
215
+ getDedupeKey(item) {
216
+ // 使用 namespace/name 作为去重 key
217
+ return item.namespace ? `${item.namespace}/${item.name}`.toLowerCase() : item.name.toLowerCase();
218
+ }
219
+
220
+ /**
221
+ * 安装命令
222
+ */
223
+ async installCommand(item) {
224
+ const repo = {
225
+ owner: item.repoOwner,
226
+ name: item.repoName,
227
+ branch: item.repoBranch
228
+ };
229
+
230
+ return this.installFromRepo(item.repoPath, repo, item.path);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Commands 服务类
236
+ */
237
+ class CommandsService {
238
+ constructor(platform = 'claude') {
239
+ this.platform = normalizePlatform(platform);
240
+ const config = PLATFORM_CONFIG[this.platform];
241
+
242
+ this.userCommandsDir = config.userCommandsDir;
243
+ if (this.platform === 'opencode') {
244
+ const legacyUserDir = config.legacyUserCommandsDir;
245
+ if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userCommandsDir)) {
246
+ this.userCommandsDir = legacyUserDir;
247
+ }
248
+ }
249
+
250
+ this.projectCommandsDir = config.projectCommandsDir;
251
+ this.repoScanner = new CommandsRepoScanner(this.platform, this.userCommandsDir);
252
+ ensureDir(this.userCommandsDir);
253
+ }
254
+
255
+ getProjectCommandsDir(projectPath) {
256
+ if (!projectPath) return null;
257
+ return this.projectCommandsDir(projectPath);
258
+ }
259
+
260
+ /**
261
+ * 获取所有命令列表
262
+ * @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
263
+ */
264
+ listCommands(projectPath = null) {
265
+ const commands = [];
266
+
267
+ // 获取用户级命令
268
+ const userCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user');
269
+ commands.push(...userCommands);
270
+
271
+ // 获取项目级命令(如果提供了项目路径)
272
+ if (projectPath) {
273
+ const projectCommandsDir = this.getProjectCommandsDir(projectPath);
274
+ const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
275
+ commands.push(...projectCommands);
276
+ }
277
+
278
+ // 按名称排序
279
+ commands.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
280
+
281
+ return {
282
+ commands,
283
+ total: commands.length,
284
+ userCount: userCommands.length,
285
+ projectCount: commands.length - userCommands.length
286
+ };
287
+ }
288
+
289
+ /**
290
+ * 获取所有命令(包括远程仓库)
291
+ * @param {boolean} forceRefresh - 强制刷新远程缓存
292
+ */
293
+ async listAllCommands(projectPath = null, forceRefresh = false) {
294
+ // 获取本地命令
295
+ const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath);
296
+
297
+ // 获取远程命令
298
+ let remoteCommands = [];
299
+ try {
300
+ remoteCommands = await this.repoScanner.listRemoteItems(forceRefresh);
301
+
302
+ // 更新安装状态
303
+ for (const cmd of remoteCommands) {
304
+ cmd.installed = this.repoScanner.isInstalled(cmd.path);
305
+ }
306
+ } catch (err) {
307
+ console.warn('[CommandsService] Failed to fetch remote commands:', err.message);
308
+ }
309
+
310
+ // 合并列表(本地优先)
311
+ const allCommands = [...localCommands];
312
+ const localKeys = new Set(localCommands.map(c =>
313
+ c.namespace ? `${c.namespace}/${c.name}`.toLowerCase() : c.name.toLowerCase()
314
+ ));
315
+
316
+ for (const remote of remoteCommands) {
317
+ const key = remote.namespace ? `${remote.namespace}/${remote.name}`.toLowerCase() : remote.name.toLowerCase();
318
+ if (!localKeys.has(key)) {
319
+ allCommands.push(remote);
320
+ }
321
+ }
322
+
323
+ // 排序
324
+ allCommands.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
325
+
326
+ return {
327
+ commands: allCommands,
328
+ total: allCommands.length,
329
+ userCount,
330
+ projectCount,
331
+ remoteCount: remoteCommands.length
332
+ };
333
+ }
334
+
335
+ /**
336
+ * 获取单个命令详情
337
+ */
338
+ getCommand(name, scope, projectPath = null, namespace = null) {
339
+ const baseDir = scope === 'user'
340
+ ? this.userCommandsDir
341
+ : this.getProjectCommandsDir(projectPath);
342
+
343
+ const relativePath = namespace
344
+ ? path.join(namespace, `${name}.md`)
345
+ : `${name}.md`;
346
+
347
+ const fullPath = path.join(baseDir, relativePath);
348
+
349
+ if (!fs.existsSync(fullPath)) {
350
+ return null;
351
+ }
352
+
353
+ const content = fs.readFileSync(fullPath, 'utf-8');
354
+ const { frontmatter, body } = parseFrontmatter(content);
355
+
356
+ return {
357
+ name,
358
+ namespace,
359
+ scope,
360
+ path: relativePath,
361
+ fullPath,
362
+ description: frontmatter.description || '',
363
+ allowedTools: frontmatter['allowed-tools'] || '',
364
+ argumentHint: frontmatter['argument-hint'] || '',
365
+ agent: frontmatter.agent || '',
366
+ model: frontmatter.model || '',
367
+ subtask: frontmatter.subtask || '',
368
+ body,
369
+ fullContent: content,
370
+ updatedAt: fs.statSync(fullPath).mtime.getTime()
371
+ };
372
+ }
373
+
374
+ /**
375
+ * 创建命令
376
+ */
377
+ createCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
378
+ if (!name || !name.trim()) {
379
+ throw new Error('命令名称不能为空');
380
+ }
381
+
382
+ // 验证命令名:只允许字母、数字、横杠、下划线
383
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
384
+ throw new Error('命令名只能包含字母、数字、横杠和下划线');
385
+ }
386
+
387
+ const baseDir = scope === 'user'
388
+ ? this.userCommandsDir
389
+ : this.getProjectCommandsDir(projectPath);
390
+
391
+ const targetDir = namespace ? path.join(baseDir, namespace) : baseDir;
392
+ ensureDir(targetDir);
393
+
394
+ const filePath = path.join(targetDir, `${name}.md`);
395
+
396
+ // 检查是否已存在
397
+ if (fs.existsSync(filePath)) {
398
+ throw new Error(`命令 "${name}" 已存在`);
399
+ }
400
+
401
+ // 生成文件内容
402
+ const frontmatterData = {};
403
+ if (description) frontmatterData.description = description;
404
+ if (this.platform !== 'opencode') {
405
+ if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
406
+ if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
407
+ }
408
+ if (agent) frontmatterData.agent = agent;
409
+ if (model) frontmatterData.model = model;
410
+ if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
411
+
412
+ let content = '';
413
+ if (Object.keys(frontmatterData).length > 0) {
414
+ content = generateCommandFrontmatter(frontmatterData) + '\n\n';
415
+ }
416
+ content += body || '';
417
+
418
+ fs.writeFileSync(filePath, content, 'utf-8');
419
+
420
+ return this.getCommand(name, scope, projectPath, namespace);
421
+ }
422
+
423
+ /**
424
+ * 更新命令
425
+ */
426
+ updateCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
427
+ const baseDir = scope === 'user'
428
+ ? this.userCommandsDir
429
+ : this.getProjectCommandsDir(projectPath);
430
+
431
+ const relativePath = namespace
432
+ ? path.join(namespace, `${name}.md`)
433
+ : `${name}.md`;
434
+
435
+ const filePath = path.join(baseDir, relativePath);
436
+
437
+ if (!fs.existsSync(filePath)) {
438
+ throw new Error(`命令 "${name}" 不存在`);
439
+ }
440
+
441
+ // 生成文件内容
442
+ const frontmatterData = {};
443
+ if (description) frontmatterData.description = description;
444
+ if (this.platform !== 'opencode') {
445
+ if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
446
+ if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
447
+ }
448
+ if (agent) frontmatterData.agent = agent;
449
+ if (model) frontmatterData.model = model;
450
+ if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
451
+
452
+ let content = '';
453
+ if (Object.keys(frontmatterData).length > 0) {
454
+ content = generateCommandFrontmatter(frontmatterData) + '\n\n';
455
+ }
456
+ content += body || '';
457
+
458
+ fs.writeFileSync(filePath, content, 'utf-8');
459
+
460
+ return this.getCommand(name, scope, projectPath, namespace);
461
+ }
462
+
463
+ /**
464
+ * 删除命令
465
+ */
466
+ deleteCommand(name, scope, projectPath = null, namespace = null) {
467
+ const baseDir = scope === 'user'
468
+ ? this.userCommandsDir
469
+ : this.getProjectCommandsDir(projectPath);
470
+
471
+ const relativePath = namespace
472
+ ? path.join(namespace, `${name}.md`)
473
+ : `${name}.md`;
474
+
475
+ const filePath = path.join(baseDir, relativePath);
476
+
477
+ if (!fs.existsSync(filePath)) {
478
+ return { success: false, message: '命令不存在' };
479
+ }
480
+
481
+ fs.unlinkSync(filePath);
482
+
483
+ // 如果目录为空,删除目录
484
+ if (namespace) {
485
+ const namespaceDir = path.join(baseDir, namespace);
486
+ try {
487
+ const remaining = fs.readdirSync(namespaceDir);
488
+ if (remaining.length === 0) {
489
+ fs.rmdirSync(namespaceDir);
490
+ }
491
+ } catch (err) {
492
+ // 忽略删除目录错误
493
+ }
494
+ }
495
+
496
+ return { success: true, message: '命令已删除' };
497
+ }
498
+
499
+ /**
500
+ * 获取统计信息
501
+ */
502
+ getStats(projectPath = null) {
503
+ const { commands, userCount, projectCount } = this.listCommands(projectPath);
504
+
505
+ // 按命名空间分组
506
+ const namespaces = {};
507
+ for (const cmd of commands) {
508
+ const ns = cmd.namespace || '(root)';
509
+ if (!namespaces[ns]) {
510
+ namespaces[ns] = 0;
511
+ }
512
+ namespaces[ns]++;
513
+ }
514
+
515
+ return {
516
+ total: commands.length,
517
+ userCount,
518
+ projectCount,
519
+ namespaces
520
+ };
521
+ }
522
+
523
+ // ==================== 仓库管理 ====================
524
+
525
+ /**
526
+ * 获取仓库列表
527
+ */
528
+ getRepos() {
529
+ return this.repoScanner.loadRepos();
530
+ }
531
+
532
+ /**
533
+ * 添加仓库
534
+ */
535
+ addRepo(repo) {
536
+ return this.repoScanner.addRepo(repo);
537
+ }
538
+
539
+ /**
540
+ * 删除仓库
541
+ */
542
+ removeRepo(owner, name, directory = '') {
543
+ return this.repoScanner.removeRepo(owner, name, directory);
544
+ }
545
+
546
+ /**
547
+ * 切换仓库启用状态
548
+ */
549
+ toggleRepo(owner, name, directory = '', enabled) {
550
+ return this.repoScanner.toggleRepo(owner, name, directory, enabled);
551
+ }
552
+
553
+ /**
554
+ * 从远程仓库安装命令
555
+ */
556
+ async installFromRemote(command) {
557
+ return this.repoScanner.installCommand(command);
558
+ }
559
+
560
+ /**
561
+ * 卸载命令
562
+ */
563
+ uninstallCommand(relativePath) {
564
+ return this.repoScanner.uninstall(relativePath);
565
+ }
566
+
567
+ // ==================== 格式转换 ====================
568
+
569
+ }
570
+
571
+ module.exports = {
572
+ CommandsService,
573
+ DEFAULT_REPOS
574
+ };