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,504 @@
1
+ /**
2
+ * 配置同步服务
3
+ *
4
+ * 支持 skills, agents, commands 的在工作区和全局之间同步
5
+ *
6
+ * 配置位置:
7
+ * - 全局: ~/.claude/
8
+ * - skills/
9
+ * - agents/
10
+ * - commands/
11
+ * - 工作区: <project>/.claude/
12
+ * - agents/
13
+ * - commands/
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ // 全局配置目录
21
+ const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.claude');
22
+
23
+ // 配置类型定义
24
+ const CONFIG_TYPES = {
25
+ skills: {
26
+ globalDir: 'skills',
27
+ projectDir: null, // skills 不支持项目级
28
+ isDirectory: true, // skills 是目录结构
29
+ markerFile: 'SKILL.md'
30
+ },
31
+ agents: {
32
+ globalDir: 'agents',
33
+ projectDir: 'agents',
34
+ isDirectory: false,
35
+ fileExtension: '.md'
36
+ },
37
+ commands: {
38
+ globalDir: 'commands',
39
+ projectDir: 'commands',
40
+ isDirectory: false,
41
+ fileExtension: '.md'
42
+ }
43
+ };
44
+
45
+ /**
46
+ * 确保目录存在
47
+ */
48
+ function ensureDir(dirPath) {
49
+ if (!fs.existsSync(dirPath)) {
50
+ fs.mkdirSync(dirPath, { recursive: true });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 递归复制目录
56
+ */
57
+ function copyDirRecursive(src, dest) {
58
+ ensureDir(dest);
59
+ const entries = fs.readdirSync(src, { withFileTypes: true });
60
+
61
+ for (const entry of entries) {
62
+ const srcPath = path.join(src, entry.name);
63
+ const destPath = path.join(dest, entry.name);
64
+
65
+ if (entry.isDirectory()) {
66
+ copyDirRecursive(srcPath, destPath);
67
+ } else {
68
+ fs.copyFileSync(srcPath, destPath);
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 配置同步服务类
75
+ */
76
+ class ConfigSyncService {
77
+ constructor() {
78
+ this.globalConfigDir = GLOBAL_CONFIG_DIR;
79
+ ensureDir(this.globalConfigDir);
80
+ }
81
+
82
+ /**
83
+ * 获取可用的配置项列表
84
+ * @param {string} source - 'global' 或 'workspace'
85
+ * @param {string} projectPath - 工作区项目路径(source 为 workspace 时必需)
86
+ * @returns {Object} 各类型的配置项列表
87
+ */
88
+ getAvailableConfigs(source, projectPath = null) {
89
+ const result = {
90
+ skills: [],
91
+ agents: [],
92
+ commands: []
93
+ };
94
+
95
+ for (const [type, config] of Object.entries(CONFIG_TYPES)) {
96
+ let dir;
97
+
98
+ if (source === 'global') {
99
+ dir = path.join(this.globalConfigDir, config.globalDir);
100
+ } else if (source === 'workspace' && projectPath) {
101
+ if (!config.projectDir) {
102
+ // skills 不支持项目级
103
+ continue;
104
+ }
105
+ dir = path.join(projectPath, '.claude', config.projectDir);
106
+ } else {
107
+ continue;
108
+ }
109
+
110
+ if (!fs.existsSync(dir)) {
111
+ continue;
112
+ }
113
+
114
+ if (config.isDirectory) {
115
+ // Skills: 扫描目录
116
+ result[type] = this._scanSkillsDir(dir);
117
+ } else {
118
+ // Agents/Commands: 扫描 md 文件
119
+ result[type] = this._scanMdFiles(dir, config.fileExtension);
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * 扫描 skills 目录
128
+ */
129
+ _scanSkillsDir(dir) {
130
+ const skills = [];
131
+
132
+ try {
133
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
134
+
135
+ for (const entry of entries) {
136
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
137
+
138
+ const skillPath = path.join(dir, entry.name);
139
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
140
+
141
+ if (fs.existsSync(skillMdPath)) {
142
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
143
+ const metadata = this._parseSkillMetadata(content);
144
+
145
+ // 获取文件列表
146
+ const files = this._getDirectoryFiles(skillPath);
147
+
148
+ skills.push({
149
+ name: metadata.name || entry.name,
150
+ directory: entry.name,
151
+ description: metadata.description || '',
152
+ files: files.length,
153
+ size: this._getDirSize(skillPath)
154
+ });
155
+ }
156
+ }
157
+ } catch (err) {
158
+ console.error('[ConfigSync] Scan skills error:', err.message);
159
+ }
160
+
161
+ return skills;
162
+ }
163
+
164
+ /**
165
+ * 扫描 md 文件(agents/commands)
166
+ */
167
+ _scanMdFiles(dir, extension = '.md') {
168
+ const items = [];
169
+
170
+ const scan = (currentDir, relativePath = '') => {
171
+ try {
172
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
173
+
174
+ for (const entry of entries) {
175
+ if (entry.name.startsWith('.')) continue;
176
+
177
+ const fullPath = path.join(currentDir, entry.name);
178
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
179
+
180
+ if (entry.isDirectory()) {
181
+ scan(fullPath, relPath);
182
+ } else if (entry.name.endsWith(extension)) {
183
+ const content = fs.readFileSync(fullPath, 'utf-8');
184
+ const metadata = this._parseFrontmatter(content);
185
+ const stats = fs.statSync(fullPath);
186
+
187
+ items.push({
188
+ name: metadata.name || path.basename(entry.name, extension),
189
+ path: relPath,
190
+ description: metadata.description || '',
191
+ size: stats.size
192
+ });
193
+ }
194
+ }
195
+ } catch (err) {
196
+ // 忽略读取错误
197
+ }
198
+ };
199
+
200
+ scan(dir);
201
+ return items;
202
+ }
203
+
204
+ /**
205
+ * 解析 SKILL.md 元数据
206
+ */
207
+ _parseSkillMetadata(content) {
208
+ const result = { name: null, description: null };
209
+
210
+ // 匹配 YAML frontmatter
211
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
212
+ if (match) {
213
+ const yaml = match[1];
214
+ const nameMatch = yaml.match(/name:\s*["']?([^"'\n]+)["']?/);
215
+ const descMatch = yaml.match(/description:\s*["']?([^"'\n]+)["']?/);
216
+
217
+ if (nameMatch) result.name = nameMatch[1].trim();
218
+ if (descMatch) result.description = descMatch[1].trim();
219
+ }
220
+
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * 解析 frontmatter
226
+ */
227
+ _parseFrontmatter(content) {
228
+ const result = { name: null, description: null };
229
+
230
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
231
+ if (match) {
232
+ const yaml = match[1];
233
+ const nameMatch = yaml.match(/name:\s*["']?([^"'\n]+)["']?/);
234
+ const descMatch = yaml.match(/description:\s*["']?([^"'\n]+)["']?/);
235
+
236
+ if (nameMatch) result.name = nameMatch[1].trim();
237
+ if (descMatch) result.description = descMatch[1].trim();
238
+ }
239
+
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * 获取目录下的文件列表
245
+ */
246
+ _getDirectoryFiles(dir) {
247
+ const files = [];
248
+
249
+ const scan = (currentDir, relativePath = '') => {
250
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
251
+
252
+ for (const entry of entries) {
253
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
254
+
255
+ if (entry.isDirectory()) {
256
+ scan(path.join(currentDir, entry.name), relPath);
257
+ } else {
258
+ files.push(relPath);
259
+ }
260
+ }
261
+ };
262
+
263
+ scan(dir);
264
+ return files;
265
+ }
266
+
267
+ /**
268
+ * 获取目录大小
269
+ */
270
+ _getDirSize(dir) {
271
+ let size = 0;
272
+
273
+ const scan = (currentDir) => {
274
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
275
+
276
+ for (const entry of entries) {
277
+ const fullPath = path.join(currentDir, entry.name);
278
+
279
+ if (entry.isDirectory()) {
280
+ scan(fullPath);
281
+ } else {
282
+ size += fs.statSync(fullPath).size;
283
+ }
284
+ }
285
+ };
286
+
287
+ scan(dir);
288
+ return size;
289
+ }
290
+
291
+ /**
292
+ * 预览同步结果
293
+ * @param {Object} options
294
+ * @param {string} options.source - 'global' 或 'workspace'
295
+ * @param {string} options.target - 'global' 或 'workspace'
296
+ * @param {string[]} options.configTypes - 要同步的配置类型
297
+ * @param {string} options.projectPath - 工作区路径
298
+ * @param {Object} options.selectedItems - 选中的项目 { skills: [], agents: [], ... }
299
+ * @returns {Object} 预览结果
300
+ */
301
+ previewSync(options) {
302
+ const { source, target, configTypes = [], projectPath, selectedItems = {} } = options;
303
+
304
+ const preview = {
305
+ willCreate: [],
306
+ willOverwrite: [],
307
+ willSkip: [],
308
+ errors: []
309
+ };
310
+
311
+ // 验证参数
312
+ if (source === target) {
313
+ preview.errors.push('源和目标不能相同');
314
+ return preview;
315
+ }
316
+
317
+ if (target === 'workspace' && !projectPath) {
318
+ preview.errors.push('同步到工作区需要指定项目路径');
319
+ return preview;
320
+ }
321
+
322
+ for (const type of configTypes) {
323
+ const config = CONFIG_TYPES[type];
324
+ if (!config) continue;
325
+
326
+ // Skills 只支持全局
327
+ if (type === 'skills' && target === 'workspace') {
328
+ preview.errors.push('Skills 不支持同步到工作区级别');
329
+ continue;
330
+ }
331
+
332
+ const items = selectedItems[type] || [];
333
+
334
+ for (const item of items) {
335
+ const targetPath = this._getTargetPath(type, item, target, projectPath);
336
+
337
+ if (fs.existsSync(targetPath)) {
338
+ preview.willOverwrite.push({
339
+ type,
340
+ name: item.name || item.directory || item.path,
341
+ targetPath
342
+ });
343
+ } else {
344
+ preview.willCreate.push({
345
+ type,
346
+ name: item.name || item.directory || item.path,
347
+ targetPath
348
+ });
349
+ }
350
+ }
351
+ }
352
+
353
+ return preview;
354
+ }
355
+
356
+ /**
357
+ * 获取目标路径
358
+ */
359
+ _getTargetPath(type, item, target, projectPath) {
360
+ const config = CONFIG_TYPES[type];
361
+ let baseDir;
362
+
363
+ if (target === 'global') {
364
+ baseDir = path.join(this.globalConfigDir, config.globalDir);
365
+ } else {
366
+ baseDir = path.join(projectPath, '.claude', config.projectDir);
367
+ }
368
+
369
+ if (config.isDirectory) {
370
+ // Skills
371
+ return path.join(baseDir, item.directory);
372
+ } else {
373
+ // Agents/Commands
374
+ return path.join(baseDir, item.path);
375
+ }
376
+ }
377
+
378
+ /**
379
+ * 执行同步
380
+ * @param {Object} options
381
+ * @param {string} options.source - 'global' 或 'workspace'
382
+ * @param {string} options.target - 'global' 或 'workspace'
383
+ * @param {string[]} options.configTypes - 要同步的配置类型
384
+ * @param {string} options.projectPath - 工作区路径
385
+ * @param {Object} options.selectedItems - 选中的项目
386
+ * @param {boolean} options.overwrite - 是否覆盖已存在的
387
+ * @returns {Object} 同步结果
388
+ */
389
+ executeSync(options) {
390
+ const { source, target, configTypes = [], projectPath, selectedItems = {}, overwrite = false } = options;
391
+
392
+ const result = {
393
+ success: [],
394
+ failed: [],
395
+ skipped: []
396
+ };
397
+
398
+ for (const type of configTypes) {
399
+ const config = CONFIG_TYPES[type];
400
+ if (!config) continue;
401
+
402
+ // Skills 只支持全局
403
+ if (type === 'skills' && target === 'workspace') {
404
+ result.failed.push({
405
+ type,
406
+ name: 'skills',
407
+ error: 'Skills 不支持同步到工作区级别'
408
+ });
409
+ continue;
410
+ }
411
+
412
+ const items = selectedItems[type] || [];
413
+
414
+ for (const item of items) {
415
+ try {
416
+ const sourcePath = this._getSourcePath(type, item, source, projectPath);
417
+ const targetPath = this._getTargetPath(type, item, target, projectPath);
418
+
419
+ // 检查目标是否存在
420
+ if (fs.existsSync(targetPath) && !overwrite) {
421
+ result.skipped.push({
422
+ type,
423
+ name: item.name || item.directory || item.path,
424
+ reason: '已存在'
425
+ });
426
+ continue;
427
+ }
428
+
429
+ // 确保目标目录存在
430
+ ensureDir(path.dirname(targetPath));
431
+
432
+ // 执行复制
433
+ if (config.isDirectory) {
434
+ copyDirRecursive(sourcePath, targetPath);
435
+ } else {
436
+ fs.copyFileSync(sourcePath, targetPath);
437
+ }
438
+
439
+ result.success.push({
440
+ type,
441
+ name: item.name || item.directory || item.path
442
+ });
443
+ } catch (err) {
444
+ result.failed.push({
445
+ type,
446
+ name: item.name || item.directory || item.path,
447
+ error: err.message
448
+ });
449
+ }
450
+ }
451
+ }
452
+
453
+ return result;
454
+ }
455
+
456
+ /**
457
+ * 获取源路径
458
+ */
459
+ _getSourcePath(type, item, source, projectPath) {
460
+ const config = CONFIG_TYPES[type];
461
+ let baseDir;
462
+
463
+ if (source === 'global') {
464
+ baseDir = path.join(this.globalConfigDir, config.globalDir);
465
+ } else {
466
+ baseDir = path.join(projectPath, '.claude', config.projectDir);
467
+ }
468
+
469
+ if (config.isDirectory) {
470
+ // Skills
471
+ return path.join(baseDir, item.directory);
472
+ } else {
473
+ // Agents/Commands
474
+ return path.join(baseDir, item.path);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 获取同步统计信息
480
+ */
481
+ getStats(projectPath = null) {
482
+ const globalConfigs = this.getAvailableConfigs('global');
483
+ const workspaceConfigs = projectPath
484
+ ? this.getAvailableConfigs('workspace', projectPath)
485
+ : { skills: [], agents: [], commands: [] };
486
+
487
+ return {
488
+ global: {
489
+ skills: globalConfigs.skills.length,
490
+ agents: globalConfigs.agents.length,
491
+ commands: globalConfigs.commands.length
492
+ },
493
+ workspace: {
494
+ agents: workspaceConfigs.agents.length,
495
+ commands: workspaceConfigs.commands.length
496
+ }
497
+ };
498
+ }
499
+ }
500
+
501
+ module.exports = {
502
+ ConfigSyncService,
503
+ CONFIG_TYPES
504
+ };