@adversity/coding-tool-x 2.2.0 → 2.3.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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -14
- package/dist/web/assets/index-dhun1bYQ.js +3555 -0
- package/dist/web/assets/index-hHb7DAda.css +41 -0
- package/dist/web/index.html +2 -2
- package/package.json +5 -4
- package/src/index.js +2 -2
- package/src/server/api/agents.js +188 -0
- package/src/server/api/commands.js +261 -0
- package/src/server/api/config-templates.js +20 -5
- package/src/server/api/permissions.js +347 -0
- package/src/server/api/rules.js +188 -0
- package/src/server/api/skills.js +66 -14
- package/src/server/api/workspaces.js +30 -55
- package/src/server/index.js +3 -0
- package/src/server/services/agents-service.js +179 -1
- package/src/server/services/commands-service.js +231 -47
- package/src/server/services/config-templates-service.js +457 -106
- package/src/server/services/format-converter.js +506 -0
- package/src/server/services/repo-scanner-base.js +678 -0
- package/src/server/services/rules-service.js +179 -1
- package/src/server/services/skill-service.js +114 -61
- package/src/server/services/workspace-service.js +110 -1
- package/dist/web/assets/index-D1AYlFLZ.js +0 -3220
- package/dist/web/assets/index-aL3cKxSK.css +0 -41
- package/docs/CHANGELOG.md +0 -582
- package/docs/DIRECTORY_MIGRATION.md +0 -112
- package/docs/PROJECT_STRUCTURE.md +0 -396
|
@@ -105,12 +105,38 @@ router.get('/check-git/*', (req, res) => {
|
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* GET /api/workspaces/available-projects
|
|
110
|
+
* 获取所有渠道(Claude/Codex/Gemini)的项目并集
|
|
111
|
+
*/
|
|
112
|
+
router.get('/available-projects', (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const projects = workspaceService.getAllAvailableProjects();
|
|
115
|
+
res.json({
|
|
116
|
+
success: true,
|
|
117
|
+
data: projects
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
res.status(500).json({
|
|
121
|
+
success: false,
|
|
122
|
+
message: error.message
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
108
127
|
/**
|
|
109
128
|
* GET /api/workspaces/:id
|
|
110
129
|
* 获取单个工作区详情
|
|
111
130
|
*/
|
|
112
|
-
|
|
131
|
+
// 注意:此路由需要放在所有静态子路由之后,避免把 /available-projects 等路径当成 id
|
|
132
|
+
router.get('/:id', (req, res, next) => {
|
|
113
133
|
try {
|
|
134
|
+
// 兜底:即便路由顺序被改动,也避免把保留路径当成工作区 ID
|
|
135
|
+
const reservedIds = new Set(['available-projects', 'read-file']);
|
|
136
|
+
if (reservedIds.has(req.params.id)) {
|
|
137
|
+
return next();
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
const workspace = workspaceService.getWorkspace(req.params.id);
|
|
115
141
|
if (!workspace) {
|
|
116
142
|
return res.status(404).json({
|
|
@@ -147,7 +173,7 @@ router.get('/:id', (req, res) => {
|
|
|
147
173
|
*/
|
|
148
174
|
router.post('/', (req, res) => {
|
|
149
175
|
try {
|
|
150
|
-
const { name, description, baseDir, projects, configTemplateId } = req.body;
|
|
176
|
+
const { name, description, baseDir, projects, configTemplateId, permissionTemplate } = req.body;
|
|
151
177
|
|
|
152
178
|
if (!name || !name.trim()) {
|
|
153
179
|
return res.status(400).json({
|
|
@@ -185,7 +211,8 @@ router.post('/', (req, res) => {
|
|
|
185
211
|
description,
|
|
186
212
|
baseDir,
|
|
187
213
|
projects,
|
|
188
|
-
configTemplateId
|
|
214
|
+
configTemplateId,
|
|
215
|
+
permissionTemplate
|
|
189
216
|
});
|
|
190
217
|
|
|
191
218
|
res.json({
|
|
@@ -322,25 +349,6 @@ router.delete('/:id/projects/:projectName', (req, res) => {
|
|
|
322
349
|
}
|
|
323
350
|
});
|
|
324
351
|
|
|
325
|
-
/**
|
|
326
|
-
* GET /api/workspaces/available-projects
|
|
327
|
-
* 获取所有渠道(Claude/Codex/Gemini)的项目并集
|
|
328
|
-
*/
|
|
329
|
-
router.get('/available-projects', (req, res) => {
|
|
330
|
-
try {
|
|
331
|
-
const projects = workspaceService.getAllAvailableProjects();
|
|
332
|
-
res.json({
|
|
333
|
-
success: true,
|
|
334
|
-
data: projects
|
|
335
|
-
});
|
|
336
|
-
} catch (error) {
|
|
337
|
-
res.status(500).json({
|
|
338
|
-
success: false,
|
|
339
|
-
message: error.message
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
|
|
344
352
|
/**
|
|
345
353
|
* POST /api/workspaces/:id/launch
|
|
346
354
|
* 获取在工作区启动 CLI 工具的命令
|
|
@@ -371,37 +379,4 @@ router.post('/:id/launch', (req, res) => {
|
|
|
371
379
|
}
|
|
372
380
|
});
|
|
373
381
|
|
|
374
|
-
/**
|
|
375
|
-
* GET /api/workspaces/check-git/:projectPath
|
|
376
|
-
* 检查项目是否是 git 仓库并获取 worktrees
|
|
377
|
-
*/
|
|
378
|
-
router.get('/check-git/*', (req, res) => {
|
|
379
|
-
try {
|
|
380
|
-
const projectPath = req.params[0];
|
|
381
|
-
|
|
382
|
-
if (!projectPath) {
|
|
383
|
-
return res.status(400).json({
|
|
384
|
-
success: false,
|
|
385
|
-
message: '项目路径不能为空'
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const isGit = workspaceService.isGitRepo(projectPath);
|
|
390
|
-
const worktrees = isGit ? workspaceService.getGitWorktrees(projectPath) : [];
|
|
391
|
-
|
|
392
|
-
res.json({
|
|
393
|
-
success: true,
|
|
394
|
-
data: {
|
|
395
|
-
isGitRepo: isGit,
|
|
396
|
-
worktrees
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
} catch (error) {
|
|
400
|
-
res.status(500).json({
|
|
401
|
-
success: false,
|
|
402
|
-
message: error.message
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
382
|
module.exports = router;
|
package/src/server/index.js
CHANGED
|
@@ -143,6 +143,9 @@ async function startServer(port) {
|
|
|
143
143
|
// 配置模板 API
|
|
144
144
|
app.use('/api/config-templates', require('./api/config-templates'));
|
|
145
145
|
|
|
146
|
+
// 命令执行权限 API
|
|
147
|
+
app.use('/api/permissions', require('./api/permissions'));
|
|
148
|
+
|
|
146
149
|
// 健康检查 API
|
|
147
150
|
app.use('/api/health-check', require('./api/health-check')(config));
|
|
148
151
|
|
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
* 代理目录:
|
|
6
6
|
* - 用户级: ~/.claude/agents/
|
|
7
7
|
* - 项目级: .claude/agents/
|
|
8
|
+
*
|
|
9
|
+
* 支持从 GitHub 仓库扫描和安装代理
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const os = require('os');
|
|
15
|
+
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
13
16
|
|
|
14
17
|
// 代理目录路径
|
|
15
18
|
const USER_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
|
|
16
19
|
|
|
20
|
+
// 默认仓库源
|
|
21
|
+
const DEFAULT_REPOS = [];
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* 确保目录存在
|
|
19
25
|
*/
|
|
@@ -154,12 +160,97 @@ function scanAgentsDir(dir, basePath, scope) {
|
|
|
154
160
|
return agents;
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Agents 仓库扫描器
|
|
165
|
+
*/
|
|
166
|
+
class AgentsRepoScanner extends RepoScannerBase {
|
|
167
|
+
constructor() {
|
|
168
|
+
super({
|
|
169
|
+
type: 'agents',
|
|
170
|
+
installDir: USER_AGENTS_DIR,
|
|
171
|
+
markerFile: null, // 直接扫描 .md 文件
|
|
172
|
+
fileExtension: '.md',
|
|
173
|
+
defaultRepos: DEFAULT_REPOS
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 获取并解析单个代理文件
|
|
179
|
+
*/
|
|
180
|
+
async fetchAndParseItem(file, repo, baseDir) {
|
|
181
|
+
try {
|
|
182
|
+
// 计算相对路径
|
|
183
|
+
const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
|
|
184
|
+
const fileName = path.basename(file.path, '.md');
|
|
185
|
+
|
|
186
|
+
// 获取文件内容
|
|
187
|
+
const content = await this.fetchRawContent(repo, file.path);
|
|
188
|
+
const { frontmatter, body } = this.parseFrontmatter(content);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
key: `${repo.owner}/${repo.name}:${relativePath}`,
|
|
192
|
+
name: frontmatter.name || fileName,
|
|
193
|
+
fileName,
|
|
194
|
+
scope: 'remote',
|
|
195
|
+
path: relativePath,
|
|
196
|
+
repoPath: file.path,
|
|
197
|
+
description: frontmatter.description || '',
|
|
198
|
+
tools: frontmatter.tools || '',
|
|
199
|
+
model: frontmatter.model || '',
|
|
200
|
+
permissionMode: frontmatter.permissionMode || '',
|
|
201
|
+
skills: frontmatter.skills || '',
|
|
202
|
+
systemPrompt: body,
|
|
203
|
+
fullContent: content,
|
|
204
|
+
installed: this.isInstalled(fileName),
|
|
205
|
+
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
|
|
206
|
+
repoOwner: repo.owner,
|
|
207
|
+
repoName: repo.name,
|
|
208
|
+
repoBranch: repo.branch,
|
|
209
|
+
repoDirectory: repo.directory || ''
|
|
210
|
+
};
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.warn(`[AgentsRepoScanner] Parse agent ${file.path} error:`, err.message);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 检查代理是否已安装
|
|
219
|
+
*/
|
|
220
|
+
isInstalled(fileName) {
|
|
221
|
+
const fullPath = path.join(this.installDir, `${fileName}.md`);
|
|
222
|
+
return fs.existsSync(fullPath);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 获取去重 key
|
|
227
|
+
*/
|
|
228
|
+
getDedupeKey(item) {
|
|
229
|
+
return item.fileName.toLowerCase();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 安装代理
|
|
234
|
+
*/
|
|
235
|
+
async installAgent(item) {
|
|
236
|
+
const repo = {
|
|
237
|
+
owner: item.repoOwner,
|
|
238
|
+
name: item.repoName,
|
|
239
|
+
branch: item.repoBranch
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// 代理安装到根目录,使用文件名
|
|
243
|
+
return this.installFromRepo(item.repoPath, repo, `${item.fileName}.md`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
157
247
|
/**
|
|
158
248
|
* Agents 服务类
|
|
159
249
|
*/
|
|
160
250
|
class AgentsService {
|
|
161
251
|
constructor() {
|
|
162
252
|
this.userAgentsDir = USER_AGENTS_DIR;
|
|
253
|
+
this.repoScanner = new AgentsRepoScanner();
|
|
163
254
|
ensureDir(this.userAgentsDir);
|
|
164
255
|
}
|
|
165
256
|
|
|
@@ -192,6 +283,48 @@ class AgentsService {
|
|
|
192
283
|
};
|
|
193
284
|
}
|
|
194
285
|
|
|
286
|
+
/**
|
|
287
|
+
* 获取所有代理(包括远程仓库)
|
|
288
|
+
*/
|
|
289
|
+
async listAllAgents(projectPath = null, forceRefresh = false) {
|
|
290
|
+
// 获取本地代理
|
|
291
|
+
const { agents: localAgents, userCount, projectCount } = this.listAgents(projectPath);
|
|
292
|
+
|
|
293
|
+
// 获取远程代理
|
|
294
|
+
let remoteAgents = [];
|
|
295
|
+
try {
|
|
296
|
+
remoteAgents = await this.repoScanner.listRemoteItems(forceRefresh);
|
|
297
|
+
|
|
298
|
+
// 更新安装状态
|
|
299
|
+
for (const agent of remoteAgents) {
|
|
300
|
+
agent.installed = this.repoScanner.isInstalled(agent.fileName);
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn('[AgentsService] Failed to fetch remote agents:', err.message);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 合并列表(本地优先)
|
|
307
|
+
const allAgents = [...localAgents];
|
|
308
|
+
const localKeys = new Set(localAgents.map(a => a.fileName.toLowerCase()));
|
|
309
|
+
|
|
310
|
+
for (const remote of remoteAgents) {
|
|
311
|
+
if (!localKeys.has(remote.fileName.toLowerCase())) {
|
|
312
|
+
allAgents.push(remote);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 排序
|
|
317
|
+
allAgents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
agents: allAgents,
|
|
321
|
+
total: allAgents.length,
|
|
322
|
+
userCount,
|
|
323
|
+
projectCount,
|
|
324
|
+
remoteCount: remoteAgents.length
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
195
328
|
/**
|
|
196
329
|
* 获取单个代理详情
|
|
197
330
|
*/
|
|
@@ -347,8 +480,53 @@ class AgentsService {
|
|
|
347
480
|
models
|
|
348
481
|
};
|
|
349
482
|
}
|
|
483
|
+
|
|
484
|
+
// ==================== 仓库管理 ====================
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 获取仓库列表
|
|
488
|
+
*/
|
|
489
|
+
getRepos() {
|
|
490
|
+
return this.repoScanner.loadRepos();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* 添加仓库
|
|
495
|
+
*/
|
|
496
|
+
addRepo(repo) {
|
|
497
|
+
return this.repoScanner.addRepo(repo);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* 删除仓库
|
|
502
|
+
*/
|
|
503
|
+
removeRepo(owner, name, directory = '') {
|
|
504
|
+
return this.repoScanner.removeRepo(owner, name, directory);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 切换仓库启用状态
|
|
509
|
+
*/
|
|
510
|
+
toggleRepo(owner, name, directory = '', enabled) {
|
|
511
|
+
return this.repoScanner.toggleRepo(owner, name, directory, enabled);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 从远程仓库安装代理
|
|
516
|
+
*/
|
|
517
|
+
async installFromRemote(agent) {
|
|
518
|
+
return this.repoScanner.installAgent(agent);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 卸载代理
|
|
523
|
+
*/
|
|
524
|
+
uninstallAgent(fileName) {
|
|
525
|
+
return this.repoScanner.uninstall(`${fileName}.md`);
|
|
526
|
+
}
|
|
350
527
|
}
|
|
351
528
|
|
|
352
529
|
module.exports = {
|
|
353
|
-
AgentsService
|
|
530
|
+
AgentsService,
|
|
531
|
+
DEFAULT_REPOS
|
|
354
532
|
};
|
|
@@ -5,15 +5,28 @@
|
|
|
5
5
|
* 命令目录:
|
|
6
6
|
* - 用户级: ~/.claude/commands/
|
|
7
7
|
* - 项目级: .claude/commands/
|
|
8
|
+
*
|
|
9
|
+
* 支持从 GitHub 仓库扫描和安装命令
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const os = require('os');
|
|
15
|
+
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
16
|
+
const {
|
|
17
|
+
parseCommandContent,
|
|
18
|
+
detectCommandFormat,
|
|
19
|
+
convertCommandToCodex,
|
|
20
|
+
convertCommandToClaude,
|
|
21
|
+
parseFrontmatter
|
|
22
|
+
} = require('./format-converter');
|
|
13
23
|
|
|
14
24
|
// 命令目录路径
|
|
15
25
|
const USER_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
|
|
16
26
|
|
|
27
|
+
// 默认仓库源
|
|
28
|
+
const DEFAULT_REPOS = [];
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* 确保目录存在
|
|
19
32
|
*/
|
|
@@ -24,51 +37,9 @@ function ensureDir(dirPath) {
|
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
|
-
*
|
|
40
|
+
* 生成 frontmatter 字符串(用于命令创建/更新)
|
|
28
41
|
*/
|
|
29
|
-
function
|
|
30
|
-
const result = {
|
|
31
|
-
frontmatter: {},
|
|
32
|
-
body: content
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// 移除 BOM
|
|
36
|
-
content = content.trim().replace(/^\uFEFF/, '');
|
|
37
|
-
|
|
38
|
-
// 解析 YAML frontmatter
|
|
39
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
40
|
-
if (!match) {
|
|
41
|
-
return result;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const frontmatterText = match[1];
|
|
45
|
-
result.body = match[2].trim();
|
|
46
|
-
|
|
47
|
-
// 简单解析 YAML(支持基本字段)
|
|
48
|
-
const lines = frontmatterText.split('\n');
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
const colonIndex = line.indexOf(':');
|
|
51
|
-
if (colonIndex === -1) continue;
|
|
52
|
-
|
|
53
|
-
const key = line.slice(0, colonIndex).trim();
|
|
54
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
55
|
-
|
|
56
|
-
// 去除引号
|
|
57
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
58
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
59
|
-
value = value.slice(1, -1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
result.frontmatter[key] = value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 生成 frontmatter 字符串
|
|
70
|
-
*/
|
|
71
|
-
function generateFrontmatter(data) {
|
|
42
|
+
function generateCommandFrontmatter(data) {
|
|
72
43
|
const lines = ['---'];
|
|
73
44
|
|
|
74
45
|
if (data.description) {
|
|
@@ -80,6 +51,15 @@ function generateFrontmatter(data) {
|
|
|
80
51
|
if (data['argument-hint']) {
|
|
81
52
|
lines.push(`argument-hint: ${data['argument-hint']}`);
|
|
82
53
|
}
|
|
54
|
+
if (data.model) {
|
|
55
|
+
lines.push(`model: ${data.model}`);
|
|
56
|
+
}
|
|
57
|
+
if (data.context) {
|
|
58
|
+
lines.push(`context: ${data.context}`);
|
|
59
|
+
}
|
|
60
|
+
if (data.agent) {
|
|
61
|
+
lines.push(`agent: ${data.agent}`);
|
|
62
|
+
}
|
|
83
63
|
|
|
84
64
|
lines.push('---');
|
|
85
65
|
return lines.join('\n');
|
|
@@ -141,12 +121,96 @@ function scanCommandsDir(dir, basePath, scope) {
|
|
|
141
121
|
return commands;
|
|
142
122
|
}
|
|
143
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Commands 仓库扫描器
|
|
126
|
+
*/
|
|
127
|
+
class CommandsRepoScanner extends RepoScannerBase {
|
|
128
|
+
constructor() {
|
|
129
|
+
super({
|
|
130
|
+
type: 'commands',
|
|
131
|
+
installDir: USER_COMMANDS_DIR,
|
|
132
|
+
markerFile: null, // 直接扫描 .md 文件
|
|
133
|
+
fileExtension: '.md',
|
|
134
|
+
defaultRepos: DEFAULT_REPOS
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 获取并解析单个命令文件
|
|
140
|
+
*/
|
|
141
|
+
async fetchAndParseItem(file, repo, baseDir) {
|
|
142
|
+
try {
|
|
143
|
+
// 计算相对路径
|
|
144
|
+
const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
|
|
145
|
+
const fileName = path.basename(file.path, '.md');
|
|
146
|
+
const namespace = path.dirname(relativePath);
|
|
147
|
+
|
|
148
|
+
// 获取文件内容
|
|
149
|
+
const content = await this.fetchRawContent(repo, file.path);
|
|
150
|
+
const { frontmatter, body } = this.parseFrontmatter(content);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
key: `${repo.owner}/${repo.name}:${relativePath}`,
|
|
154
|
+
name: fileName,
|
|
155
|
+
namespace: namespace === '.' ? null : namespace,
|
|
156
|
+
scope: 'remote',
|
|
157
|
+
path: relativePath,
|
|
158
|
+
repoPath: file.path,
|
|
159
|
+
description: frontmatter.description || '',
|
|
160
|
+
allowedTools: frontmatter['allowed-tools'] || '',
|
|
161
|
+
argumentHint: frontmatter['argument-hint'] || '',
|
|
162
|
+
body,
|
|
163
|
+
fullContent: content,
|
|
164
|
+
installed: this.isInstalled(relativePath),
|
|
165
|
+
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
|
|
166
|
+
repoOwner: repo.owner,
|
|
167
|
+
repoName: repo.name,
|
|
168
|
+
repoBranch: repo.branch,
|
|
169
|
+
repoDirectory: repo.directory || ''
|
|
170
|
+
};
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn(`[CommandsRepoScanner] Parse command ${file.path} error:`, err.message);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 检查命令是否已安装
|
|
179
|
+
*/
|
|
180
|
+
isInstalled(relativePath) {
|
|
181
|
+
const fullPath = path.join(this.installDir, relativePath);
|
|
182
|
+
return fs.existsSync(fullPath);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 获取去重 key
|
|
187
|
+
*/
|
|
188
|
+
getDedupeKey(item) {
|
|
189
|
+
// 使用 namespace/name 作为去重 key
|
|
190
|
+
return item.namespace ? `${item.namespace}/${item.name}`.toLowerCase() : item.name.toLowerCase();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 安装命令
|
|
195
|
+
*/
|
|
196
|
+
async installCommand(item) {
|
|
197
|
+
const repo = {
|
|
198
|
+
owner: item.repoOwner,
|
|
199
|
+
name: item.repoName,
|
|
200
|
+
branch: item.repoBranch
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return this.installFromRepo(item.repoPath, repo, item.path);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
144
207
|
/**
|
|
145
208
|
* Commands 服务类
|
|
146
209
|
*/
|
|
147
210
|
class CommandsService {
|
|
148
211
|
constructor() {
|
|
149
212
|
this.userCommandsDir = USER_COMMANDS_DIR;
|
|
213
|
+
this.repoScanner = new CommandsRepoScanner();
|
|
150
214
|
ensureDir(this.userCommandsDir);
|
|
151
215
|
}
|
|
152
216
|
|
|
@@ -179,6 +243,52 @@ class CommandsService {
|
|
|
179
243
|
};
|
|
180
244
|
}
|
|
181
245
|
|
|
246
|
+
/**
|
|
247
|
+
* 获取所有命令(包括远程仓库)
|
|
248
|
+
* @param {boolean} forceRefresh - 强制刷新远程缓存
|
|
249
|
+
*/
|
|
250
|
+
async listAllCommands(projectPath = null, forceRefresh = false) {
|
|
251
|
+
// 获取本地命令
|
|
252
|
+
const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath);
|
|
253
|
+
|
|
254
|
+
// 获取远程命令
|
|
255
|
+
let remoteCommands = [];
|
|
256
|
+
try {
|
|
257
|
+
remoteCommands = await this.repoScanner.listRemoteItems(forceRefresh);
|
|
258
|
+
|
|
259
|
+
// 更新安装状态
|
|
260
|
+
for (const cmd of remoteCommands) {
|
|
261
|
+
cmd.installed = this.repoScanner.isInstalled(cmd.path);
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.warn('[CommandsService] Failed to fetch remote commands:', err.message);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 合并列表(本地优先)
|
|
268
|
+
const allCommands = [...localCommands];
|
|
269
|
+
const localKeys = new Set(localCommands.map(c =>
|
|
270
|
+
c.namespace ? `${c.namespace}/${c.name}`.toLowerCase() : c.name.toLowerCase()
|
|
271
|
+
));
|
|
272
|
+
|
|
273
|
+
for (const remote of remoteCommands) {
|
|
274
|
+
const key = remote.namespace ? `${remote.namespace}/${remote.name}`.toLowerCase() : remote.name.toLowerCase();
|
|
275
|
+
if (!localKeys.has(key)) {
|
|
276
|
+
allCommands.push(remote);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 排序
|
|
281
|
+
allCommands.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
commands: allCommands,
|
|
285
|
+
total: allCommands.length,
|
|
286
|
+
userCount,
|
|
287
|
+
projectCount,
|
|
288
|
+
remoteCount: remoteCommands.length
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
182
292
|
/**
|
|
183
293
|
* 获取单个命令详情
|
|
184
294
|
*/
|
|
@@ -250,7 +360,7 @@ class CommandsService {
|
|
|
250
360
|
|
|
251
361
|
let content = '';
|
|
252
362
|
if (Object.keys(frontmatterData).length > 0) {
|
|
253
|
-
content =
|
|
363
|
+
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
254
364
|
}
|
|
255
365
|
content += body || '';
|
|
256
366
|
|
|
@@ -285,7 +395,7 @@ class CommandsService {
|
|
|
285
395
|
|
|
286
396
|
let content = '';
|
|
287
397
|
if (Object.keys(frontmatterData).length > 0) {
|
|
288
|
-
content =
|
|
398
|
+
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
289
399
|
}
|
|
290
400
|
content += body || '';
|
|
291
401
|
|
|
@@ -353,8 +463,82 @@ class CommandsService {
|
|
|
353
463
|
namespaces
|
|
354
464
|
};
|
|
355
465
|
}
|
|
466
|
+
|
|
467
|
+
// ==================== 仓库管理 ====================
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 获取仓库列表
|
|
471
|
+
*/
|
|
472
|
+
getRepos() {
|
|
473
|
+
return this.repoScanner.loadRepos();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 添加仓库
|
|
478
|
+
*/
|
|
479
|
+
addRepo(repo) {
|
|
480
|
+
return this.repoScanner.addRepo(repo);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 删除仓库
|
|
485
|
+
*/
|
|
486
|
+
removeRepo(owner, name, directory = '') {
|
|
487
|
+
return this.repoScanner.removeRepo(owner, name, directory);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 切换仓库启用状态
|
|
492
|
+
*/
|
|
493
|
+
toggleRepo(owner, name, directory = '', enabled) {
|
|
494
|
+
return this.repoScanner.toggleRepo(owner, name, directory, enabled);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 从远程仓库安装命令
|
|
499
|
+
*/
|
|
500
|
+
async installFromRemote(command) {
|
|
501
|
+
return this.repoScanner.installCommand(command);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 卸载命令
|
|
506
|
+
*/
|
|
507
|
+
uninstallCommand(relativePath) {
|
|
508
|
+
return this.repoScanner.uninstall(relativePath);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ==================== 格式转换 ====================
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* 转换命令格式
|
|
515
|
+
* @param {string} content - 命令内容
|
|
516
|
+
* @param {string} targetFormat - 目标格式 ('claude' | 'codex')
|
|
517
|
+
*/
|
|
518
|
+
convertCommandFormat(content, targetFormat) {
|
|
519
|
+
const sourceFormat = detectCommandFormat(content);
|
|
520
|
+
|
|
521
|
+
if (sourceFormat === targetFormat) {
|
|
522
|
+
return { content, warnings: [], format: targetFormat };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (targetFormat === 'codex') {
|
|
526
|
+
return convertCommandToCodex(content);
|
|
527
|
+
} else {
|
|
528
|
+
return convertCommandToClaude(content);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 检测命令格式
|
|
534
|
+
* @param {string} content - 命令内容
|
|
535
|
+
*/
|
|
536
|
+
detectFormat(content) {
|
|
537
|
+
return detectCommandFormat(content);
|
|
538
|
+
}
|
|
356
539
|
}
|
|
357
540
|
|
|
358
541
|
module.exports = {
|
|
359
|
-
CommandsService
|
|
542
|
+
CommandsService,
|
|
543
|
+
DEFAULT_REPOS
|
|
360
544
|
};
|