@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,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Registry Service
|
|
3
|
+
*
|
|
4
|
+
* Manages a unified config registry at ~/.claude/cc-tool/config-registry.json
|
|
5
|
+
* that tracks skills, commands, agents, rules with enable/disable and per-platform support.
|
|
6
|
+
*
|
|
7
|
+
* Storage directories: ~/.claude/cc-tool/configs/{skills,commands,agents,rules}/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
// Configuration paths
|
|
15
|
+
const CC_TOOL_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
16
|
+
const REGISTRY_FILE = path.join(CC_TOOL_DIR, 'config-registry.json');
|
|
17
|
+
const CONFIGS_DIR = path.join(CC_TOOL_DIR, 'configs');
|
|
18
|
+
|
|
19
|
+
// Claude Code native directories
|
|
20
|
+
const CLAUDE_DIRS = {
|
|
21
|
+
skills: path.join(os.homedir(), '.claude', 'skills'),
|
|
22
|
+
commands: path.join(os.homedir(), '.claude', 'commands'),
|
|
23
|
+
agents: path.join(os.homedir(), '.claude', 'agents'),
|
|
24
|
+
rules: path.join(os.homedir(), '.claude', 'rules'),
|
|
25
|
+
plugins: path.join(os.homedir(), '.claude', 'plugins')
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Valid config types
|
|
29
|
+
const CONFIG_TYPES = ['skills', 'commands', 'agents', 'rules', 'plugins'];
|
|
30
|
+
|
|
31
|
+
// Default registry structure
|
|
32
|
+
const DEFAULT_REGISTRY = {
|
|
33
|
+
version: 1,
|
|
34
|
+
skills: {},
|
|
35
|
+
commands: {},
|
|
36
|
+
agents: {},
|
|
37
|
+
rules: {},
|
|
38
|
+
plugins: {}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ensure directory exists
|
|
43
|
+
*/
|
|
44
|
+
function ensureDir(dirPath) {
|
|
45
|
+
if (!fs.existsSync(dirPath)) {
|
|
46
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Config Registry Service
|
|
52
|
+
*/
|
|
53
|
+
class ConfigRegistryService {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.registryPath = REGISTRY_FILE;
|
|
56
|
+
this.configsDir = CONFIGS_DIR;
|
|
57
|
+
|
|
58
|
+
// Ensure directories exist
|
|
59
|
+
this._ensureDirs();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ensure all required directories exist
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
_ensureDirs() {
|
|
67
|
+
ensureDir(CC_TOOL_DIR);
|
|
68
|
+
ensureDir(this.configsDir);
|
|
69
|
+
|
|
70
|
+
for (const type of CONFIG_TYPES) {
|
|
71
|
+
ensureDir(path.join(this.configsDir, type));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read registry from file
|
|
77
|
+
* @returns {Object} Registry data
|
|
78
|
+
*/
|
|
79
|
+
_readRegistry() {
|
|
80
|
+
try {
|
|
81
|
+
if (fs.existsSync(this.registryPath)) {
|
|
82
|
+
const content = fs.readFileSync(this.registryPath, 'utf-8');
|
|
83
|
+
const data = JSON.parse(content);
|
|
84
|
+
|
|
85
|
+
// Ensure all type keys exist
|
|
86
|
+
for (const type of CONFIG_TYPES) {
|
|
87
|
+
if (!data[type]) {
|
|
88
|
+
data[type] = {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('[ConfigRegistry] Failed to read registry:', err.message);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { ...DEFAULT_REGISTRY };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Write registry to file (atomic write via temp file + rename)
|
|
103
|
+
* @param {Object} data - Registry data to write
|
|
104
|
+
*/
|
|
105
|
+
_writeRegistry(data) {
|
|
106
|
+
try {
|
|
107
|
+
ensureDir(path.dirname(this.registryPath));
|
|
108
|
+
|
|
109
|
+
const tempPath = this.registryPath + '.tmp';
|
|
110
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
111
|
+
fs.renameSync(tempPath, this.registryPath);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('[ConfigRegistry] Failed to write registry:', err.message);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a single item from registry
|
|
120
|
+
* @param {string} type - Config type (skills, commands, agents, rules)
|
|
121
|
+
* @param {string} name - Item name/key
|
|
122
|
+
* @returns {Object|null} Registry entry or null
|
|
123
|
+
*/
|
|
124
|
+
getItem(type, name) {
|
|
125
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
126
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const registry = this._readRegistry();
|
|
130
|
+
return registry[type][name] || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Set/update an item in registry
|
|
135
|
+
* @param {string} type - Config type
|
|
136
|
+
* @param {string} name - Item name/key
|
|
137
|
+
* @param {Object} data - Item data
|
|
138
|
+
* @returns {Object} Updated entry
|
|
139
|
+
*/
|
|
140
|
+
setItem(type, name, data) {
|
|
141
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
142
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const registry = this._readRegistry();
|
|
146
|
+
const now = new Date().toISOString();
|
|
147
|
+
|
|
148
|
+
const existing = registry[type][name];
|
|
149
|
+
const entry = {
|
|
150
|
+
enabled: data.enabled !== undefined ? data.enabled : true,
|
|
151
|
+
platforms: data.platforms || { claude: true, codex: false },
|
|
152
|
+
createdAt: existing?.createdAt || now,
|
|
153
|
+
updatedAt: now,
|
|
154
|
+
source: data.source || 'local',
|
|
155
|
+
...data
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
registry[type][name] = entry;
|
|
159
|
+
this._writeRegistry(registry);
|
|
160
|
+
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Remove an item from registry
|
|
166
|
+
* @param {string} type - Config type
|
|
167
|
+
* @param {string} name - Item name/key
|
|
168
|
+
* @returns {boolean} True if removed, false if not found
|
|
169
|
+
*/
|
|
170
|
+
removeItem(type, name) {
|
|
171
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
172
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const registry = this._readRegistry();
|
|
176
|
+
|
|
177
|
+
if (!registry[type][name]) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
delete registry[type][name];
|
|
182
|
+
this._writeRegistry(registry);
|
|
183
|
+
|
|
184
|
+
// Also remove the actual config files
|
|
185
|
+
const configPath = this.getConfigPath(type, name);
|
|
186
|
+
if (fs.existsSync(configPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const stats = fs.statSync(configPath);
|
|
189
|
+
if (stats.isDirectory()) {
|
|
190
|
+
fs.rmSync(configPath, { recursive: true, force: true });
|
|
191
|
+
} else {
|
|
192
|
+
fs.unlinkSync(configPath);
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error(`[ConfigRegistry] Failed to remove config files for ${type}/${name}:`, err.message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* List all items of a type
|
|
204
|
+
* @param {string} type - Config type
|
|
205
|
+
* @returns {Object} { name: registryEntry } map
|
|
206
|
+
*/
|
|
207
|
+
listItems(type) {
|
|
208
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
209
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const registry = this._readRegistry();
|
|
213
|
+
return registry[type] || {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Toggle enabled status for an item
|
|
218
|
+
* @param {string} type - Config type
|
|
219
|
+
* @param {string} name - Item name/key
|
|
220
|
+
* @param {boolean} enabled - New enabled status
|
|
221
|
+
* @returns {Object} Updated entry
|
|
222
|
+
*/
|
|
223
|
+
toggleEnabled(type, name, enabled) {
|
|
224
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
225
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const registry = this._readRegistry();
|
|
229
|
+
const entry = registry[type][name];
|
|
230
|
+
|
|
231
|
+
if (!entry) {
|
|
232
|
+
throw new Error(`Item "${name}" not found in ${type}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
entry.enabled = enabled;
|
|
236
|
+
entry.updatedAt = new Date().toISOString();
|
|
237
|
+
|
|
238
|
+
this._writeRegistry(registry);
|
|
239
|
+
|
|
240
|
+
return entry;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Toggle platform support for an item
|
|
245
|
+
* @param {string} type - Config type
|
|
246
|
+
* @param {string} name - Item name/key
|
|
247
|
+
* @param {string} platform - Platform name (claude, codex)
|
|
248
|
+
* @param {boolean} enabled - New platform status
|
|
249
|
+
* @returns {Object} Updated entry
|
|
250
|
+
*/
|
|
251
|
+
togglePlatform(type, name, platform, enabled) {
|
|
252
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
253
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!['claude', 'codex'].includes(platform)) {
|
|
257
|
+
throw new Error(`Invalid platform: ${platform}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const registry = this._readRegistry();
|
|
261
|
+
const entry = registry[type][name];
|
|
262
|
+
|
|
263
|
+
if (!entry) {
|
|
264
|
+
throw new Error(`Item "${name}" not found in ${type}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!entry.platforms) {
|
|
268
|
+
entry.platforms = { claude: true, codex: false };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
entry.platforms[platform] = enabled;
|
|
272
|
+
entry.updatedAt = new Date().toISOString();
|
|
273
|
+
|
|
274
|
+
this._writeRegistry(registry);
|
|
275
|
+
|
|
276
|
+
return entry;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Import configs from Claude Code native directories
|
|
281
|
+
* @param {string} type - Config type to import
|
|
282
|
+
* @returns {Object} { imported: number, skipped: number, items: string[] }
|
|
283
|
+
*/
|
|
284
|
+
importFromClaude(type) {
|
|
285
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
286
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const sourceDir = CLAUDE_DIRS[type];
|
|
290
|
+
const destDir = path.join(this.configsDir, type);
|
|
291
|
+
|
|
292
|
+
if (!fs.existsSync(sourceDir)) {
|
|
293
|
+
return { imported: 0, skipped: 0, items: [] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const registry = this._readRegistry();
|
|
297
|
+
const result = {
|
|
298
|
+
imported: 0,
|
|
299
|
+
skipped: 0,
|
|
300
|
+
items: []
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (type === 'skills') {
|
|
304
|
+
// Skills are directory-based with SKILL.md marker
|
|
305
|
+
this._importSkills(sourceDir, destDir, registry, result);
|
|
306
|
+
} else if (type === 'plugins') {
|
|
307
|
+
// Plugins are directory-based (similar to skills)
|
|
308
|
+
this._importPlugins(sourceDir, destDir, registry, result);
|
|
309
|
+
} else {
|
|
310
|
+
// Commands, agents, rules are file-based (.md files)
|
|
311
|
+
this._importFileBasedConfigs(type, sourceDir, destDir, '', registry, result);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this._writeRegistry(registry);
|
|
315
|
+
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Import skills (directory-based)
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
_importSkills(sourceDir, destDir, registry, result) {
|
|
324
|
+
try {
|
|
325
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
326
|
+
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const skillDir = path.join(sourceDir, entry.name);
|
|
333
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
334
|
+
|
|
335
|
+
// Check if it's a valid skill directory
|
|
336
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const name = entry.name;
|
|
341
|
+
|
|
342
|
+
// Skip if already in registry
|
|
343
|
+
if (registry.skills[name]) {
|
|
344
|
+
result.skipped++;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Copy to cc-tool configs
|
|
349
|
+
const destPath = path.join(destDir, name);
|
|
350
|
+
try {
|
|
351
|
+
this._copyDirRecursive(skillDir, destPath);
|
|
352
|
+
|
|
353
|
+
// Add to registry
|
|
354
|
+
registry.skills[name] = {
|
|
355
|
+
enabled: true,
|
|
356
|
+
platforms: { claude: true, codex: false },
|
|
357
|
+
createdAt: new Date().toISOString(),
|
|
358
|
+
updatedAt: new Date().toISOString(),
|
|
359
|
+
source: 'imported'
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
result.imported++;
|
|
363
|
+
result.items.push(name);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(`[ConfigRegistry] Failed to import skill "${name}":`, err.message);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.error('[ConfigRegistry] Failed to scan skills directory:', err.message);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Import plugins (directory-based, similar to skills)
|
|
375
|
+
* Plugins are directories containing plugin.json or similar marker
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
_importPlugins(sourceDir, destDir, registry, result) {
|
|
379
|
+
try {
|
|
380
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
381
|
+
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const pluginDir = path.join(sourceDir, entry.name);
|
|
388
|
+
|
|
389
|
+
// Check if it's a valid plugin directory (has plugin.json or any content)
|
|
390
|
+
// Plugins may have various structures, so we just check it's a non-empty directory
|
|
391
|
+
const contents = fs.readdirSync(pluginDir);
|
|
392
|
+
if (contents.length === 0) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const name = entry.name;
|
|
397
|
+
|
|
398
|
+
// Skip if already in registry
|
|
399
|
+
if (registry.plugins[name]) {
|
|
400
|
+
result.skipped++;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Copy to cc-tool configs
|
|
405
|
+
const destPath = path.join(destDir, name);
|
|
406
|
+
try {
|
|
407
|
+
this._copyDirRecursive(pluginDir, destPath);
|
|
408
|
+
|
|
409
|
+
// Add to registry
|
|
410
|
+
registry.plugins[name] = {
|
|
411
|
+
enabled: true,
|
|
412
|
+
platforms: { claude: true, codex: false },
|
|
413
|
+
createdAt: new Date().toISOString(),
|
|
414
|
+
updatedAt: new Date().toISOString(),
|
|
415
|
+
source: 'imported'
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
result.imported++;
|
|
419
|
+
result.items.push(name);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
console.error(`[ConfigRegistry] Failed to import plugin "${name}":`, err.message);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error('[ConfigRegistry] Failed to scan plugins directory:', err.message);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Import file-based configs (commands, agents, rules)
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
_importFileBasedConfigs(type, sourceDir, destDir, relativePath, registry, result) {
|
|
434
|
+
try {
|
|
435
|
+
const currentSource = relativePath ? path.join(sourceDir, relativePath) : sourceDir;
|
|
436
|
+
|
|
437
|
+
if (!fs.existsSync(currentSource)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const entries = fs.readdirSync(currentSource, { withFileTypes: true });
|
|
442
|
+
|
|
443
|
+
for (const entry of entries) {
|
|
444
|
+
if (entry.name.startsWith('.')) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
449
|
+
|
|
450
|
+
if (entry.isDirectory()) {
|
|
451
|
+
// Recursively scan subdirectories
|
|
452
|
+
this._importFileBasedConfigs(type, sourceDir, destDir, entryRelativePath, registry, result);
|
|
453
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
454
|
+
// This is a config file
|
|
455
|
+
const name = entryRelativePath; // Key is the relative path
|
|
456
|
+
|
|
457
|
+
// Skip if already in registry
|
|
458
|
+
if (registry[type][name]) {
|
|
459
|
+
result.skipped++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Copy file to cc-tool configs
|
|
464
|
+
const sourcePath = path.join(sourceDir, entryRelativePath);
|
|
465
|
+
const destPath = path.join(destDir, entryRelativePath);
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
// Ensure destination directory exists
|
|
469
|
+
ensureDir(path.dirname(destPath));
|
|
470
|
+
|
|
471
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
472
|
+
|
|
473
|
+
// Add to registry
|
|
474
|
+
registry[type][name] = {
|
|
475
|
+
enabled: true,
|
|
476
|
+
platforms: { claude: true, codex: false },
|
|
477
|
+
createdAt: new Date().toISOString(),
|
|
478
|
+
updatedAt: new Date().toISOString(),
|
|
479
|
+
source: 'imported'
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
result.imported++;
|
|
483
|
+
result.items.push(name);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
console.error(`[ConfigRegistry] Failed to import ${type}/${name}:`, err.message);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
console.error(`[ConfigRegistry] Failed to scan ${type} directory:`, err.message);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Recursively copy a directory
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
_copyDirRecursive(src, dest) {
|
|
499
|
+
ensureDir(dest);
|
|
500
|
+
|
|
501
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
502
|
+
|
|
503
|
+
for (const entry of entries) {
|
|
504
|
+
const srcPath = path.join(src, entry.name);
|
|
505
|
+
const destPath = path.join(dest, entry.name);
|
|
506
|
+
|
|
507
|
+
if (entry.isDirectory()) {
|
|
508
|
+
this._copyDirRecursive(srcPath, destPath);
|
|
509
|
+
} else {
|
|
510
|
+
fs.copyFileSync(srcPath, destPath);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get statistics for all config types
|
|
517
|
+
* @returns {Object} Stats with counts per type and enabled/disabled breakdown
|
|
518
|
+
*/
|
|
519
|
+
getStats() {
|
|
520
|
+
const registry = this._readRegistry();
|
|
521
|
+
const stats = {
|
|
522
|
+
total: 0,
|
|
523
|
+
byType: {},
|
|
524
|
+
byPlatform: {
|
|
525
|
+
claude: 0,
|
|
526
|
+
codex: 0
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
for (const type of CONFIG_TYPES) {
|
|
531
|
+
const items = Object.values(registry[type] || {});
|
|
532
|
+
const typeStats = {
|
|
533
|
+
total: items.length,
|
|
534
|
+
enabled: items.filter(i => i.enabled).length,
|
|
535
|
+
disabled: items.filter(i => !i.enabled).length,
|
|
536
|
+
claude: items.filter(i => i.platforms?.claude).length,
|
|
537
|
+
codex: items.filter(i => i.platforms?.codex).length
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
stats.byType[type] = typeStats;
|
|
541
|
+
stats.total += typeStats.total;
|
|
542
|
+
stats.byPlatform.claude += typeStats.claude;
|
|
543
|
+
stats.byPlatform.codex += typeStats.codex;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return stats;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Get the path to a config in cc-tool storage
|
|
551
|
+
* @param {string} type - Config type
|
|
552
|
+
* @param {string} name - Item name/key
|
|
553
|
+
* @returns {string} Full path to config
|
|
554
|
+
*/
|
|
555
|
+
getConfigPath(type, name) {
|
|
556
|
+
if (!CONFIG_TYPES.includes(type)) {
|
|
557
|
+
throw new Error(`Invalid config type: ${type}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return path.join(this.configsDir, type, name);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Check if a config file/directory exists in storage
|
|
565
|
+
* @param {string} type - Config type
|
|
566
|
+
* @param {string} name - Item name/key
|
|
567
|
+
* @returns {boolean} True if exists
|
|
568
|
+
*/
|
|
569
|
+
configExists(type, name) {
|
|
570
|
+
const configPath = this.getConfigPath(type, name);
|
|
571
|
+
return fs.existsSync(configPath);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get config content
|
|
576
|
+
* @param {string} type - Config type
|
|
577
|
+
* @param {string} name - Item name/key
|
|
578
|
+
* @returns {string|null} File content or null
|
|
579
|
+
*/
|
|
580
|
+
getConfigContent(type, name) {
|
|
581
|
+
const configPath = this.getConfigPath(type, name);
|
|
582
|
+
|
|
583
|
+
if (!fs.existsSync(configPath)) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const stats = fs.statSync(configPath);
|
|
588
|
+
|
|
589
|
+
if (stats.isDirectory()) {
|
|
590
|
+
// For skills, return SKILL.md content
|
|
591
|
+
const skillMdPath = path.join(configPath, 'SKILL.md');
|
|
592
|
+
if (fs.existsSync(skillMdPath)) {
|
|
593
|
+
return fs.readFileSync(skillMdPath, 'utf-8');
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return fs.readFileSync(configPath, 'utf-8');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Sync registry with actual files in storage
|
|
603
|
+
* Adds missing entries, removes orphaned entries
|
|
604
|
+
* @returns {Object} { added: number, removed: number }
|
|
605
|
+
*/
|
|
606
|
+
syncRegistry() {
|
|
607
|
+
const registry = this._readRegistry();
|
|
608
|
+
const result = { added: 0, removed: 0 };
|
|
609
|
+
|
|
610
|
+
for (const type of CONFIG_TYPES) {
|
|
611
|
+
const typeDir = path.join(this.configsDir, type);
|
|
612
|
+
|
|
613
|
+
if (!fs.existsSync(typeDir)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Find orphaned registry entries (file deleted)
|
|
618
|
+
for (const name of Object.keys(registry[type])) {
|
|
619
|
+
if (!this.configExists(type, name)) {
|
|
620
|
+
delete registry[type][name];
|
|
621
|
+
result.removed++;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Find missing registry entries (file exists but not registered)
|
|
626
|
+
if (type === 'skills') {
|
|
627
|
+
this._syncSkillsRegistry(typeDir, registry, result);
|
|
628
|
+
} else if (type === 'plugins') {
|
|
629
|
+
this._syncPluginsRegistry(typeDir, registry, result);
|
|
630
|
+
} else {
|
|
631
|
+
this._syncFileBasedRegistry(type, typeDir, '', registry, result);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
this._writeRegistry(registry);
|
|
636
|
+
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Sync skills registry
|
|
642
|
+
* @private
|
|
643
|
+
*/
|
|
644
|
+
_syncSkillsRegistry(typeDir, registry, result) {
|
|
645
|
+
try {
|
|
646
|
+
const entries = fs.readdirSync(typeDir, { withFileTypes: true });
|
|
647
|
+
|
|
648
|
+
for (const entry of entries) {
|
|
649
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const skillMdPath = path.join(typeDir, entry.name, 'SKILL.md');
|
|
654
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const name = entry.name;
|
|
659
|
+
|
|
660
|
+
if (!registry.skills[name]) {
|
|
661
|
+
registry.skills[name] = {
|
|
662
|
+
enabled: true,
|
|
663
|
+
platforms: { claude: true, codex: false },
|
|
664
|
+
createdAt: new Date().toISOString(),
|
|
665
|
+
updatedAt: new Date().toISOString(),
|
|
666
|
+
source: 'synced'
|
|
667
|
+
};
|
|
668
|
+
result.added++;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.error('[ConfigRegistry] Failed to sync skills registry:', err.message);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Sync plugins registry
|
|
678
|
+
* @private
|
|
679
|
+
*/
|
|
680
|
+
_syncPluginsRegistry(typeDir, registry, result) {
|
|
681
|
+
try {
|
|
682
|
+
const entries = fs.readdirSync(typeDir, { withFileTypes: true });
|
|
683
|
+
|
|
684
|
+
for (const entry of entries) {
|
|
685
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const pluginDir = path.join(typeDir, entry.name);
|
|
690
|
+
const contents = fs.readdirSync(pluginDir);
|
|
691
|
+
if (contents.length === 0) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const name = entry.name;
|
|
696
|
+
|
|
697
|
+
if (!registry.plugins[name]) {
|
|
698
|
+
registry.plugins[name] = {
|
|
699
|
+
enabled: true,
|
|
700
|
+
platforms: { claude: true, codex: false },
|
|
701
|
+
createdAt: new Date().toISOString(),
|
|
702
|
+
updatedAt: new Date().toISOString(),
|
|
703
|
+
source: 'synced'
|
|
704
|
+
};
|
|
705
|
+
result.added++;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.error('[ConfigRegistry] Failed to sync plugins registry:', err.message);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Sync file-based config registry
|
|
715
|
+
* @private
|
|
716
|
+
*/
|
|
717
|
+
_syncFileBasedRegistry(type, baseDir, relativePath, registry, result) {
|
|
718
|
+
try {
|
|
719
|
+
const currentDir = relativePath ? path.join(baseDir, relativePath) : baseDir;
|
|
720
|
+
|
|
721
|
+
if (!fs.existsSync(currentDir)) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
726
|
+
|
|
727
|
+
for (const entry of entries) {
|
|
728
|
+
if (entry.name.startsWith('.')) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
733
|
+
|
|
734
|
+
if (entry.isDirectory()) {
|
|
735
|
+
this._syncFileBasedRegistry(type, baseDir, entryRelativePath, registry, result);
|
|
736
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
737
|
+
const name = entryRelativePath;
|
|
738
|
+
|
|
739
|
+
if (!registry[type][name]) {
|
|
740
|
+
registry[type][name] = {
|
|
741
|
+
enabled: true,
|
|
742
|
+
platforms: { claude: true, codex: false },
|
|
743
|
+
createdAt: new Date().toISOString(),
|
|
744
|
+
updatedAt: new Date().toISOString(),
|
|
745
|
+
source: 'synced'
|
|
746
|
+
};
|
|
747
|
+
result.added++;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
} catch (err) {
|
|
752
|
+
console.error(`[ConfigRegistry] Failed to sync ${type} registry:`, err.message);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
module.exports = {
|
|
758
|
+
ConfigRegistryService,
|
|
759
|
+
CONFIG_TYPES,
|
|
760
|
+
CONFIGS_DIR,
|
|
761
|
+
REGISTRY_FILE
|
|
762
|
+
};
|