@adversity/coding-tool-x 2.3.0 → 2.4.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.
- package/CHANGELOG.md +41 -0
- package/README.md +8 -0
- package/dist/web/assets/icons-Dom8a0SN.js +1 -0
- package/dist/web/assets/index-CQeUIH7E.css +41 -0
- package/dist/web/assets/index-YrjlFzC4.js +14 -0
- package/dist/web/assets/naive-ui-BjMHakwv.js +1 -0
- package/dist/web/assets/vendors-DtJKdpSj.js +7 -0
- package/dist/web/assets/vue-vendor-VFuFB5f4.js +44 -0
- package/dist/web/index.html +6 -2
- package/package.json +2 -2
- package/src/commands/export-config.js +205 -0
- package/src/config/default.js +1 -1
- package/src/server/api/config-export.js +122 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +12 -6
- package/src/server/api/health-check.js +1 -89
- package/src/server/api/permissions.js +92 -69
- package/src/server/api/projects.js +2 -2
- package/src/server/api/sessions.js +70 -70
- package/src/server/api/skills.js +206 -0
- package/src/server/api/terminal.js +26 -0
- package/src/server/index.js +7 -11
- package/src/server/services/config-export-service.js +415 -0
- package/src/server/services/config-sync-service.js +515 -0
- package/src/server/services/config-templates-service.js +61 -38
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/health-check.js +1 -315
- package/src/server/services/permission-templates-service.js +339 -0
- package/src/server/services/pty-manager.js +35 -1
- package/src/server/services/sessions.js +122 -44
- package/src/server/services/skill-service.js +252 -2
- package/src/server/services/workspace-service.js +44 -84
- package/src/server/websocket-server.js +4 -1
- package/dist/web/assets/index-dhun1bYQ.js +0 -3555
- package/dist/web/assets/index-hHb7DAda.css +0 -41
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置同步服务
|
|
3
|
+
*
|
|
4
|
+
* 支持 skills, rules, agents, commands 的在工作区和全局之间同步
|
|
5
|
+
*
|
|
6
|
+
* 配置位置:
|
|
7
|
+
* - 全局: ~/.claude/
|
|
8
|
+
* - skills/
|
|
9
|
+
* - rules/
|
|
10
|
+
* - agents/
|
|
11
|
+
* - commands/
|
|
12
|
+
* - 工作区: <project>/.claude/
|
|
13
|
+
* - rules/
|
|
14
|
+
* - agents/
|
|
15
|
+
* - commands/
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
// 全局配置目录
|
|
23
|
+
const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.claude');
|
|
24
|
+
|
|
25
|
+
// 配置类型定义
|
|
26
|
+
const CONFIG_TYPES = {
|
|
27
|
+
skills: {
|
|
28
|
+
globalDir: 'skills',
|
|
29
|
+
projectDir: null, // skills 不支持项目级
|
|
30
|
+
isDirectory: true, // skills 是目录结构
|
|
31
|
+
markerFile: 'SKILL.md'
|
|
32
|
+
},
|
|
33
|
+
rules: {
|
|
34
|
+
globalDir: 'rules',
|
|
35
|
+
projectDir: 'rules',
|
|
36
|
+
isDirectory: false,
|
|
37
|
+
fileExtension: '.md'
|
|
38
|
+
},
|
|
39
|
+
agents: {
|
|
40
|
+
globalDir: 'agents',
|
|
41
|
+
projectDir: 'agents',
|
|
42
|
+
isDirectory: false,
|
|
43
|
+
fileExtension: '.md'
|
|
44
|
+
},
|
|
45
|
+
commands: {
|
|
46
|
+
globalDir: 'commands',
|
|
47
|
+
projectDir: 'commands',
|
|
48
|
+
isDirectory: false,
|
|
49
|
+
fileExtension: '.md'
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 确保目录存在
|
|
55
|
+
*/
|
|
56
|
+
function ensureDir(dirPath) {
|
|
57
|
+
if (!fs.existsSync(dirPath)) {
|
|
58
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 递归复制目录
|
|
64
|
+
*/
|
|
65
|
+
function copyDirRecursive(src, dest) {
|
|
66
|
+
ensureDir(dest);
|
|
67
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const srcPath = path.join(src, entry.name);
|
|
71
|
+
const destPath = path.join(dest, entry.name);
|
|
72
|
+
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
copyDirRecursive(srcPath, destPath);
|
|
75
|
+
} else {
|
|
76
|
+
fs.copyFileSync(srcPath, destPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 配置同步服务类
|
|
83
|
+
*/
|
|
84
|
+
class ConfigSyncService {
|
|
85
|
+
constructor() {
|
|
86
|
+
this.globalConfigDir = GLOBAL_CONFIG_DIR;
|
|
87
|
+
ensureDir(this.globalConfigDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 获取可用的配置项列表
|
|
92
|
+
* @param {string} source - 'global' 或 'workspace'
|
|
93
|
+
* @param {string} projectPath - 工作区项目路径(source 为 workspace 时必需)
|
|
94
|
+
* @returns {Object} 各类型的配置项列表
|
|
95
|
+
*/
|
|
96
|
+
getAvailableConfigs(source, projectPath = null) {
|
|
97
|
+
const result = {
|
|
98
|
+
skills: [],
|
|
99
|
+
rules: [],
|
|
100
|
+
agents: [],
|
|
101
|
+
commands: []
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const [type, config] of Object.entries(CONFIG_TYPES)) {
|
|
105
|
+
let dir;
|
|
106
|
+
|
|
107
|
+
if (source === 'global') {
|
|
108
|
+
dir = path.join(this.globalConfigDir, config.globalDir);
|
|
109
|
+
} else if (source === 'workspace' && projectPath) {
|
|
110
|
+
if (!config.projectDir) {
|
|
111
|
+
// skills 不支持项目级
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
dir = path.join(projectPath, '.claude', config.projectDir);
|
|
115
|
+
} else {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(dir)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (config.isDirectory) {
|
|
124
|
+
// Skills: 扫描目录
|
|
125
|
+
result[type] = this._scanSkillsDir(dir);
|
|
126
|
+
} else {
|
|
127
|
+
// Rules/Agents/Commands: 扫描 md 文件
|
|
128
|
+
result[type] = this._scanMdFiles(dir, config.fileExtension);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 扫描 skills 目录
|
|
137
|
+
*/
|
|
138
|
+
_scanSkillsDir(dir) {
|
|
139
|
+
const skills = [];
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
143
|
+
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
146
|
+
|
|
147
|
+
const skillPath = path.join(dir, entry.name);
|
|
148
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
149
|
+
|
|
150
|
+
if (fs.existsSync(skillMdPath)) {
|
|
151
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
152
|
+
const metadata = this._parseSkillMetadata(content);
|
|
153
|
+
|
|
154
|
+
// 获取文件列表
|
|
155
|
+
const files = this._getDirectoryFiles(skillPath);
|
|
156
|
+
|
|
157
|
+
skills.push({
|
|
158
|
+
name: metadata.name || entry.name,
|
|
159
|
+
directory: entry.name,
|
|
160
|
+
description: metadata.description || '',
|
|
161
|
+
files: files.length,
|
|
162
|
+
size: this._getDirSize(skillPath)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error('[ConfigSync] Scan skills error:', err.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return skills;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 扫描 md 文件(rules/agents/commands)
|
|
175
|
+
*/
|
|
176
|
+
_scanMdFiles(dir, extension = '.md') {
|
|
177
|
+
const items = [];
|
|
178
|
+
|
|
179
|
+
const scan = (currentDir, relativePath = '') => {
|
|
180
|
+
try {
|
|
181
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
182
|
+
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (entry.name.startsWith('.')) continue;
|
|
185
|
+
|
|
186
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
187
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
188
|
+
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
scan(fullPath, relPath);
|
|
191
|
+
} else if (entry.name.endsWith(extension)) {
|
|
192
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
193
|
+
const metadata = this._parseFrontmatter(content);
|
|
194
|
+
const stats = fs.statSync(fullPath);
|
|
195
|
+
|
|
196
|
+
items.push({
|
|
197
|
+
name: metadata.name || path.basename(entry.name, extension),
|
|
198
|
+
path: relPath,
|
|
199
|
+
description: metadata.description || '',
|
|
200
|
+
size: stats.size
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
// 忽略读取错误
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
scan(dir);
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 解析 SKILL.md 元数据
|
|
215
|
+
*/
|
|
216
|
+
_parseSkillMetadata(content) {
|
|
217
|
+
const result = { name: null, description: null };
|
|
218
|
+
|
|
219
|
+
// 匹配 YAML frontmatter
|
|
220
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
221
|
+
if (match) {
|
|
222
|
+
const yaml = match[1];
|
|
223
|
+
const nameMatch = yaml.match(/name:\s*["']?([^"'\n]+)["']?/);
|
|
224
|
+
const descMatch = yaml.match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
225
|
+
|
|
226
|
+
if (nameMatch) result.name = nameMatch[1].trim();
|
|
227
|
+
if (descMatch) result.description = descMatch[1].trim();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 解析 frontmatter
|
|
235
|
+
*/
|
|
236
|
+
_parseFrontmatter(content) {
|
|
237
|
+
const result = { name: null, description: null };
|
|
238
|
+
|
|
239
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
240
|
+
if (match) {
|
|
241
|
+
const yaml = match[1];
|
|
242
|
+
const nameMatch = yaml.match(/name:\s*["']?([^"'\n]+)["']?/);
|
|
243
|
+
const descMatch = yaml.match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
244
|
+
|
|
245
|
+
if (nameMatch) result.name = nameMatch[1].trim();
|
|
246
|
+
if (descMatch) result.description = descMatch[1].trim();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 获取目录下的文件列表
|
|
254
|
+
*/
|
|
255
|
+
_getDirectoryFiles(dir) {
|
|
256
|
+
const files = [];
|
|
257
|
+
|
|
258
|
+
const scan = (currentDir, relativePath = '') => {
|
|
259
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
260
|
+
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
263
|
+
|
|
264
|
+
if (entry.isDirectory()) {
|
|
265
|
+
scan(path.join(currentDir, entry.name), relPath);
|
|
266
|
+
} else {
|
|
267
|
+
files.push(relPath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
scan(dir);
|
|
273
|
+
return files;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 获取目录大小
|
|
278
|
+
*/
|
|
279
|
+
_getDirSize(dir) {
|
|
280
|
+
let size = 0;
|
|
281
|
+
|
|
282
|
+
const scan = (currentDir) => {
|
|
283
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
284
|
+
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
287
|
+
|
|
288
|
+
if (entry.isDirectory()) {
|
|
289
|
+
scan(fullPath);
|
|
290
|
+
} else {
|
|
291
|
+
size += fs.statSync(fullPath).size;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
scan(dir);
|
|
297
|
+
return size;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 预览同步结果
|
|
302
|
+
* @param {Object} options
|
|
303
|
+
* @param {string} options.source - 'global' 或 'workspace'
|
|
304
|
+
* @param {string} options.target - 'global' 或 'workspace'
|
|
305
|
+
* @param {string[]} options.configTypes - 要同步的配置类型
|
|
306
|
+
* @param {string} options.projectPath - 工作区路径
|
|
307
|
+
* @param {Object} options.selectedItems - 选中的项目 { skills: [], rules: [], ... }
|
|
308
|
+
* @returns {Object} 预览结果
|
|
309
|
+
*/
|
|
310
|
+
previewSync(options) {
|
|
311
|
+
const { source, target, configTypes = [], projectPath, selectedItems = {} } = options;
|
|
312
|
+
|
|
313
|
+
const preview = {
|
|
314
|
+
willCreate: [],
|
|
315
|
+
willOverwrite: [],
|
|
316
|
+
willSkip: [],
|
|
317
|
+
errors: []
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// 验证参数
|
|
321
|
+
if (source === target) {
|
|
322
|
+
preview.errors.push('源和目标不能相同');
|
|
323
|
+
return preview;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (target === 'workspace' && !projectPath) {
|
|
327
|
+
preview.errors.push('同步到工作区需要指定项目路径');
|
|
328
|
+
return preview;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const type of configTypes) {
|
|
332
|
+
const config = CONFIG_TYPES[type];
|
|
333
|
+
if (!config) continue;
|
|
334
|
+
|
|
335
|
+
// Skills 只支持全局
|
|
336
|
+
if (type === 'skills' && target === 'workspace') {
|
|
337
|
+
preview.errors.push('Skills 不支持同步到工作区级别');
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const items = selectedItems[type] || [];
|
|
342
|
+
|
|
343
|
+
for (const item of items) {
|
|
344
|
+
const targetPath = this._getTargetPath(type, item, target, projectPath);
|
|
345
|
+
|
|
346
|
+
if (fs.existsSync(targetPath)) {
|
|
347
|
+
preview.willOverwrite.push({
|
|
348
|
+
type,
|
|
349
|
+
name: item.name || item.directory || item.path,
|
|
350
|
+
targetPath
|
|
351
|
+
});
|
|
352
|
+
} else {
|
|
353
|
+
preview.willCreate.push({
|
|
354
|
+
type,
|
|
355
|
+
name: item.name || item.directory || item.path,
|
|
356
|
+
targetPath
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return preview;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 获取目标路径
|
|
367
|
+
*/
|
|
368
|
+
_getTargetPath(type, item, target, projectPath) {
|
|
369
|
+
const config = CONFIG_TYPES[type];
|
|
370
|
+
let baseDir;
|
|
371
|
+
|
|
372
|
+
if (target === 'global') {
|
|
373
|
+
baseDir = path.join(this.globalConfigDir, config.globalDir);
|
|
374
|
+
} else {
|
|
375
|
+
baseDir = path.join(projectPath, '.claude', config.projectDir);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (config.isDirectory) {
|
|
379
|
+
// Skills
|
|
380
|
+
return path.join(baseDir, item.directory);
|
|
381
|
+
} else {
|
|
382
|
+
// Rules/Agents/Commands
|
|
383
|
+
return path.join(baseDir, item.path);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 执行同步
|
|
389
|
+
* @param {Object} options
|
|
390
|
+
* @param {string} options.source - 'global' 或 'workspace'
|
|
391
|
+
* @param {string} options.target - 'global' 或 'workspace'
|
|
392
|
+
* @param {string[]} options.configTypes - 要同步的配置类型
|
|
393
|
+
* @param {string} options.projectPath - 工作区路径
|
|
394
|
+
* @param {Object} options.selectedItems - 选中的项目
|
|
395
|
+
* @param {boolean} options.overwrite - 是否覆盖已存在的
|
|
396
|
+
* @returns {Object} 同步结果
|
|
397
|
+
*/
|
|
398
|
+
executeSync(options) {
|
|
399
|
+
const { source, target, configTypes = [], projectPath, selectedItems = {}, overwrite = false } = options;
|
|
400
|
+
|
|
401
|
+
const result = {
|
|
402
|
+
success: [],
|
|
403
|
+
failed: [],
|
|
404
|
+
skipped: []
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
for (const type of configTypes) {
|
|
408
|
+
const config = CONFIG_TYPES[type];
|
|
409
|
+
if (!config) continue;
|
|
410
|
+
|
|
411
|
+
// Skills 只支持全局
|
|
412
|
+
if (type === 'skills' && target === 'workspace') {
|
|
413
|
+
result.failed.push({
|
|
414
|
+
type,
|
|
415
|
+
name: 'skills',
|
|
416
|
+
error: 'Skills 不支持同步到工作区级别'
|
|
417
|
+
});
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const items = selectedItems[type] || [];
|
|
422
|
+
|
|
423
|
+
for (const item of items) {
|
|
424
|
+
try {
|
|
425
|
+
const sourcePath = this._getSourcePath(type, item, source, projectPath);
|
|
426
|
+
const targetPath = this._getTargetPath(type, item, target, projectPath);
|
|
427
|
+
|
|
428
|
+
// 检查目标是否存在
|
|
429
|
+
if (fs.existsSync(targetPath) && !overwrite) {
|
|
430
|
+
result.skipped.push({
|
|
431
|
+
type,
|
|
432
|
+
name: item.name || item.directory || item.path,
|
|
433
|
+
reason: '已存在'
|
|
434
|
+
});
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 确保目标目录存在
|
|
439
|
+
ensureDir(path.dirname(targetPath));
|
|
440
|
+
|
|
441
|
+
// 执行复制
|
|
442
|
+
if (config.isDirectory) {
|
|
443
|
+
copyDirRecursive(sourcePath, targetPath);
|
|
444
|
+
} else {
|
|
445
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
result.success.push({
|
|
449
|
+
type,
|
|
450
|
+
name: item.name || item.directory || item.path
|
|
451
|
+
});
|
|
452
|
+
} catch (err) {
|
|
453
|
+
result.failed.push({
|
|
454
|
+
type,
|
|
455
|
+
name: item.name || item.directory || item.path,
|
|
456
|
+
error: err.message
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 获取源路径
|
|
467
|
+
*/
|
|
468
|
+
_getSourcePath(type, item, source, projectPath) {
|
|
469
|
+
const config = CONFIG_TYPES[type];
|
|
470
|
+
let baseDir;
|
|
471
|
+
|
|
472
|
+
if (source === 'global') {
|
|
473
|
+
baseDir = path.join(this.globalConfigDir, config.globalDir);
|
|
474
|
+
} else {
|
|
475
|
+
baseDir = path.join(projectPath, '.claude', config.projectDir);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (config.isDirectory) {
|
|
479
|
+
// Skills
|
|
480
|
+
return path.join(baseDir, item.directory);
|
|
481
|
+
} else {
|
|
482
|
+
// Rules/Agents/Commands
|
|
483
|
+
return path.join(baseDir, item.path);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 获取同步统计信息
|
|
489
|
+
*/
|
|
490
|
+
getStats(projectPath = null) {
|
|
491
|
+
const globalConfigs = this.getAvailableConfigs('global');
|
|
492
|
+
const workspaceConfigs = projectPath
|
|
493
|
+
? this.getAvailableConfigs('workspace', projectPath)
|
|
494
|
+
: { skills: [], rules: [], agents: [], commands: [] };
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
global: {
|
|
498
|
+
skills: globalConfigs.skills.length,
|
|
499
|
+
rules: globalConfigs.rules.length,
|
|
500
|
+
agents: globalConfigs.agents.length,
|
|
501
|
+
commands: globalConfigs.commands.length
|
|
502
|
+
},
|
|
503
|
+
workspace: {
|
|
504
|
+
rules: workspaceConfigs.rules.length,
|
|
505
|
+
agents: workspaceConfigs.agents.length,
|
|
506
|
+
commands: workspaceConfigs.commands.length
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
module.exports = {
|
|
513
|
+
ConfigSyncService,
|
|
514
|
+
CONFIG_TYPES
|
|
515
|
+
};
|
|
@@ -802,7 +802,8 @@ function generateRuleContent(rule) {
|
|
|
802
802
|
* @param {string} targetDir - 目标项目目录
|
|
803
803
|
* @param {string} templateId - 模板 ID
|
|
804
804
|
* @param {object} options - 可选配置
|
|
805
|
-
* @param {string} options.
|
|
805
|
+
* @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini']
|
|
806
|
+
* @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
|
|
806
807
|
*/
|
|
807
808
|
function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
808
809
|
const template = getTemplateById(templateId);
|
|
@@ -813,7 +814,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
813
814
|
ensureDir(targetDir);
|
|
814
815
|
|
|
815
816
|
const results = {
|
|
816
|
-
|
|
817
|
+
aiConfigs: [], // 改为数组存储多个 AI 配置结果
|
|
817
818
|
skills: { applied: template.skills?.length || 0, items: template.skills?.map(s => s.directory || s.name) || [] },
|
|
818
819
|
agents: { applied: 0, files: [] },
|
|
819
820
|
commands: { applied: 0, files: [] },
|
|
@@ -822,27 +823,37 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
822
823
|
};
|
|
823
824
|
|
|
824
825
|
// 1. 写入 AI 配置文件(支持多 AI 类型选择)
|
|
825
|
-
|
|
826
|
+
// 兼容旧版单值参数
|
|
827
|
+
let aiConfigTypes = options.aiConfigTypes;
|
|
828
|
+
if (!aiConfigTypes) {
|
|
829
|
+
aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : ['claude'];
|
|
830
|
+
}
|
|
831
|
+
if (!Array.isArray(aiConfigTypes)) {
|
|
832
|
+
aiConfigTypes = [aiConfigTypes];
|
|
833
|
+
}
|
|
834
|
+
|
|
826
835
|
const aiConfigMap = {
|
|
827
836
|
claude: { fileName: 'CLAUDE.md', name: 'Claude' },
|
|
828
|
-
codex: { fileName: '
|
|
837
|
+
codex: { fileName: 'AGENTS.md', name: 'Codex' },
|
|
829
838
|
gemini: { fileName: 'GEMINI.md', name: 'Gemini' }
|
|
830
839
|
};
|
|
831
840
|
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
841
|
+
// 遍历所有选中的 AI 配置类型
|
|
842
|
+
for (const aiConfigType of aiConfigTypes) {
|
|
843
|
+
let aiConfig = null;
|
|
844
|
+
if (template.aiConfigs && template.aiConfigs[aiConfigType]) {
|
|
845
|
+
aiConfig = template.aiConfigs[aiConfigType];
|
|
846
|
+
} else if (aiConfigType === 'claude' && template.claudeMd) {
|
|
847
|
+
// 兼容旧的 claudeMd 字段
|
|
848
|
+
aiConfig = template.claudeMd;
|
|
849
|
+
}
|
|
840
850
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
851
|
+
if (aiConfig?.enabled && aiConfig?.content) {
|
|
852
|
+
const configInfo = aiConfigMap[aiConfigType];
|
|
853
|
+
const configPath = path.join(targetDir, configInfo.fileName);
|
|
854
|
+
fs.writeFileSync(configPath, aiConfig.content, 'utf-8');
|
|
855
|
+
results.aiConfigs.push({ applied: true, path: configInfo.fileName, type: configInfo.name, key: aiConfigType });
|
|
856
|
+
}
|
|
846
857
|
}
|
|
847
858
|
|
|
848
859
|
// 2. 写入 Agents
|
|
@@ -932,8 +943,8 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
932
943
|
templateId: template.id,
|
|
933
944
|
templateName: template.name,
|
|
934
945
|
appliedAt: new Date().toISOString(),
|
|
935
|
-
|
|
936
|
-
|
|
946
|
+
aiConfigTypes: aiConfigTypes,
|
|
947
|
+
aiConfigPaths: results.aiConfigs.map(c => c.path),
|
|
937
948
|
skills: template.skills?.map(s => s.directory || s.name) || [],
|
|
938
949
|
agents: template.agents?.map(a => a.fileName || a.name) || [],
|
|
939
950
|
commands: template.commands?.map(c => c.name) || [],
|
|
@@ -955,7 +966,8 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
955
966
|
* @param {string} targetDir - 目标项目目录
|
|
956
967
|
* @param {string} templateId - 模板 ID
|
|
957
968
|
* @param {object} options - 可选配置
|
|
958
|
-
* @param {string} options.
|
|
969
|
+
* @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini']
|
|
970
|
+
* @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
|
|
959
971
|
*/
|
|
960
972
|
function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
961
973
|
const template = getTemplateById(templateId);
|
|
@@ -967,7 +979,7 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
967
979
|
willCreate: [],
|
|
968
980
|
willOverwrite: [],
|
|
969
981
|
summary: {
|
|
970
|
-
|
|
982
|
+
aiConfigs: [], // 改为数组
|
|
971
983
|
skills: 0,
|
|
972
984
|
agents: 0,
|
|
973
985
|
commands: 0,
|
|
@@ -976,30 +988,41 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
976
988
|
}
|
|
977
989
|
};
|
|
978
990
|
|
|
979
|
-
// 检查 AI
|
|
980
|
-
|
|
991
|
+
// 检查 AI 配置文件(支持多选)
|
|
992
|
+
// 兼容旧版单值参数
|
|
993
|
+
let aiConfigTypes = options.aiConfigTypes;
|
|
994
|
+
if (!aiConfigTypes) {
|
|
995
|
+
aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : ['claude'];
|
|
996
|
+
}
|
|
997
|
+
if (!Array.isArray(aiConfigTypes)) {
|
|
998
|
+
aiConfigTypes = [aiConfigTypes];
|
|
999
|
+
}
|
|
1000
|
+
|
|
981
1001
|
const aiConfigMap = {
|
|
982
1002
|
claude: { fileName: 'CLAUDE.md', name: 'Claude' },
|
|
983
|
-
codex: { fileName: '
|
|
1003
|
+
codex: { fileName: 'AGENTS.md', name: 'Codex' },
|
|
984
1004
|
gemini: { fileName: 'GEMINI.md', name: 'Gemini' }
|
|
985
1005
|
};
|
|
986
1006
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
aiConfig =
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1007
|
+
// 遍历所有选中的 AI 配置类型
|
|
1008
|
+
for (const aiConfigType of aiConfigTypes) {
|
|
1009
|
+
let aiConfig = null;
|
|
1010
|
+
if (template.aiConfigs && template.aiConfigs[aiConfigType]) {
|
|
1011
|
+
aiConfig = template.aiConfigs[aiConfigType];
|
|
1012
|
+
} else if (aiConfigType === 'claude' && template.claudeMd) {
|
|
1013
|
+
aiConfig = template.claudeMd;
|
|
1014
|
+
}
|
|
993
1015
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1016
|
+
if (aiConfig?.enabled && aiConfig?.content) {
|
|
1017
|
+
const configInfo = aiConfigMap[aiConfigType];
|
|
1018
|
+
const configPath = path.join(targetDir, configInfo.fileName);
|
|
1019
|
+
if (fs.existsSync(configPath)) {
|
|
1020
|
+
preview.willOverwrite.push(configInfo.fileName);
|
|
1021
|
+
} else {
|
|
1022
|
+
preview.willCreate.push(configInfo.fileName);
|
|
1023
|
+
}
|
|
1024
|
+
preview.summary.aiConfigs.push({ type: aiConfigType, fileName: configInfo.fileName, name: configInfo.name });
|
|
1001
1025
|
}
|
|
1002
|
-
preview.summary.aiConfig = { type: aiConfigType, fileName: configInfo.fileName, name: configInfo.name };
|
|
1003
1026
|
}
|
|
1004
1027
|
|
|
1005
1028
|
// Skills 摘要
|