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