@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
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
* 规则目录:
|
|
6
6
|
* - 用户级: ~/.claude/rules/
|
|
7
7
|
* - 项目级: .claude/rules/
|
|
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_RULES_DIR = path.join(os.homedir(), '.claude', 'rules');
|
|
16
19
|
|
|
20
|
+
// 默认仓库源
|
|
21
|
+
const DEFAULT_REPOS = [];
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* 确保目录存在
|
|
19
25
|
*/
|
|
@@ -134,12 +140,94 @@ function scanRulesDir(dir, basePath, scope) {
|
|
|
134
140
|
return rules;
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Rules 仓库扫描器
|
|
145
|
+
*/
|
|
146
|
+
class RulesRepoScanner extends RepoScannerBase {
|
|
147
|
+
constructor() {
|
|
148
|
+
super({
|
|
149
|
+
type: 'rules',
|
|
150
|
+
installDir: USER_RULES_DIR,
|
|
151
|
+
markerFile: null, // 直接扫描 .md 文件
|
|
152
|
+
fileExtension: '.md',
|
|
153
|
+
defaultRepos: DEFAULT_REPOS
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 获取并解析单个规则文件
|
|
159
|
+
*/
|
|
160
|
+
async fetchAndParseItem(file, repo, baseDir) {
|
|
161
|
+
try {
|
|
162
|
+
// 计算相对路径
|
|
163
|
+
const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
|
|
164
|
+
const fileName = path.basename(file.path, '.md');
|
|
165
|
+
const directory = path.dirname(relativePath);
|
|
166
|
+
|
|
167
|
+
// 获取文件内容
|
|
168
|
+
const content = await this.fetchRawContent(repo, file.path);
|
|
169
|
+
const { frontmatter, body } = this.parseFrontmatter(content);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
key: `${repo.owner}/${repo.name}:${relativePath}`,
|
|
173
|
+
name: fileName,
|
|
174
|
+
fileName,
|
|
175
|
+
directory: directory === '.' ? null : directory,
|
|
176
|
+
scope: 'remote',
|
|
177
|
+
path: relativePath,
|
|
178
|
+
repoPath: file.path,
|
|
179
|
+
paths: frontmatter.paths || '',
|
|
180
|
+
body,
|
|
181
|
+
fullContent: content,
|
|
182
|
+
installed: this.isInstalled(relativePath),
|
|
183
|
+
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
|
|
184
|
+
repoOwner: repo.owner,
|
|
185
|
+
repoName: repo.name,
|
|
186
|
+
repoBranch: repo.branch,
|
|
187
|
+
repoDirectory: repo.directory || ''
|
|
188
|
+
};
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.warn(`[RulesRepoScanner] Parse rule ${file.path} error:`, err.message);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 检查规则是否已安装
|
|
197
|
+
*/
|
|
198
|
+
isInstalled(relativePath) {
|
|
199
|
+
const fullPath = path.join(this.installDir, relativePath);
|
|
200
|
+
return fs.existsSync(fullPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 获取去重 key
|
|
205
|
+
*/
|
|
206
|
+
getDedupeKey(item) {
|
|
207
|
+
return item.directory ? `${item.directory}/${item.fileName}`.toLowerCase() : item.fileName.toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 安装规则
|
|
212
|
+
*/
|
|
213
|
+
async installRule(item) {
|
|
214
|
+
const repo = {
|
|
215
|
+
owner: item.repoOwner,
|
|
216
|
+
name: item.repoName,
|
|
217
|
+
branch: item.repoBranch
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return this.installFromRepo(item.repoPath, repo, item.path);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
137
224
|
/**
|
|
138
225
|
* Rules 服务类
|
|
139
226
|
*/
|
|
140
227
|
class RulesService {
|
|
141
228
|
constructor() {
|
|
142
229
|
this.userRulesDir = USER_RULES_DIR;
|
|
230
|
+
this.repoScanner = new RulesRepoScanner();
|
|
143
231
|
ensureDir(this.userRulesDir);
|
|
144
232
|
}
|
|
145
233
|
|
|
@@ -172,6 +260,51 @@ class RulesService {
|
|
|
172
260
|
};
|
|
173
261
|
}
|
|
174
262
|
|
|
263
|
+
/**
|
|
264
|
+
* 获取所有规则(包括远程仓库)
|
|
265
|
+
*/
|
|
266
|
+
async listAllRules(projectPath = null, forceRefresh = false) {
|
|
267
|
+
// 获取本地规则
|
|
268
|
+
const { rules: localRules, userCount, projectCount } = this.listRules(projectPath);
|
|
269
|
+
|
|
270
|
+
// 获取远程规则
|
|
271
|
+
let remoteRules = [];
|
|
272
|
+
try {
|
|
273
|
+
remoteRules = await this.repoScanner.listRemoteItems(forceRefresh);
|
|
274
|
+
|
|
275
|
+
// 更新安装状态
|
|
276
|
+
for (const rule of remoteRules) {
|
|
277
|
+
rule.installed = this.repoScanner.isInstalled(rule.path);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.warn('[RulesService] Failed to fetch remote rules:', err.message);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 合并列表(本地优先)
|
|
284
|
+
const allRules = [...localRules];
|
|
285
|
+
const localKeys = new Set(localRules.map(r =>
|
|
286
|
+
r.directory ? `${r.directory}/${r.fileName}`.toLowerCase() : r.fileName.toLowerCase()
|
|
287
|
+
));
|
|
288
|
+
|
|
289
|
+
for (const remote of remoteRules) {
|
|
290
|
+
const key = remote.directory ? `${remote.directory}/${remote.fileName}`.toLowerCase() : remote.fileName.toLowerCase();
|
|
291
|
+
if (!localKeys.has(key)) {
|
|
292
|
+
allRules.push(remote);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 排序
|
|
297
|
+
allRules.sort((a, b) => a.path.toLowerCase().localeCompare(b.path.toLowerCase()));
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
rules: allRules,
|
|
301
|
+
total: allRules.length,
|
|
302
|
+
userCount,
|
|
303
|
+
projectCount,
|
|
304
|
+
remoteCount: remoteRules.length
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
175
308
|
/**
|
|
176
309
|
* 获取单个规则详情
|
|
177
310
|
*/
|
|
@@ -394,8 +527,53 @@ class RulesService {
|
|
|
394
527
|
directories
|
|
395
528
|
};
|
|
396
529
|
}
|
|
530
|
+
|
|
531
|
+
// ==================== 仓库管理 ====================
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* 获取仓库列表
|
|
535
|
+
*/
|
|
536
|
+
getRepos() {
|
|
537
|
+
return this.repoScanner.loadRepos();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 添加仓库
|
|
542
|
+
*/
|
|
543
|
+
addRepo(repo) {
|
|
544
|
+
return this.repoScanner.addRepo(repo);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* 删除仓库
|
|
549
|
+
*/
|
|
550
|
+
removeRepo(owner, name, directory = '') {
|
|
551
|
+
return this.repoScanner.removeRepo(owner, name, directory);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 切换仓库启用状态
|
|
556
|
+
*/
|
|
557
|
+
toggleRepo(owner, name, directory = '', enabled) {
|
|
558
|
+
return this.repoScanner.toggleRepo(owner, name, directory, enabled);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* 从远程仓库安装规则
|
|
563
|
+
*/
|
|
564
|
+
async installFromRemote(rule) {
|
|
565
|
+
return this.repoScanner.installRule(rule);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 卸载规则
|
|
570
|
+
*/
|
|
571
|
+
uninstallRule(relativePath) {
|
|
572
|
+
return this.repoScanner.uninstall(relativePath);
|
|
573
|
+
}
|
|
397
574
|
}
|
|
398
575
|
|
|
399
576
|
module.exports = {
|
|
400
|
-
RulesService
|
|
577
|
+
RulesService,
|
|
578
|
+
DEFAULT_REPOS
|
|
401
579
|
};
|
|
@@ -13,10 +13,17 @@ const http = require('http');
|
|
|
13
13
|
const { createWriteStream } = require('fs');
|
|
14
14
|
const { pipeline } = require('stream/promises');
|
|
15
15
|
const AdmZip = require('adm-zip');
|
|
16
|
+
const {
|
|
17
|
+
parseSkillContent,
|
|
18
|
+
detectSkillFormat,
|
|
19
|
+
convertSkillToCodex,
|
|
20
|
+
convertSkillToClaude
|
|
21
|
+
} = require('./format-converter');
|
|
16
22
|
|
|
17
23
|
// 默认仓库源 - 只预设官方仓库,其他由用户手动添加
|
|
24
|
+
// directory 字段支持指定仓库子目录
|
|
18
25
|
const DEFAULT_REPOS = [
|
|
19
|
-
{ owner: 'anthropics', name: 'skills', branch: 'main', enabled: true }
|
|
26
|
+
{ owner: 'anthropics', name: 'skills', branch: 'main', directory: '', enabled: true }
|
|
20
27
|
];
|
|
21
28
|
|
|
22
29
|
// 缓存有效期(5分钟)
|
|
@@ -70,10 +77,21 @@ class SkillService {
|
|
|
70
77
|
|
|
71
78
|
/**
|
|
72
79
|
* 添加仓库
|
|
80
|
+
* @param {Object} repo - 仓库配置
|
|
81
|
+
* @param {string} repo.owner - 仓库所有者
|
|
82
|
+
* @param {string} repo.name - 仓库名称
|
|
83
|
+
* @param {string} repo.branch - 分支名称
|
|
84
|
+
* @param {string} [repo.directory] - 扫描的子目录路径(可选)
|
|
85
|
+
* @param {boolean} repo.enabled - 是否启用
|
|
73
86
|
*/
|
|
74
87
|
addRepo(repo) {
|
|
75
88
|
const repos = this.loadRepos();
|
|
76
|
-
|
|
89
|
+
// 使用 owner/name/directory 作为唯一标识
|
|
90
|
+
const existingIndex = repos.findIndex(r =>
|
|
91
|
+
r.owner === repo.owner &&
|
|
92
|
+
r.name === repo.name &&
|
|
93
|
+
(r.directory || '') === (repo.directory || '')
|
|
94
|
+
);
|
|
77
95
|
|
|
78
96
|
if (existingIndex >= 0) {
|
|
79
97
|
repos[existingIndex] = repo;
|
|
@@ -82,28 +100,52 @@ class SkillService {
|
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
this.saveRepos(repos);
|
|
103
|
+
// 清除缓存
|
|
104
|
+
this.skillsCache = null;
|
|
105
|
+
this.cacheTime = 0;
|
|
85
106
|
return repos;
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
/**
|
|
89
110
|
* 删除仓库
|
|
111
|
+
* @param {string} owner - 仓库所有者
|
|
112
|
+
* @param {string} name - 仓库名称
|
|
113
|
+
* @param {string} [directory=''] - 子目录路径
|
|
90
114
|
*/
|
|
91
|
-
removeRepo(owner, name) {
|
|
115
|
+
removeRepo(owner, name, directory = '') {
|
|
92
116
|
const repos = this.loadRepos();
|
|
93
|
-
const filtered = repos.filter(r => !(
|
|
117
|
+
const filtered = repos.filter(r => !(
|
|
118
|
+
r.owner === owner &&
|
|
119
|
+
r.name === name &&
|
|
120
|
+
(r.directory || '') === directory
|
|
121
|
+
));
|
|
94
122
|
this.saveRepos(filtered);
|
|
123
|
+
// 清除缓存
|
|
124
|
+
this.skillsCache = null;
|
|
125
|
+
this.cacheTime = 0;
|
|
95
126
|
return filtered;
|
|
96
127
|
}
|
|
97
128
|
|
|
98
129
|
/**
|
|
99
130
|
* 切换仓库启用状态
|
|
131
|
+
* @param {string} owner - 仓库所有者
|
|
132
|
+
* @param {string} name - 仓库名称
|
|
133
|
+
* @param {string} [directory=''] - 子目录路径
|
|
134
|
+
* @param {boolean} enabled - 是否启用
|
|
100
135
|
*/
|
|
101
|
-
toggleRepo(owner, name, enabled) {
|
|
136
|
+
toggleRepo(owner, name, directory = '', enabled) {
|
|
102
137
|
const repos = this.loadRepos();
|
|
103
|
-
const repo = repos.find(r =>
|
|
138
|
+
const repo = repos.find(r =>
|
|
139
|
+
r.owner === owner &&
|
|
140
|
+
r.name === name &&
|
|
141
|
+
(r.directory || '') === directory
|
|
142
|
+
);
|
|
104
143
|
if (repo) {
|
|
105
144
|
repo.enabled = enabled;
|
|
106
145
|
this.saveRepos(repos);
|
|
146
|
+
// 清除缓存
|
|
147
|
+
this.skillsCache = null;
|
|
148
|
+
this.cacheTime = 0;
|
|
107
149
|
}
|
|
108
150
|
return repos;
|
|
109
151
|
}
|
|
@@ -229,6 +271,7 @@ class SkillService {
|
|
|
229
271
|
|
|
230
272
|
/**
|
|
231
273
|
* 从 GitHub 仓库获取技能列表(使用 Tree API 一次性获取)
|
|
274
|
+
* 支持指定子目录扫描
|
|
232
275
|
*/
|
|
233
276
|
async fetchRepoSkills(repo) {
|
|
234
277
|
const skills = [];
|
|
@@ -243,10 +286,21 @@ class SkillService {
|
|
|
243
286
|
return skills;
|
|
244
287
|
}
|
|
245
288
|
|
|
246
|
-
//
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
289
|
+
// 获取基础目录(如果配置了 directory)
|
|
290
|
+
const baseDir = repo.directory || '';
|
|
291
|
+
const baseDirPrefix = baseDir ? `${baseDir}/` : '';
|
|
292
|
+
|
|
293
|
+
// 找到所有 SKILL.md 文件(如果配置了子目录,只扫描该目录下的)
|
|
294
|
+
const skillFiles = tree.tree.filter(item => {
|
|
295
|
+
if (item.type !== 'blob' || !item.path.endsWith('/SKILL.md')) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
// 如果配置了子目录,只返回该子目录下的文件
|
|
299
|
+
if (baseDir && !item.path.startsWith(baseDirPrefix)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
250
304
|
|
|
251
305
|
// 并行获取所有 SKILL.md 的内容(限制并发数)
|
|
252
306
|
const batchSize = 5;
|
|
@@ -254,7 +308,7 @@ class SkillService {
|
|
|
254
308
|
for (let i = 0; i < skillFiles.length; i += batchSize) {
|
|
255
309
|
const batch = skillFiles.slice(i, i + batchSize);
|
|
256
310
|
const results = await Promise.allSettled(
|
|
257
|
-
batch.map(file => this.fetchAndParseSkill(file, repo))
|
|
311
|
+
batch.map(file => this.fetchAndParseSkill(file, repo, baseDir))
|
|
258
312
|
);
|
|
259
313
|
|
|
260
314
|
for (const result of results) {
|
|
@@ -273,26 +327,34 @@ class SkillService {
|
|
|
273
327
|
|
|
274
328
|
/**
|
|
275
329
|
* 获取并解析单个 SKILL.md
|
|
330
|
+
* @param {Object} file - GitHub tree 文件对象
|
|
331
|
+
* @param {Object} repo - 仓库配置
|
|
332
|
+
* @param {string} baseDir - 基础目录(用于计算相对路径)
|
|
276
333
|
*/
|
|
277
|
-
async fetchAndParseSkill(file, repo) {
|
|
334
|
+
async fetchAndParseSkill(file, repo, baseDir = '') {
|
|
278
335
|
try {
|
|
279
336
|
// 从路径提取目录名 (e.g., "algorithmic-art/SKILL.md" -> "algorithmic-art")
|
|
280
|
-
const
|
|
337
|
+
const fullDirectory = file.path.replace(/\/SKILL\.md$/, '');
|
|
338
|
+
|
|
339
|
+
// 计算相对于 baseDir 的目录名(用于显示和安装)
|
|
340
|
+
const directory = baseDir ? fullDirectory.slice(baseDir.length + 1) : fullDirectory;
|
|
281
341
|
|
|
282
342
|
// 使用 raw.githubusercontent.com 获取文件内容(不消耗 API 限额)
|
|
283
343
|
const content = await this.fetchBlobContent(file.sha, repo, file.path);
|
|
284
344
|
const metadata = this.parseSkillMd(content);
|
|
285
345
|
|
|
286
346
|
return {
|
|
287
|
-
key: `${repo.owner}/${repo.name}:${
|
|
347
|
+
key: `${repo.owner}/${repo.name}:${fullDirectory}`,
|
|
288
348
|
name: metadata.name || directory.split('/').pop(),
|
|
289
349
|
description: metadata.description || '',
|
|
290
|
-
directory,
|
|
350
|
+
directory, // 相对目录(用于安装)
|
|
351
|
+
fullDirectory, // 完整目录(用于从仓库下载)
|
|
291
352
|
installed: this.isInstalled(directory),
|
|
292
|
-
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch}/${
|
|
353
|
+
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch}/${fullDirectory}`,
|
|
293
354
|
repoOwner: repo.owner,
|
|
294
355
|
repoName: repo.name,
|
|
295
356
|
repoBranch: repo.branch,
|
|
357
|
+
repoDirectory: repo.directory || '', // 仓库配置的子目录
|
|
296
358
|
license: metadata.license
|
|
297
359
|
};
|
|
298
360
|
} catch (err) {
|
|
@@ -545,55 +607,40 @@ class SkillService {
|
|
|
545
607
|
}
|
|
546
608
|
|
|
547
609
|
/**
|
|
548
|
-
* 解析 SKILL.md
|
|
610
|
+
* 解析 SKILL.md 文件(支持 Claude Code 和 Codex CLI 格式)
|
|
549
611
|
*/
|
|
550
612
|
parseSkillMd(content) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
613
|
+
// 使用格式转换器统一解析
|
|
614
|
+
const parsed = parseSkillContent(content);
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
name: parsed.name || null,
|
|
618
|
+
description: parsed.description || null,
|
|
619
|
+
license: parsed.license || null,
|
|
620
|
+
allowedTools: parsed.allowedTools ? [parsed.allowedTools] : [],
|
|
621
|
+
metadata: parsed.metadata || {},
|
|
622
|
+
shortDescription: parsed.shortDescription || null,
|
|
623
|
+
format: parsed.format
|
|
557
624
|
};
|
|
625
|
+
}
|
|
558
626
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const frontmatter = match[1];
|
|
567
|
-
|
|
568
|
-
// 简单解析 YAML
|
|
569
|
-
const lines = frontmatter.split('\n');
|
|
570
|
-
for (const line of lines) {
|
|
571
|
-
const colonIndex = line.indexOf(':');
|
|
572
|
-
if (colonIndex === -1) continue;
|
|
573
|
-
|
|
574
|
-
const key = line.slice(0, colonIndex).trim();
|
|
575
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
576
|
-
|
|
577
|
-
// 去除引号
|
|
578
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
579
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
580
|
-
value = value.slice(1, -1);
|
|
581
|
-
}
|
|
627
|
+
/**
|
|
628
|
+
* 转换技能格式
|
|
629
|
+
* @param {string} content - 技能内容
|
|
630
|
+
* @param {string} targetFormat - 目标格式 ('claude' | 'codex')
|
|
631
|
+
*/
|
|
632
|
+
convertSkillFormat(content, targetFormat) {
|
|
633
|
+
const sourceFormat = detectSkillFormat(content);
|
|
582
634
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
result.name = value;
|
|
586
|
-
break;
|
|
587
|
-
case 'description':
|
|
588
|
-
result.description = value;
|
|
589
|
-
break;
|
|
590
|
-
case 'license':
|
|
591
|
-
result.license = value;
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
635
|
+
if (sourceFormat === targetFormat) {
|
|
636
|
+
return { content, warnings: [], format: targetFormat };
|
|
594
637
|
}
|
|
595
638
|
|
|
596
|
-
|
|
639
|
+
if (targetFormat === 'codex') {
|
|
640
|
+
return convertSkillToCodex(content);
|
|
641
|
+
} else {
|
|
642
|
+
return convertSkillToClaude(content);
|
|
643
|
+
}
|
|
597
644
|
}
|
|
598
645
|
|
|
599
646
|
/**
|
|
@@ -702,8 +749,11 @@ class SkillService {
|
|
|
702
749
|
|
|
703
750
|
/**
|
|
704
751
|
* 安装技能
|
|
752
|
+
* @param {string} directory - 本地安装目录(相对于 installDir)
|
|
753
|
+
* @param {Object} repo - 仓库配置
|
|
754
|
+
* @param {string} [fullDirectory] - 仓库中的完整路径(可选,默认与 directory 相同)
|
|
705
755
|
*/
|
|
706
|
-
async installSkill(directory, repo) {
|
|
756
|
+
async installSkill(directory, repo, fullDirectory = null) {
|
|
707
757
|
const dest = path.join(this.installDir, directory);
|
|
708
758
|
|
|
709
759
|
// 已安装则跳过
|
|
@@ -711,6 +761,9 @@ class SkillService {
|
|
|
711
761
|
return { success: true, message: 'Already installed' };
|
|
712
762
|
}
|
|
713
763
|
|
|
764
|
+
// 使用 fullDirectory(仓库中的完整路径)或 directory(向后兼容)
|
|
765
|
+
const sourcePath = fullDirectory || directory;
|
|
766
|
+
|
|
714
767
|
// 下载仓库 ZIP
|
|
715
768
|
const zipUrl = `https://github.com/${repo.owner}/${repo.name}/archive/refs/heads/${repo.branch}.zip`;
|
|
716
769
|
const tempDir = path.join(os.tmpdir(), `skill-${Date.now()}`);
|
|
@@ -736,10 +789,10 @@ class SkillService {
|
|
|
736
789
|
}
|
|
737
790
|
|
|
738
791
|
const repoDir = path.join(tempDir, extractedDirs[0]);
|
|
739
|
-
const sourceDir = path.join(repoDir,
|
|
792
|
+
const sourceDir = path.join(repoDir, sourcePath);
|
|
740
793
|
|
|
741
794
|
if (!fs.existsSync(sourceDir)) {
|
|
742
|
-
throw new Error(`Skill directory not found: ${
|
|
795
|
+
throw new Error(`Skill directory not found: ${sourcePath}`);
|
|
743
796
|
}
|
|
744
797
|
|
|
745
798
|
// 复制到安装目录
|
|
@@ -151,7 +151,7 @@ function getGitWorktrees(repoPath) {
|
|
|
151
151
|
* @param {Array} options.projects - 项目列表 [{sourcePath, name, createWorktree, branch}]
|
|
152
152
|
*/
|
|
153
153
|
function createWorkspace(options) {
|
|
154
|
-
const { name, description = '', baseDir, projects = [], configTemplateId } = options;
|
|
154
|
+
const { name, description = '', baseDir, projects = [], configTemplateId, permissionTemplate } = options;
|
|
155
155
|
|
|
156
156
|
if (!name || name.trim() === '') {
|
|
157
157
|
throw new Error('工作区名称不能为空');
|
|
@@ -299,6 +299,114 @@ function createWorkspace(options) {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// 应用权限模板(如果指定且不是 'none')
|
|
303
|
+
let permissionInfo = null;
|
|
304
|
+
if (permissionTemplate && permissionTemplate !== 'none') {
|
|
305
|
+
try {
|
|
306
|
+
const permissionTemplates = {
|
|
307
|
+
safe: {
|
|
308
|
+
allow: [
|
|
309
|
+
'Bash(cat:*)',
|
|
310
|
+
'Bash(ls:*)',
|
|
311
|
+
'Bash(pwd)',
|
|
312
|
+
'Bash(echo:*)',
|
|
313
|
+
'Bash(head:*)',
|
|
314
|
+
'Bash(tail:*)',
|
|
315
|
+
'Bash(grep:*)',
|
|
316
|
+
'Read(*)'
|
|
317
|
+
],
|
|
318
|
+
deny: [
|
|
319
|
+
'Bash(rm:*)',
|
|
320
|
+
'Bash(sudo:*)',
|
|
321
|
+
'Bash(git push:*)',
|
|
322
|
+
'Bash(git reset --hard:*)',
|
|
323
|
+
'Bash(chmod:*)',
|
|
324
|
+
'Bash(chown:*)',
|
|
325
|
+
'Edit(*)'
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
balanced: {
|
|
329
|
+
allow: [
|
|
330
|
+
'Bash(cat:*)',
|
|
331
|
+
'Bash(ls:*)',
|
|
332
|
+
'Bash(pwd)',
|
|
333
|
+
'Bash(echo:*)',
|
|
334
|
+
'Bash(head:*)',
|
|
335
|
+
'Bash(tail:*)',
|
|
336
|
+
'Bash(grep:*)',
|
|
337
|
+
'Bash(find:*)',
|
|
338
|
+
'Bash(git status)',
|
|
339
|
+
'Bash(git diff:*)',
|
|
340
|
+
'Bash(git log:*)',
|
|
341
|
+
'Bash(npm run:*)',
|
|
342
|
+
'Bash(pnpm:*)',
|
|
343
|
+
'Bash(yarn:*)',
|
|
344
|
+
'Read(*)',
|
|
345
|
+
'Edit(*)'
|
|
346
|
+
],
|
|
347
|
+
deny: [
|
|
348
|
+
'Bash(rm -rf:*)',
|
|
349
|
+
'Bash(sudo:*)',
|
|
350
|
+
'Bash(git push --force:*)',
|
|
351
|
+
'Bash(git reset --hard:*)'
|
|
352
|
+
]
|
|
353
|
+
},
|
|
354
|
+
permissive: {
|
|
355
|
+
allow: [
|
|
356
|
+
'Bash(*)',
|
|
357
|
+
'Read(*)',
|
|
358
|
+
'Edit(*)'
|
|
359
|
+
],
|
|
360
|
+
deny: [
|
|
361
|
+
'Bash(rm -rf /*)',
|
|
362
|
+
'Bash(sudo rm -rf:*)'
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const permSettings = permissionTemplates[permissionTemplate];
|
|
368
|
+
if (permSettings) {
|
|
369
|
+
// 为工作区中的每个项目应用权限设置
|
|
370
|
+
for (const proj of workspaceProjects) {
|
|
371
|
+
const projSettingsDir = path.join(proj.targetPath, '.claude');
|
|
372
|
+
const projSettingsFile = path.join(projSettingsDir, 'settings.json');
|
|
373
|
+
|
|
374
|
+
// 确保 .claude 目录存在
|
|
375
|
+
if (!fs.existsSync(projSettingsDir)) {
|
|
376
|
+
fs.mkdirSync(projSettingsDir, { recursive: true });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 读取现有设置或创建新的
|
|
380
|
+
let settings = {};
|
|
381
|
+
if (fs.existsSync(projSettingsFile)) {
|
|
382
|
+
try {
|
|
383
|
+
settings = JSON.parse(fs.readFileSync(projSettingsFile, 'utf8'));
|
|
384
|
+
} catch (e) {
|
|
385
|
+
settings = {};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 更新权限设置
|
|
390
|
+
settings.permissions = {
|
|
391
|
+
allow: permSettings.allow,
|
|
392
|
+
deny: permSettings.deny
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// 保存设置
|
|
396
|
+
fs.writeFileSync(projSettingsFile, JSON.stringify(settings, null, 2), 'utf8');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
permissionInfo = {
|
|
400
|
+
template: permissionTemplate,
|
|
401
|
+
appliedAt: new Date().toISOString()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
} catch (permError) {
|
|
405
|
+
console.warn('应用权限模板失败:', permError.message);
|
|
406
|
+
// 不中断工作区创建流程
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
302
410
|
// 保存工作区配置
|
|
303
411
|
const workspaceId = generateWorkspaceId();
|
|
304
412
|
const workspace = {
|
|
@@ -308,6 +416,7 @@ function createWorkspace(options) {
|
|
|
308
416
|
path: workspacePath,
|
|
309
417
|
projects: workspaceProjects,
|
|
310
418
|
configTemplate: templateInfo,
|
|
419
|
+
permissionTemplate: permissionInfo,
|
|
311
420
|
createdAt: new Date().toISOString(),
|
|
312
421
|
lastUsed: new Date().toISOString()
|
|
313
422
|
};
|