@adversity/coding-tool-x 3.0.4 → 3.0.6
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 +19 -0
- package/README.md +35 -0
- package/dist/web/assets/{icons-BlzwYoRU.js → icons-BxudHPiX.js} +1 -1
- package/dist/web/assets/index-D2VfwJBa.js +14 -0
- package/dist/web/assets/index-oXBzu0bd.css +41 -0
- package/dist/web/assets/{naive-ui-B1TP-0TP.js → naive-ui-DT-Uur8K.js} +1 -1
- package/dist/web/index.html +4 -4
- package/docs/model-redirection.md +251 -0
- package/package.json +1 -1
- package/src/server/api/channels.js +3 -0
- package/src/server/api/codex-channels.js +40 -0
- package/src/server/api/config-registry.js +341 -0
- package/src/server/api/gemini-channels.js +40 -0
- package/src/server/api/permissions.js +30 -15
- package/src/server/codex-proxy-server.js +126 -1
- package/src/server/gemini-proxy-server.js +61 -1
- package/src/server/index.js +3 -0
- package/src/server/proxy-server.js +98 -1
- package/src/server/services/channel-scheduler.js +3 -1
- package/src/server/services/channels.js +4 -1
- package/src/server/services/codex-channels.js +9 -3
- package/src/server/services/config-registry-service.js +762 -0
- package/src/server/services/config-sync-manager.js +456 -0
- package/src/server/services/config-templates-service.js +38 -3
- package/src/server/services/gemini-channels.js +7 -1
- package/src/server/services/model-detector.js +116 -23
- package/src/server/services/permission-templates-service.js +0 -31
- package/dist/web/assets/index-Bpjcdalh.js +0 -14
- package/dist/web/assets/index-CB782_71.css +0 -41
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Sync Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages file synchronization between cc-tool central storage and CLI directories:
|
|
5
|
+
* - Claude Code: ~/.claude/{skills,commands,agents,rules}/
|
|
6
|
+
* - Codex CLI: ~/.codex/skills/, ~/.codex/prompts/
|
|
7
|
+
*
|
|
8
|
+
* Config types:
|
|
9
|
+
* - skills: directory-based (each skill is a dir with SKILL.md)
|
|
10
|
+
* - commands: file-based (.md), may be nested in subdirectories
|
|
11
|
+
* - agents: file-based (.md), flat directory
|
|
12
|
+
* - rules: file-based (.md), may be nested in subdirectories
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const { convertSkillToCodex, convertCommandToCodex } = require('./format-converter');
|
|
19
|
+
|
|
20
|
+
// Paths
|
|
21
|
+
const HOME = os.homedir();
|
|
22
|
+
const CC_TOOL_CONFIGS = path.join(HOME, '.claude', 'cc-tool', 'configs');
|
|
23
|
+
const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
|
|
24
|
+
const CODEX_DIR = path.join(HOME, '.codex');
|
|
25
|
+
|
|
26
|
+
// Config type definitions
|
|
27
|
+
const CONFIG_TYPES = {
|
|
28
|
+
skills: {
|
|
29
|
+
isDirectory: true,
|
|
30
|
+
markerFile: 'SKILL.md',
|
|
31
|
+
claudeTarget: 'skills',
|
|
32
|
+
codexTarget: 'skills',
|
|
33
|
+
codexSupported: true,
|
|
34
|
+
convertForCodex: true
|
|
35
|
+
},
|
|
36
|
+
commands: {
|
|
37
|
+
isDirectory: false,
|
|
38
|
+
extension: '.md',
|
|
39
|
+
claudeTarget: 'commands',
|
|
40
|
+
codexTarget: 'prompts',
|
|
41
|
+
codexSupported: true,
|
|
42
|
+
convertForCodex: true
|
|
43
|
+
},
|
|
44
|
+
agents: {
|
|
45
|
+
isDirectory: false,
|
|
46
|
+
extension: '.md',
|
|
47
|
+
claudeTarget: 'agents',
|
|
48
|
+
codexSupported: false
|
|
49
|
+
},
|
|
50
|
+
rules: {
|
|
51
|
+
isDirectory: false,
|
|
52
|
+
extension: '.md',
|
|
53
|
+
claudeTarget: 'rules',
|
|
54
|
+
codexSupported: false
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
class ConfigSyncManager {
|
|
59
|
+
constructor() {
|
|
60
|
+
this.ccToolConfigs = CC_TOOL_CONFIGS;
|
|
61
|
+
this.claudeDir = CLAUDE_CODE_DIR;
|
|
62
|
+
this.codexDir = CODEX_DIR;
|
|
63
|
+
this.configTypes = CONFIG_TYPES;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sync a config item to Claude Code
|
|
68
|
+
* @param {string} type - Config type (skills, commands, agents, rules)
|
|
69
|
+
* @param {string} name - Item name (directory name for skills, file path for others)
|
|
70
|
+
* @returns {Object} Result with success status
|
|
71
|
+
*/
|
|
72
|
+
syncToClaude(type, name) {
|
|
73
|
+
const config = this.configTypes[type];
|
|
74
|
+
if (!config) {
|
|
75
|
+
console.log(`[ConfigSyncManager] Unknown config type: ${type}`);
|
|
76
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sourcePath = path.join(this.ccToolConfigs, type, name);
|
|
80
|
+
const targetPath = path.join(this.claudeDir, config.claudeTarget, name);
|
|
81
|
+
|
|
82
|
+
// Check if source exists
|
|
83
|
+
if (!fs.existsSync(sourcePath)) {
|
|
84
|
+
console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
|
|
85
|
+
return { success: false, error: 'Source not found' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (config.isDirectory) {
|
|
90
|
+
// Copy entire directory recursively
|
|
91
|
+
this._ensureDir(path.dirname(targetPath));
|
|
92
|
+
this._copyDirRecursive(sourcePath, targetPath);
|
|
93
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (directory)`);
|
|
94
|
+
} else {
|
|
95
|
+
// Copy single file, preserving subdirectory structure
|
|
96
|
+
this._ensureDir(path.dirname(targetPath));
|
|
97
|
+
this._copyFile(sourcePath, targetPath);
|
|
98
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (file)`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { success: true, target: targetPath };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`[ConfigSyncManager] Sync to Claude failed:`, err.message);
|
|
104
|
+
return { success: false, error: err.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove a config item from Claude Code
|
|
110
|
+
* @param {string} type - Config type
|
|
111
|
+
* @param {string} name - Item name
|
|
112
|
+
* @returns {Object} Result with success status
|
|
113
|
+
*/
|
|
114
|
+
removeFromClaude(type, name) {
|
|
115
|
+
const config = this.configTypes[type];
|
|
116
|
+
if (!config) {
|
|
117
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const targetPath = path.join(this.claudeDir, config.claudeTarget, name);
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(targetPath)) {
|
|
123
|
+
console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
|
|
124
|
+
return { success: true, message: 'Already removed' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (config.isDirectory) {
|
|
129
|
+
// Remove entire directory
|
|
130
|
+
this._removeRecursive(targetPath);
|
|
131
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (directory)`);
|
|
132
|
+
} else {
|
|
133
|
+
// Remove file
|
|
134
|
+
fs.unlinkSync(targetPath);
|
|
135
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (file)`);
|
|
136
|
+
|
|
137
|
+
// Clean up empty parent directories for commands/rules
|
|
138
|
+
this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.claudeDir, config.claudeTarget));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { success: true };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(`[ConfigSyncManager] Remove from Claude failed:`, err.message);
|
|
144
|
+
return { success: false, error: err.message };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sync a config item to Codex CLI
|
|
150
|
+
* Only skills and commands are supported
|
|
151
|
+
* @param {string} type - Config type
|
|
152
|
+
* @param {string} name - Item name
|
|
153
|
+
* @returns {Object} Result with success status and any warnings
|
|
154
|
+
*/
|
|
155
|
+
syncToCodex(type, name) {
|
|
156
|
+
const config = this.configTypes[type];
|
|
157
|
+
if (!config) {
|
|
158
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!config.codexSupported) {
|
|
162
|
+
console.log(`[ConfigSyncManager] ${type} not supported by Codex, skipping`);
|
|
163
|
+
return { success: true, skipped: true, reason: 'Not supported by Codex' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const sourcePath = path.join(this.ccToolConfigs, type, name);
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(sourcePath)) {
|
|
169
|
+
console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
|
|
170
|
+
return { success: false, error: 'Source not found' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const warnings = [];
|
|
175
|
+
|
|
176
|
+
if (type === 'skills') {
|
|
177
|
+
// Skills: copy directory, convert SKILL.md content
|
|
178
|
+
const targetPath = path.join(this.codexDir, config.codexTarget, name);
|
|
179
|
+
this._ensureDir(targetPath);
|
|
180
|
+
|
|
181
|
+
// Copy all files, converting SKILL.md
|
|
182
|
+
this._copyDirWithConversion(sourcePath, targetPath, (filePath, content) => {
|
|
183
|
+
if (path.basename(filePath) === 'SKILL.md') {
|
|
184
|
+
const result = convertSkillToCodex(content);
|
|
185
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
186
|
+
warnings.push(...result.warnings);
|
|
187
|
+
}
|
|
188
|
+
return result.content;
|
|
189
|
+
}
|
|
190
|
+
return content;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (skill directory)`);
|
|
194
|
+
return { success: true, target: targetPath, warnings };
|
|
195
|
+
|
|
196
|
+
} else if (type === 'commands') {
|
|
197
|
+
// Commands: convert and write to prompts directory
|
|
198
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
199
|
+
const result = convertCommandToCodex(content);
|
|
200
|
+
|
|
201
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
202
|
+
warnings.push(...result.warnings);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Target path in codex prompts (same relative path structure)
|
|
206
|
+
const targetPath = path.join(this.codexDir, config.codexTarget, name);
|
|
207
|
+
this._ensureDir(path.dirname(targetPath));
|
|
208
|
+
fs.writeFileSync(targetPath, result.content, 'utf-8');
|
|
209
|
+
|
|
210
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (prompt)`);
|
|
211
|
+
return { success: true, target: targetPath, warnings };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { success: false, error: 'Unexpected type' };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(`[ConfigSyncManager] Sync to Codex failed:`, err.message);
|
|
217
|
+
return { success: false, error: err.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Remove a config item from Codex CLI
|
|
223
|
+
* @param {string} type - Config type
|
|
224
|
+
* @param {string} name - Item name
|
|
225
|
+
* @returns {Object} Result with success status
|
|
226
|
+
*/
|
|
227
|
+
removeFromCodex(type, name) {
|
|
228
|
+
const config = this.configTypes[type];
|
|
229
|
+
if (!config) {
|
|
230
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!config.codexSupported) {
|
|
234
|
+
return { success: true, skipped: true, reason: 'Not supported by Codex' };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const targetPath = path.join(this.codexDir, config.codexTarget, name);
|
|
238
|
+
|
|
239
|
+
if (!fs.existsSync(targetPath)) {
|
|
240
|
+
console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
|
|
241
|
+
return { success: true, message: 'Already removed' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
if (type === 'skills') {
|
|
246
|
+
// Remove entire directory
|
|
247
|
+
this._removeRecursive(targetPath);
|
|
248
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (skill directory)`);
|
|
249
|
+
} else {
|
|
250
|
+
// Remove file
|
|
251
|
+
fs.unlinkSync(targetPath);
|
|
252
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (prompt)`);
|
|
253
|
+
|
|
254
|
+
// Clean up empty parent directories
|
|
255
|
+
this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.codexDir, config.codexTarget));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { success: true };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(`[ConfigSyncManager] Remove from Codex failed:`, err.message);
|
|
261
|
+
return { success: false, error: err.message };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Batch sync based on registry data
|
|
267
|
+
* @param {string} type - Config type
|
|
268
|
+
* @param {Object} registryItems - Registry items { name: { enabled, platforms: { claude, codex } } }
|
|
269
|
+
* @returns {Object} Results summary
|
|
270
|
+
*/
|
|
271
|
+
syncAll(type, registryItems) {
|
|
272
|
+
const results = {
|
|
273
|
+
synced: [],
|
|
274
|
+
removed: [],
|
|
275
|
+
errors: [],
|
|
276
|
+
warnings: []
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (!registryItems || typeof registryItems !== 'object') {
|
|
280
|
+
return results;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const [name, item] of Object.entries(registryItems)) {
|
|
284
|
+
if (!item || typeof item !== 'object') continue;
|
|
285
|
+
|
|
286
|
+
const { enabled, platforms } = item;
|
|
287
|
+
|
|
288
|
+
if (enabled && platforms) {
|
|
289
|
+
// Sync to enabled platforms
|
|
290
|
+
if (platforms.claude) {
|
|
291
|
+
const result = this.syncToClaude(type, name);
|
|
292
|
+
if (result.success && !result.skipped) {
|
|
293
|
+
results.synced.push({ type, name, platform: 'claude' });
|
|
294
|
+
} else if (!result.success) {
|
|
295
|
+
results.errors.push({ type, name, platform: 'claude', error: result.error });
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Platform disabled, remove
|
|
299
|
+
const result = this.removeFromClaude(type, name);
|
|
300
|
+
if (result.success && !result.message) {
|
|
301
|
+
results.removed.push({ type, name, platform: 'claude' });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (platforms.codex) {
|
|
306
|
+
const result = this.syncToCodex(type, name);
|
|
307
|
+
if (result.success && !result.skipped) {
|
|
308
|
+
results.synced.push({ type, name, platform: 'codex' });
|
|
309
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
310
|
+
results.warnings.push({ type, name, platform: 'codex', warnings: result.warnings });
|
|
311
|
+
}
|
|
312
|
+
} else if (!result.success) {
|
|
313
|
+
results.errors.push({ type, name, platform: 'codex', error: result.error });
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Platform disabled, remove
|
|
317
|
+
const result = this.removeFromCodex(type, name);
|
|
318
|
+
if (result.success && !result.message && !result.skipped) {
|
|
319
|
+
results.removed.push({ type, name, platform: 'codex' });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Item disabled, remove from all platforms
|
|
324
|
+
const claudeResult = this.removeFromClaude(type, name);
|
|
325
|
+
if (claudeResult.success && !claudeResult.message) {
|
|
326
|
+
results.removed.push({ type, name, platform: 'claude' });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const codexResult = this.removeFromCodex(type, name);
|
|
330
|
+
if (codexResult.success && !codexResult.message && !codexResult.skipped) {
|
|
331
|
+
results.removed.push({ type, name, platform: 'codex' });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log(`[ConfigSyncManager] syncAll(${type}): synced=${results.synced.length}, removed=${results.removed.length}, errors=${results.errors.length}`);
|
|
337
|
+
return results;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ==================== Helper Methods ====================
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Ensure a directory exists
|
|
344
|
+
*/
|
|
345
|
+
_ensureDir(dir) {
|
|
346
|
+
if (!fs.existsSync(dir)) {
|
|
347
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Recursively copy a directory
|
|
353
|
+
*/
|
|
354
|
+
_copyDirRecursive(src, dest) {
|
|
355
|
+
this._ensureDir(dest);
|
|
356
|
+
|
|
357
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
358
|
+
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
const srcPath = path.join(src, entry.name);
|
|
361
|
+
const destPath = path.join(dest, entry.name);
|
|
362
|
+
|
|
363
|
+
if (entry.isDirectory()) {
|
|
364
|
+
this._copyDirRecursive(srcPath, destPath);
|
|
365
|
+
} else {
|
|
366
|
+
fs.copyFileSync(srcPath, destPath);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Copy a directory with content transformation
|
|
373
|
+
* @param {string} src - Source directory
|
|
374
|
+
* @param {string} dest - Destination directory
|
|
375
|
+
* @param {Function} transform - Function(filePath, content) => transformedContent
|
|
376
|
+
*/
|
|
377
|
+
_copyDirWithConversion(src, dest, transform) {
|
|
378
|
+
this._ensureDir(dest);
|
|
379
|
+
|
|
380
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
381
|
+
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
const srcPath = path.join(src, entry.name);
|
|
384
|
+
const destPath = path.join(dest, entry.name);
|
|
385
|
+
|
|
386
|
+
if (entry.isDirectory()) {
|
|
387
|
+
this._copyDirWithConversion(srcPath, destPath, transform);
|
|
388
|
+
} else {
|
|
389
|
+
// Check if it's a text file that should be transformed
|
|
390
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
391
|
+
const textExtensions = ['.md', '.txt', '.json', '.js', '.ts', '.py', '.sh', '.yaml', '.yml'];
|
|
392
|
+
|
|
393
|
+
if (textExtensions.includes(ext)) {
|
|
394
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
395
|
+
const transformed = transform(srcPath, content);
|
|
396
|
+
fs.writeFileSync(destPath, transformed, 'utf-8');
|
|
397
|
+
} else {
|
|
398
|
+
// Binary file, copy as-is
|
|
399
|
+
fs.copyFileSync(srcPath, destPath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Copy a single file
|
|
407
|
+
*/
|
|
408
|
+
_copyFile(src, dest) {
|
|
409
|
+
fs.copyFileSync(src, dest);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Recursively remove a file or directory
|
|
414
|
+
*/
|
|
415
|
+
_removeRecursive(target) {
|
|
416
|
+
if (!fs.existsSync(target)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Clean up empty parent directories up to the base directory
|
|
425
|
+
*/
|
|
426
|
+
_cleanupEmptyParents(dir, baseDir) {
|
|
427
|
+
// Normalize paths for comparison
|
|
428
|
+
const normalizedDir = path.resolve(dir);
|
|
429
|
+
const normalizedBase = path.resolve(baseDir);
|
|
430
|
+
|
|
431
|
+
// Don't go above base directory
|
|
432
|
+
if (!normalizedDir.startsWith(normalizedBase) || normalizedDir === normalizedBase) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const entries = fs.readdirSync(dir);
|
|
438
|
+
if (entries.length === 0) {
|
|
439
|
+
fs.rmdirSync(dir);
|
|
440
|
+
console.log(`[ConfigSyncManager] Removed empty directory: ${dir}`);
|
|
441
|
+
// Recurse to parent
|
|
442
|
+
this._cleanupEmptyParents(path.dirname(dir), baseDir);
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
// Ignore errors (directory might not exist or permission issues)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
module.exports = {
|
|
451
|
+
ConfigSyncManager,
|
|
452
|
+
CONFIG_TYPES,
|
|
453
|
+
CC_TOOL_CONFIGS,
|
|
454
|
+
CLAUDE_CODE_DIR,
|
|
455
|
+
CODEX_DIR
|
|
456
|
+
};
|
|
@@ -13,8 +13,10 @@ const { AgentsService } = require('./agents-service');
|
|
|
13
13
|
const { CommandsService } = require('./commands-service');
|
|
14
14
|
const { RulesService } = require('./rules-service');
|
|
15
15
|
const { SkillService } = require('./skill-service');
|
|
16
|
+
const { PluginsService } = require('./plugins-service');
|
|
16
17
|
const mcpService = require('./mcp-service');
|
|
17
18
|
const skillService = new SkillService();
|
|
19
|
+
const pluginsService = new PluginsService();
|
|
18
20
|
|
|
19
21
|
// 配置模板文件路径
|
|
20
22
|
const TEMPLATES_FILE = path.join(PATHS.config, 'config-templates.json');
|
|
@@ -149,6 +151,7 @@ You are an experienced full-stack developer focused on delivering high-quality c
|
|
|
149
151
|
rules: [],
|
|
150
152
|
commands: [],
|
|
151
153
|
agents: [],
|
|
154
|
+
plugins: [],
|
|
152
155
|
mcpServers: ['github', 'context7', 'fetch', 'memory'],
|
|
153
156
|
isBuiltin: true
|
|
154
157
|
},
|
|
@@ -297,6 +300,7 @@ You are a senior technical architect focused on system design and technical plan
|
|
|
297
300
|
rules: [],
|
|
298
301
|
commands: [],
|
|
299
302
|
agents: [],
|
|
303
|
+
plugins: [],
|
|
300
304
|
mcpServers: ['context7', 'fetch', 'memory'],
|
|
301
305
|
isBuiltin: true
|
|
302
306
|
},
|
|
@@ -455,6 +459,7 @@ For each issue:
|
|
|
455
459
|
rules: [],
|
|
456
460
|
commands: [],
|
|
457
461
|
agents: [],
|
|
462
|
+
plugins: [],
|
|
458
463
|
mcpServers: ['github'],
|
|
459
464
|
isBuiltin: true
|
|
460
465
|
},
|
|
@@ -472,6 +477,7 @@ For each issue:
|
|
|
472
477
|
rules: [],
|
|
473
478
|
commands: [],
|
|
474
479
|
agents: [],
|
|
480
|
+
plugins: [],
|
|
475
481
|
mcpServers: [],
|
|
476
482
|
isBuiltin: true
|
|
477
483
|
}
|
|
@@ -567,6 +573,7 @@ function createCustomTemplate(template) {
|
|
|
567
573
|
rules: template.rules || [],
|
|
568
574
|
commands: template.commands || [],
|
|
569
575
|
agents: template.agents || [],
|
|
576
|
+
plugins: template.plugins || [],
|
|
570
577
|
mcpServers: template.mcpServers || [],
|
|
571
578
|
isBuiltin: false,
|
|
572
579
|
createdAt: new Date().toISOString()
|
|
@@ -637,6 +644,7 @@ function applyTemplate(targetDir, templateId) {
|
|
|
637
644
|
rules: 0,
|
|
638
645
|
commands: 0,
|
|
639
646
|
agents: 0,
|
|
647
|
+
plugins: 0,
|
|
640
648
|
mcpServers: 0
|
|
641
649
|
};
|
|
642
650
|
|
|
@@ -656,6 +664,7 @@ function applyTemplate(targetDir, templateId) {
|
|
|
656
664
|
rules: template.rules,
|
|
657
665
|
commands: template.commands,
|
|
658
666
|
agents: template.agents,
|
|
667
|
+
plugins: template.plugins,
|
|
659
668
|
mcpServers: template.mcpServers
|
|
660
669
|
};
|
|
661
670
|
|
|
@@ -693,7 +702,7 @@ function readCurrentConfig(targetDir) {
|
|
|
693
702
|
|
|
694
703
|
/**
|
|
695
704
|
* 获取所有可用配置(用于模板编辑器选择)
|
|
696
|
-
* 返回用户级的 agents, commands, rules + MCP 服务器列表
|
|
705
|
+
* 返回用户级的 agents, commands, rules, plugins + MCP 服务器列表
|
|
697
706
|
*/
|
|
698
707
|
function getAvailableConfigs() {
|
|
699
708
|
const agentsService = new AgentsService();
|
|
@@ -706,6 +715,9 @@ function getAvailableConfigs() {
|
|
|
706
715
|
const { rules } = rulesService.listRules();
|
|
707
716
|
const installedSkills = skillService.getInstalledSkills();
|
|
708
717
|
|
|
718
|
+
// 获取已安装的插件和市场插件
|
|
719
|
+
const { plugins: installedPlugins } = pluginsService.listPlugins();
|
|
720
|
+
|
|
709
721
|
// 获取 MCP 服务器
|
|
710
722
|
const mcpServers = mcpService.getAllServers();
|
|
711
723
|
const mcpServerList = Object.values(mcpServers).map(s => ({
|
|
@@ -754,6 +766,14 @@ function getAvailableConfigs() {
|
|
|
754
766
|
paths: r.paths,
|
|
755
767
|
body: r.body
|
|
756
768
|
})),
|
|
769
|
+
plugins: installedPlugins.map(p => ({
|
|
770
|
+
name: p.name,
|
|
771
|
+
description: p.description || '',
|
|
772
|
+
version: p.version || '1.0.0',
|
|
773
|
+
marketplace: p.marketplace || null,
|
|
774
|
+
source: p.source || null,
|
|
775
|
+
repoUrl: p.repoUrl || null
|
|
776
|
+
})),
|
|
757
777
|
mcpServers: mcpServerList,
|
|
758
778
|
mcpPresets
|
|
759
779
|
};
|
|
@@ -819,6 +839,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
819
839
|
agents: { applied: 0, files: [] },
|
|
820
840
|
commands: { applied: 0, files: [] },
|
|
821
841
|
rules: { applied: 0, files: [] },
|
|
842
|
+
plugins: { applied: 0, items: [] },
|
|
822
843
|
mcpServers: { applied: 0 }
|
|
823
844
|
};
|
|
824
845
|
|
|
@@ -910,7 +931,14 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
910
931
|
}
|
|
911
932
|
}
|
|
912
933
|
|
|
913
|
-
// 5.
|
|
934
|
+
// 5. 记录 Plugins(插件只记录,不自动安装)
|
|
935
|
+
// 插件安装需要用户手动确认,这里只记录模板中包含的插件信息
|
|
936
|
+
if (template.plugins?.length > 0) {
|
|
937
|
+
results.plugins.applied = template.plugins.length;
|
|
938
|
+
results.plugins.items = template.plugins.map(p => p.name);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// 6. 写入 MCP 配置到 .mcp.json
|
|
914
942
|
if (template.mcpServers?.length > 0) {
|
|
915
943
|
const mcpConfig = { mcpServers: {} };
|
|
916
944
|
const allServers = mcpService.getAllServers();
|
|
@@ -938,7 +966,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
938
966
|
}
|
|
939
967
|
}
|
|
940
968
|
|
|
941
|
-
//
|
|
969
|
+
// 7. 创建配置记录文件
|
|
942
970
|
const configRecord = {
|
|
943
971
|
templateId: template.id,
|
|
944
972
|
templateName: template.name,
|
|
@@ -949,6 +977,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
949
977
|
agents: template.agents?.map(a => a.fileName || a.name) || [],
|
|
950
978
|
commands: template.commands?.map(c => c.name) || [],
|
|
951
979
|
rules: template.rules?.map(r => r.fileName) || [],
|
|
980
|
+
plugins: template.plugins?.map(p => p.name) || [],
|
|
952
981
|
mcpServers: template.mcpServers || []
|
|
953
982
|
};
|
|
954
983
|
const recordPath = path.join(targetDir, '.ctx-config.json');
|
|
@@ -984,6 +1013,7 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
984
1013
|
agents: 0,
|
|
985
1014
|
commands: 0,
|
|
986
1015
|
rules: 0,
|
|
1016
|
+
plugins: 0,
|
|
987
1017
|
mcpServers: 0
|
|
988
1018
|
}
|
|
989
1019
|
};
|
|
@@ -1088,6 +1118,11 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
1088
1118
|
preview.summary.mcpServers = template.mcpServers.length;
|
|
1089
1119
|
}
|
|
1090
1120
|
|
|
1121
|
+
// 统计 Plugins(插件不写入文件,只记录数量)
|
|
1122
|
+
if (template.plugins?.length > 0) {
|
|
1123
|
+
preview.summary.plugins = template.plugins.length;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1091
1126
|
return preview;
|
|
1092
1127
|
}
|
|
1093
1128
|
|
|
@@ -70,7 +70,9 @@ function loadChannels() {
|
|
|
70
70
|
...ch,
|
|
71
71
|
enabled: ch.enabled !== false, // 默认启用
|
|
72
72
|
weight: ch.weight || 1,
|
|
73
|
-
maxConcurrency: ch.maxConcurrency || null
|
|
73
|
+
maxConcurrency: ch.maxConcurrency || null,
|
|
74
|
+
modelRedirects: ch.modelRedirects || [],
|
|
75
|
+
speedTestModel: ch.speedTestModel || null
|
|
74
76
|
}));
|
|
75
77
|
}
|
|
76
78
|
return data;
|
|
@@ -171,6 +173,8 @@ function createChannel(name, baseUrl, apiKey, model = 'gemini-2.5-pro', extraCon
|
|
|
171
173
|
enabled: extraConfig.enabled !== false, // 默认启用
|
|
172
174
|
weight: extraConfig.weight || 1,
|
|
173
175
|
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
176
|
+
modelRedirects: extraConfig.modelRedirects || [],
|
|
177
|
+
speedTestModel: extraConfig.speedTestModel || null,
|
|
174
178
|
createdAt: Date.now(),
|
|
175
179
|
updatedAt: Date.now()
|
|
176
180
|
};
|
|
@@ -208,6 +212,8 @@ function updateChannel(channelId, updates) {
|
|
|
208
212
|
...updates,
|
|
209
213
|
id: channelId, // 保持 ID 不变
|
|
210
214
|
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
215
|
+
modelRedirects: updates.modelRedirects !== undefined ? updates.modelRedirects : (oldChannel.modelRedirects || []),
|
|
216
|
+
speedTestModel: updates.speedTestModel !== undefined ? updates.speedTestModel : (oldChannel.speedTestModel || null),
|
|
211
217
|
updatedAt: Date.now()
|
|
212
218
|
};
|
|
213
219
|
|