@adversity/coding-tool-x 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +15 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +39 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/paths.js +105 -33
  49. package/src/index.js +64 -3
  50. package/src/plugins/constants.js +3 -2
  51. package/src/plugins/plugin-api.js +1 -1
  52. package/src/reset-config.js +4 -2
  53. package/src/server/api/agents.js +57 -14
  54. package/src/server/api/channels.js +112 -33
  55. package/src/server/api/codex-channels.js +111 -18
  56. package/src/server/api/codex-proxy.js +14 -8
  57. package/src/server/api/commands.js +71 -18
  58. package/src/server/api/config-export.js +0 -6
  59. package/src/server/api/config-registry.js +11 -3
  60. package/src/server/api/config.js +376 -5
  61. package/src/server/api/convert.js +133 -0
  62. package/src/server/api/dashboard.js +22 -6
  63. package/src/server/api/gemini-channels.js +107 -18
  64. package/src/server/api/gemini-proxy.js +14 -8
  65. package/src/server/api/gemini-sessions.js +1 -1
  66. package/src/server/api/health-check.js +4 -3
  67. package/src/server/api/mcp.js +3 -3
  68. package/src/server/api/opencode-channels.js +419 -0
  69. package/src/server/api/opencode-projects.js +99 -0
  70. package/src/server/api/opencode-proxy.js +198 -0
  71. package/src/server/api/opencode-sessions.js +403 -0
  72. package/src/server/api/opencode-statistics.js +57 -0
  73. package/src/server/api/plugins.js +66 -19
  74. package/src/server/api/prompts.js +2 -2
  75. package/src/server/api/proxy.js +7 -4
  76. package/src/server/api/sessions.js +3 -0
  77. package/src/server/api/skills.js +69 -18
  78. package/src/server/api/workspaces.js +78 -6
  79. package/src/server/codex-proxy-server.js +30 -18
  80. package/src/server/dev-server.js +1 -1
  81. package/src/server/gemini-proxy-server.js +15 -3
  82. package/src/server/index.js +165 -58
  83. package/src/server/opencode-proxy-server.js +4375 -0
  84. package/src/server/proxy-server.js +27 -18
  85. package/src/server/services/agents-service.js +61 -24
  86. package/src/server/services/channel-scheduler.js +9 -5
  87. package/src/server/services/channels.js +64 -37
  88. package/src/server/services/codex-channels.js +56 -43
  89. package/src/server/services/codex-settings-manager.js +271 -49
  90. package/src/server/services/codex-statistics-service.js +2 -2
  91. package/src/server/services/commands-service.js +84 -25
  92. package/src/server/services/config-export-service.js +7 -45
  93. package/src/server/services/config-registry-service.js +63 -17
  94. package/src/server/services/config-sync-manager.js +160 -7
  95. package/src/server/services/config-templates-service.js +204 -51
  96. package/src/server/services/env-checker.js +26 -12
  97. package/src/server/services/env-manager.js +126 -18
  98. package/src/server/services/favorites.js +5 -3
  99. package/src/server/services/gemini-channels.js +33 -44
  100. package/src/server/services/gemini-statistics-service.js +2 -2
  101. package/src/server/services/mcp-service.js +350 -9
  102. package/src/server/services/model-detector.js +707 -221
  103. package/src/server/services/network-access.js +80 -0
  104. package/src/server/services/opencode-channels.js +206 -0
  105. package/src/server/services/opencode-gateway-converter.js +639 -0
  106. package/src/server/services/opencode-sessions.js +663 -0
  107. package/src/server/services/opencode-settings-manager.js +342 -0
  108. package/src/server/services/opencode-statistics-service.js +255 -0
  109. package/src/server/services/plugins-service.js +479 -22
  110. package/src/server/services/prompts-service.js +53 -11
  111. package/src/server/services/proxy-runtime.js +1 -1
  112. package/src/server/services/repo-scanner-base.js +1 -1
  113. package/src/server/services/security-config.js +1 -1
  114. package/src/server/services/session-cache.js +1 -1
  115. package/src/server/services/skill-service.js +300 -46
  116. package/src/server/services/speed-test.js +464 -186
  117. package/src/server/services/statistics-service.js +2 -2
  118. package/src/server/services/terminal-commands.js +10 -3
  119. package/src/server/services/terminal-config.js +1 -1
  120. package/src/server/services/ui-config.js +1 -1
  121. package/src/server/services/workspace-service.js +57 -100
  122. package/src/server/websocket-server.js +132 -3
  123. package/src/ui/menu.js +49 -40
  124. package/src/utils/port-helper.js +22 -8
  125. package/src/utils/session.js +5 -4
  126. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  127. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  128. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  129. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  130. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  131. package/src/server/api/oauth.js +0 -294
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/config/oauth-providers.js +0 -68
  134. package/src/server/services/oauth-callback-server.js +0 -284
  135. package/src/server/services/oauth-service.js +0 -378
  136. package/src/server/services/oauth-token-storage.js +0 -135
  137. package/src/server/services/permission-templates-service.js +0 -308
@@ -4,24 +4,28 @@
4
4
  * Manages file synchronization between cc-tool central storage and CLI directories:
5
5
  * - Claude Code: ~/.claude/{skills,commands,agents,rules}/
6
6
  * - Codex CLI: ~/.codex/skills/, ~/.codex/prompts/
7
+ * - OpenCode CLI: ~/.config/opencode/{skills,commands,agents,plugins}/
7
8
  *
8
9
  * Config types:
9
10
  * - skills: directory-based (each skill is a dir with SKILL.md)
10
11
  * - commands: file-based (.md), may be nested in subdirectories
11
12
  * - agents: file-based (.md), flat directory
12
13
  * - rules: file-based (.md), may be nested in subdirectories
14
+ * - plugins: directory-based
13
15
  */
14
16
 
15
17
  const fs = require('fs');
16
18
  const path = require('path');
17
19
  const os = require('os');
18
20
  const { convertSkillToCodex, convertCommandToCodex } = require('./format-converter');
21
+ const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('../../config/paths');
19
22
 
20
23
  // Paths
21
24
  const HOME = os.homedir();
22
- const CC_TOOL_CONFIGS = path.join(HOME, '.claude', 'cc-tool', 'configs');
25
+ const CC_TOOL_CONFIGS = path.join(PATHS.base, 'configs');
23
26
  const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
24
27
  const CODEX_DIR = path.join(HOME, '.codex');
28
+ const OPENCODE_DIR = NATIVE_PATHS.opencode.config;
25
29
 
26
30
  // Config type definitions
27
31
  const CONFIG_TYPES = {
@@ -31,7 +35,10 @@ const CONFIG_TYPES = {
31
35
  claudeTarget: 'skills',
32
36
  codexTarget: 'skills',
33
37
  codexSupported: true,
34
- convertForCodex: true
38
+ convertForCodex: true,
39
+ opencodeTarget: 'skills',
40
+ opencodeLegacyTarget: 'skill',
41
+ opencodeSupported: true
35
42
  },
36
43
  commands: {
37
44
  isDirectory: false,
@@ -39,27 +46,44 @@ const CONFIG_TYPES = {
39
46
  claudeTarget: 'commands',
40
47
  codexTarget: 'prompts',
41
48
  codexSupported: true,
42
- convertForCodex: true
49
+ convertForCodex: true,
50
+ opencodeTarget: 'commands',
51
+ opencodeLegacyTarget: 'command',
52
+ opencodeSupported: true
43
53
  },
44
54
  agents: {
45
55
  isDirectory: false,
46
56
  extension: '.md',
47
57
  claudeTarget: 'agents',
48
- codexSupported: false
58
+ codexSupported: false,
59
+ opencodeTarget: 'agents',
60
+ opencodeLegacyTarget: 'agent',
61
+ opencodeSupported: true
49
62
  },
50
63
  rules: {
51
64
  isDirectory: false,
52
65
  extension: '.md',
53
66
  claudeTarget: 'rules',
54
- codexSupported: false
67
+ codexSupported: false,
68
+ opencodeSupported: false
69
+ },
70
+ plugins: {
71
+ isDirectory: true,
72
+ claudeTarget: 'plugins',
73
+ codexSupported: false,
74
+ opencodeTarget: 'plugins',
75
+ opencodeLegacyTarget: 'plugin',
76
+ opencodeSupported: true
55
77
  }
56
78
  };
57
79
 
58
80
  class ConfigSyncManager {
59
81
  constructor() {
82
+ ensureStorageDirMigrated();
60
83
  this.ccToolConfigs = CC_TOOL_CONFIGS;
61
84
  this.claudeDir = CLAUDE_CODE_DIR;
62
85
  this.codexDir = CODEX_DIR;
86
+ this.opencodeDir = OPENCODE_DIR;
63
87
  this.configTypes = CONFIG_TYPES;
64
88
  }
65
89
 
@@ -262,10 +286,96 @@ class ConfigSyncManager {
262
286
  }
263
287
  }
264
288
 
289
+ /**
290
+ * Sync a config item to OpenCode CLI
291
+ * Supports skills, commands, agents, plugins
292
+ * @param {string} type - Config type
293
+ * @param {string} name - Item name
294
+ * @returns {Object} Result with success status
295
+ */
296
+ syncToOpenCode(type, name) {
297
+ const config = this.configTypes[type];
298
+ if (!config) {
299
+ return { success: false, error: `Unknown config type: ${type}` };
300
+ }
301
+
302
+ if (!config.opencodeSupported) {
303
+ console.log(`[ConfigSyncManager] ${type} not supported by OpenCode, skipping`);
304
+ return { success: true, skipped: true, reason: 'Not supported by OpenCode' };
305
+ }
306
+
307
+ const sourcePath = path.join(this.ccToolConfigs, type, name);
308
+ if (!fs.existsSync(sourcePath)) {
309
+ console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
310
+ return { success: false, error: 'Source not found' };
311
+ }
312
+
313
+ try {
314
+ const targetBaseDir = this._getOpenCodeTypeBaseDir(config);
315
+ const targetPath = path.join(targetBaseDir, name);
316
+
317
+ if (config.isDirectory) {
318
+ this._ensureDir(path.dirname(targetPath));
319
+ this._copyDirRecursive(sourcePath, targetPath);
320
+ console.log(`[ConfigSyncManager] Synced ${type}/${name} to OpenCode (directory)`);
321
+ } else {
322
+ this._ensureDir(path.dirname(targetPath));
323
+ this._copyFile(sourcePath, targetPath);
324
+ console.log(`[ConfigSyncManager] Synced ${type}/${name} to OpenCode (file)`);
325
+ }
326
+
327
+ return { success: true, target: targetPath };
328
+ } catch (err) {
329
+ console.error(`[ConfigSyncManager] Sync to OpenCode failed:`, err.message);
330
+ return { success: false, error: err.message };
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Remove a config item from OpenCode CLI
336
+ * @param {string} type - Config type
337
+ * @param {string} name - Item name
338
+ * @returns {Object} Result with success status
339
+ */
340
+ removeFromOpenCode(type, name) {
341
+ const config = this.configTypes[type];
342
+ if (!config) {
343
+ return { success: false, error: `Unknown config type: ${type}` };
344
+ }
345
+
346
+ if (!config.opencodeSupported) {
347
+ return { success: true, skipped: true, reason: 'Not supported by OpenCode' };
348
+ }
349
+
350
+ const targetBaseDir = this._getOpenCodeTypeBaseDir(config);
351
+ const targetPath = path.join(targetBaseDir, name);
352
+
353
+ if (!fs.existsSync(targetPath)) {
354
+ console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
355
+ return { success: true, message: 'Already removed' };
356
+ }
357
+
358
+ try {
359
+ if (config.isDirectory) {
360
+ this._removeRecursive(targetPath);
361
+ console.log(`[ConfigSyncManager] Removed ${type}/${name} from OpenCode (directory)`);
362
+ } else {
363
+ fs.unlinkSync(targetPath);
364
+ console.log(`[ConfigSyncManager] Removed ${type}/${name} from OpenCode (file)`);
365
+ this._cleanupEmptyParents(path.dirname(targetPath), targetBaseDir);
366
+ }
367
+
368
+ return { success: true };
369
+ } catch (err) {
370
+ console.error(`[ConfigSyncManager] Remove from OpenCode failed:`, err.message);
371
+ return { success: false, error: err.message };
372
+ }
373
+ }
374
+
265
375
  /**
266
376
  * Batch sync based on registry data
267
377
  * @param {string} type - Config type
268
- * @param {Object} registryItems - Registry items { name: { enabled, platforms: { claude, codex } } }
378
+ * @param {Object} registryItems - Registry items { name: { enabled, platforms: { claude, codex, opencode } } }
269
379
  * @returns {Object} Results summary
270
380
  */
271
381
  syncAll(type, registryItems) {
@@ -319,6 +429,20 @@ class ConfigSyncManager {
319
429
  results.removed.push({ type, name, platform: 'codex' });
320
430
  }
321
431
  }
432
+
433
+ if (platforms.opencode) {
434
+ const result = this.syncToOpenCode(type, name);
435
+ if (result.success && !result.skipped) {
436
+ results.synced.push({ type, name, platform: 'opencode' });
437
+ } else if (!result.success) {
438
+ results.errors.push({ type, name, platform: 'opencode', error: result.error });
439
+ }
440
+ } else {
441
+ const result = this.removeFromOpenCode(type, name);
442
+ if (result.success && !result.message && !result.skipped) {
443
+ results.removed.push({ type, name, platform: 'opencode' });
444
+ }
445
+ }
322
446
  } else {
323
447
  // Item disabled, remove from all platforms
324
448
  const claudeResult = this.removeFromClaude(type, name);
@@ -330,6 +454,11 @@ class ConfigSyncManager {
330
454
  if (codexResult.success && !codexResult.message && !codexResult.skipped) {
331
455
  results.removed.push({ type, name, platform: 'codex' });
332
456
  }
457
+
458
+ const opencodeResult = this.removeFromOpenCode(type, name);
459
+ if (opencodeResult.success && !opencodeResult.message && !opencodeResult.skipped) {
460
+ results.removed.push({ type, name, platform: 'opencode' });
461
+ }
333
462
  }
334
463
  }
335
464
 
@@ -409,6 +538,29 @@ class ConfigSyncManager {
409
538
  fs.copyFileSync(src, dest);
410
539
  }
411
540
 
541
+ /**
542
+ * Resolve OpenCode base target directory for a config type.
543
+ * OpenCode supports both plural (new) and singular (legacy) folder names.
544
+ */
545
+ _getOpenCodeTypeBaseDir(config) {
546
+ const modernDir = path.join(this.opencodeDir, config.opencodeTarget);
547
+ // 技能目录强制使用 modern/plural 形式,避免 legacy 目录带来的跨平台历史污染
548
+ if (config === this.configTypes.skills) {
549
+ return modernDir;
550
+ }
551
+
552
+ if (!config.opencodeLegacyTarget) {
553
+ return modernDir;
554
+ }
555
+
556
+ const legacyDir = path.join(this.opencodeDir, config.opencodeLegacyTarget);
557
+ if (fs.existsSync(legacyDir) && !fs.existsSync(modernDir)) {
558
+ return legacyDir;
559
+ }
560
+
561
+ return modernDir;
562
+ }
563
+
412
564
  /**
413
565
  * Recursively remove a file or directory
414
566
  */
@@ -452,5 +604,6 @@ module.exports = {
452
604
  CONFIG_TYPES,
453
605
  CC_TOOL_CONFIGS,
454
606
  CLAUDE_CODE_DIR,
455
- CODEX_DIR
607
+ CODEX_DIR,
608
+ OPENCODE_DIR
456
609
  };
@@ -22,7 +22,7 @@ const pluginsService = new PluginsService();
22
22
  const TEMPLATES_FILE = path.join(PATHS.config, 'config-templates.json');
23
23
 
24
24
  // 内置配置模板
25
- // aiConfigs 结构: { claude: { enabled, content }, codex: { enabled, content }, gemini: { enabled, content } }
25
+ // aiConfigs 结构: { claude: { enabled, content }, codex: { enabled, content }, gemini: { enabled, content }, opencode: { enabled, content } }
26
26
  const BUILTIN_TEMPLATES = [
27
27
  {
28
28
  id: 'full-stack',
@@ -492,6 +492,57 @@ function ensureDir(dirPath) {
492
492
  }
493
493
  }
494
494
 
495
+ function normalizeAiConfigs(aiConfigs = {}, claudeMd = null) {
496
+ const normalized = {
497
+ claude: { enabled: false, content: '' },
498
+ codex: { enabled: false, content: '' },
499
+ gemini: { enabled: false, content: '' },
500
+ opencode: { enabled: false, content: '' }
501
+ };
502
+
503
+ for (const key of Object.keys(normalized)) {
504
+ const cfg = aiConfigs?.[key];
505
+ if (cfg && typeof cfg === 'object') {
506
+ normalized[key] = {
507
+ enabled: !!cfg.enabled,
508
+ content: cfg.content || ''
509
+ };
510
+ }
511
+ }
512
+
513
+ if (claudeMd?.enabled && claudeMd?.content && !normalized.claude.content) {
514
+ normalized.claude = {
515
+ enabled: true,
516
+ content: claudeMd.content
517
+ };
518
+ }
519
+
520
+ // OpenCode defaults to Codex profile if not explicitly configured.
521
+ if (!normalized.opencode.content) {
522
+ const fallback = normalized.codex.content ? normalized.codex : normalized.claude;
523
+ normalized.opencode = {
524
+ enabled: !!fallback.enabled,
525
+ content: fallback.content || ''
526
+ };
527
+ }
528
+
529
+ return normalized;
530
+ }
531
+
532
+ function normalizeTemplate(template) {
533
+ if (!template || typeof template !== 'object') {
534
+ return template;
535
+ }
536
+
537
+ const normalized = { ...template };
538
+ normalized.aiConfigs = normalizeAiConfigs(template.aiConfigs, template.claudeMd);
539
+ if (!normalized.claudeMd) {
540
+ normalized.claudeMd = { enabled: false, content: '' };
541
+ }
542
+
543
+ return normalized;
544
+ }
545
+
495
546
  /**
496
547
  * 加载配置模板
497
548
  */
@@ -502,8 +553,8 @@ function loadTemplates() {
502
553
  const data = JSON.parse(content);
503
554
  // 合并内置模板和用户模板
504
555
  return {
505
- builtin: BUILTIN_TEMPLATES,
506
- custom: data.custom || []
556
+ builtin: BUILTIN_TEMPLATES.map(normalizeTemplate),
557
+ custom: (data.custom || []).map(normalizeTemplate)
507
558
  };
508
559
  }
509
560
  } catch (error) {
@@ -511,7 +562,7 @@ function loadTemplates() {
511
562
  }
512
563
 
513
564
  return {
514
- builtin: BUILTIN_TEMPLATES,
565
+ builtin: BUILTIN_TEMPLATES.map(normalizeTemplate),
515
566
  custom: []
516
567
  };
517
568
  }
@@ -569,6 +620,7 @@ function createCustomTemplate(template) {
569
620
  name: template.name,
570
621
  description: template.description || '',
571
622
  claudeMd: template.claudeMd || { enabled: false, content: '' },
623
+ aiConfigs: normalizeAiConfigs(template.aiConfigs, template.claudeMd),
572
624
  skills: template.skills || [],
573
625
  rules: template.rules || [],
574
626
  commands: template.commands || [],
@@ -579,10 +631,10 @@ function createCustomTemplate(template) {
579
631
  createdAt: new Date().toISOString()
580
632
  };
581
633
 
582
- custom.push(newTemplate);
634
+ custom.push(normalizeTemplate(newTemplate));
583
635
  saveCustomTemplates(custom);
584
636
 
585
- return newTemplate;
637
+ return normalizeTemplate(newTemplate);
586
638
  }
587
639
 
588
640
  /**
@@ -599,6 +651,7 @@ function updateCustomTemplate(id, updates) {
599
651
  custom[index] = {
600
652
  ...custom[index],
601
653
  ...updates,
654
+ aiConfigs: normalizeAiConfigs(updates.aiConfigs || custom[index].aiConfigs, updates.claudeMd || custom[index].claudeMd),
602
655
  id: custom[index].id, // 保持 ID 不变
603
656
  isBuiltin: false,
604
657
  updatedAt: new Date().toISOString()
@@ -817,12 +870,51 @@ function generateRuleContent(rule) {
817
870
  return content + (rule.body || '');
818
871
  }
819
872
 
873
+ /**
874
+ * 转换为 OpenCode MCP 结构(local/remote)
875
+ */
876
+ function convertToOpenCodeMcpSpec(spec = {}) {
877
+ const type = spec.type || 'stdio';
878
+
879
+ if (type === 'local' || type === 'remote') {
880
+ return { ...spec };
881
+ }
882
+
883
+ if (type === 'stdio') {
884
+ const command = [];
885
+ if (spec.command) command.push(spec.command);
886
+ if (Array.isArray(spec.args)) command.push(...spec.args);
887
+
888
+ const result = {
889
+ type: 'local',
890
+ command
891
+ };
892
+
893
+ if (spec.env && typeof spec.env === 'object') {
894
+ result.environment = spec.env;
895
+ }
896
+ if (spec.cwd) {
897
+ result.cwd = spec.cwd;
898
+ }
899
+ return result;
900
+ }
901
+
902
+ const result = {
903
+ type: 'remote',
904
+ url: spec.url || ''
905
+ };
906
+ if (spec.headers && typeof spec.headers === 'object') {
907
+ result.headers = spec.headers;
908
+ }
909
+ return result;
910
+ }
911
+
820
912
  /**
821
913
  * 应用模板到项目目录(完整应用,写入实际文件)
822
914
  * @param {string} targetDir - 目标项目目录
823
915
  * @param {string} templateId - 模板 ID
824
916
  * @param {object} options - 可选配置
825
- * @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini']
917
+ * @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini', 'opencode']
826
918
  * @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
827
919
  */
828
920
  function applyTemplateToProject(targetDir, templateId, options = {}) {
@@ -856,7 +948,8 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
856
948
  const aiConfigMap = {
857
949
  claude: { fileName: 'CLAUDE.md', name: 'Claude' },
858
950
  codex: { fileName: 'AGENTS.md', name: 'Codex' },
859
- gemini: { fileName: 'GEMINI.md', name: 'Gemini' }
951
+ gemini: { fileName: 'GEMINI.md', name: 'Gemini' },
952
+ opencode: { fileName: '.opencode/AGENTS.md', name: 'OpenCode' }
860
953
  };
861
954
 
862
955
  // 遍历所有选中的 AI 配置类型
@@ -872,6 +965,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
872
965
  if (aiConfig?.enabled && aiConfig?.content) {
873
966
  const configInfo = aiConfigMap[aiConfigType];
874
967
  const configPath = path.join(targetDir, configInfo.fileName);
968
+ ensureDir(path.dirname(configPath));
875
969
  fs.writeFileSync(configPath, aiConfig.content, 'utf-8');
876
970
  results.aiConfigs.push({ applied: true, path: configInfo.fileName, type: configInfo.name, key: aiConfigType });
877
971
  }
@@ -879,34 +973,54 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
879
973
 
880
974
  // 2. 写入 Agents
881
975
  if (template.agents?.length > 0) {
882
- const agentsDir = path.join(targetDir, '.claude', 'agents');
883
- ensureDir(agentsDir);
976
+ const agentTargets = [
977
+ { baseDir: path.join(targetDir, '.claude', 'agents'), prefix: '.claude/agents' },
978
+ { baseDir: path.join(targetDir, '.opencode', 'agents'), prefix: '.opencode/agents' }
979
+ ];
980
+
981
+ for (const target of agentTargets) {
982
+ ensureDir(target.baseDir);
983
+ }
984
+
884
985
  for (const agent of template.agents) {
885
986
  const content = generateAgentContent(agent);
886
987
  const fileName = agent.fileName || agent.name.toLowerCase().replace(/\s+/g, '-');
887
- const filePath = path.join(agentsDir, `${fileName}.md`);
888
- fs.writeFileSync(filePath, content, 'utf-8');
889
- results.agents.files.push(`.claude/agents/${fileName}.md`);
988
+ for (const target of agentTargets) {
989
+ const filePath = path.join(target.baseDir, `${fileName}.md`);
990
+ fs.writeFileSync(filePath, content, 'utf-8');
991
+ results.agents.files.push(`${target.prefix}/${fileName}.md`);
992
+ }
890
993
  results.agents.applied++;
891
994
  }
892
995
  }
893
996
 
894
997
  // 3. 写入 Commands
895
998
  if (template.commands?.length > 0) {
896
- const commandsDir = path.join(targetDir, '.claude', 'commands');
897
- ensureDir(commandsDir);
999
+ const commandTargets = [
1000
+ { baseDir: path.join(targetDir, '.claude', 'commands'), prefix: '.claude/commands' },
1001
+ { baseDir: path.join(targetDir, '.opencode', 'commands'), prefix: '.opencode/commands' }
1002
+ ];
1003
+
1004
+ for (const target of commandTargets) {
1005
+ ensureDir(target.baseDir);
1006
+ }
1007
+
898
1008
  for (const command of template.commands) {
899
1009
  const content = generateCommandContent(command);
900
- const targetCmdDir = command.namespace
901
- ? path.join(commandsDir, command.namespace)
902
- : commandsDir;
903
- ensureDir(targetCmdDir);
904
- const filePath = path.join(targetCmdDir, `${command.name}.md`);
905
- fs.writeFileSync(filePath, content, 'utf-8');
906
- const relativePath = command.namespace
907
- ? `.claude/commands/${command.namespace}/${command.name}.md`
908
- : `.claude/commands/${command.name}.md`;
909
- results.commands.files.push(relativePath);
1010
+
1011
+ for (const target of commandTargets) {
1012
+ const targetCmdDir = command.namespace
1013
+ ? path.join(target.baseDir, command.namespace)
1014
+ : target.baseDir;
1015
+ ensureDir(targetCmdDir);
1016
+ const filePath = path.join(targetCmdDir, `${command.name}.md`);
1017
+ fs.writeFileSync(filePath, content, 'utf-8');
1018
+ const relativePath = command.namespace
1019
+ ? `${target.prefix}/${command.namespace}/${command.name}.md`
1020
+ : `${target.prefix}/${command.name}.md`;
1021
+ results.commands.files.push(relativePath);
1022
+ }
1023
+
910
1024
  results.commands.applied++;
911
1025
  }
912
1026
  }
@@ -938,13 +1052,16 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
938
1052
  results.plugins.items = template.plugins.map(p => p.name);
939
1053
  }
940
1054
 
941
- // 6. 写入 MCP 配置到 .mcp.json
942
- if (template.mcpServers?.length > 0) {
1055
+ // 6. 写入 MCP/OpenCode 配置
1056
+ const hasMcp = template.mcpServers?.length > 0;
1057
+ const hasPlugins = template.plugins?.length > 0;
1058
+ if (hasMcp || hasPlugins) {
943
1059
  const mcpConfig = { mcpServers: {} };
1060
+ const opencodeConfig = { mcp: {}, plugin: [] };
944
1061
  const allServers = mcpService.getAllServers();
945
1062
  const presets = mcpService.getPresets();
946
1063
 
947
- for (const serverId of template.mcpServers) {
1064
+ for (const serverId of template.mcpServers || []) {
948
1065
  // 先从已配置的服务器中查找
949
1066
  let serverSpec = allServers[serverId]?.server;
950
1067
  // 如果没有,从预设中查找
@@ -956,6 +1073,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
956
1073
  }
957
1074
  if (serverSpec) {
958
1075
  mcpConfig.mcpServers[serverId] = serverSpec;
1076
+ opencodeConfig.mcp[serverId] = convertToOpenCodeMcpSpec(serverSpec);
959
1077
  results.mcpServers.applied++;
960
1078
  }
961
1079
  }
@@ -964,6 +1082,16 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
964
1082
  const mcpPath = path.join(targetDir, '.mcp.json');
965
1083
  fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
966
1084
  }
1085
+
1086
+ if (hasPlugins) {
1087
+ opencodeConfig.plugin = (template.plugins || []).map(p => p.name).filter(Boolean);
1088
+ }
1089
+ if (Object.keys(opencodeConfig.mcp).length > 0 || opencodeConfig.plugin.length > 0) {
1090
+ const opencodeDir = path.join(targetDir, '.opencode');
1091
+ ensureDir(opencodeDir);
1092
+ const opencodePath = path.join(opencodeDir, 'opencode.json');
1093
+ fs.writeFileSync(opencodePath, JSON.stringify(opencodeConfig, null, 2), 'utf-8');
1094
+ }
967
1095
  }
968
1096
 
969
1097
  // 7. 创建配置记录文件
@@ -995,7 +1123,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
995
1123
  * @param {string} targetDir - 目标项目目录
996
1124
  * @param {string} templateId - 模板 ID
997
1125
  * @param {object} options - 可选配置
998
- * @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini']
1126
+ * @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini', 'opencode']
999
1127
  * @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
1000
1128
  */
1001
1129
  function previewTemplateApplication(targetDir, templateId, options = {}) {
@@ -1031,7 +1159,8 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
1031
1159
  const aiConfigMap = {
1032
1160
  claude: { fileName: 'CLAUDE.md', name: 'Claude' },
1033
1161
  codex: { fileName: 'AGENTS.md', name: 'Codex' },
1034
- gemini: { fileName: 'GEMINI.md', name: 'Gemini' }
1162
+ gemini: { fileName: 'GEMINI.md', name: 'Gemini' },
1163
+ opencode: { fileName: '.opencode/AGENTS.md', name: 'OpenCode' }
1035
1164
  };
1036
1165
 
1037
1166
  // 遍历所有选中的 AI 配置类型
@@ -1064,12 +1193,18 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
1064
1193
  if (template.agents?.length > 0) {
1065
1194
  for (const agent of template.agents) {
1066
1195
  const fileName = agent.fileName || agent.name.toLowerCase().replace(/\s+/g, '-');
1067
- const relativePath = `.claude/agents/${fileName}.md`;
1068
- const fullPath = path.join(targetDir, relativePath);
1069
- if (fs.existsSync(fullPath)) {
1070
- preview.willOverwrite.push(relativePath);
1071
- } else {
1072
- preview.willCreate.push(relativePath);
1196
+ const relativePaths = [
1197
+ `.claude/agents/${fileName}.md`,
1198
+ `.opencode/agents/${fileName}.md`
1199
+ ];
1200
+
1201
+ for (const relativePath of relativePaths) {
1202
+ const fullPath = path.join(targetDir, relativePath);
1203
+ if (fs.existsSync(fullPath)) {
1204
+ preview.willOverwrite.push(relativePath);
1205
+ } else {
1206
+ preview.willCreate.push(relativePath);
1207
+ }
1073
1208
  }
1074
1209
  preview.summary.agents++;
1075
1210
  }
@@ -1078,14 +1213,22 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
1078
1213
  // 检查 Commands
1079
1214
  if (template.commands?.length > 0) {
1080
1215
  for (const command of template.commands) {
1081
- const relativePath = command.namespace
1082
- ? `.claude/commands/${command.namespace}/${command.name}.md`
1083
- : `.claude/commands/${command.name}.md`;
1084
- const fullPath = path.join(targetDir, relativePath);
1085
- if (fs.existsSync(fullPath)) {
1086
- preview.willOverwrite.push(relativePath);
1087
- } else {
1088
- preview.willCreate.push(relativePath);
1216
+ const relativePaths = [
1217
+ command.namespace
1218
+ ? `.claude/commands/${command.namespace}/${command.name}.md`
1219
+ : `.claude/commands/${command.name}.md`,
1220
+ command.namespace
1221
+ ? `.opencode/commands/${command.namespace}/${command.name}.md`
1222
+ : `.opencode/commands/${command.name}.md`
1223
+ ];
1224
+
1225
+ for (const relativePath of relativePaths) {
1226
+ const fullPath = path.join(targetDir, relativePath);
1227
+ if (fs.existsSync(fullPath)) {
1228
+ preview.willOverwrite.push(relativePath);
1229
+ } else {
1230
+ preview.willCreate.push(relativePath);
1231
+ }
1089
1232
  }
1090
1233
  preview.summary.commands++;
1091
1234
  }
@@ -1107,15 +1250,25 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
1107
1250
  }
1108
1251
  }
1109
1252
 
1110
- // 检查 MCP
1111
- if (template.mcpServers?.length > 0) {
1112
- const mcpPath = path.join(targetDir, '.mcp.json');
1113
- if (fs.existsSync(mcpPath)) {
1114
- preview.willOverwrite.push('.mcp.json');
1253
+ // 检查 MCP / OpenCode 配置
1254
+ if (template.mcpServers?.length > 0 || template.plugins?.length > 0) {
1255
+ if (template.mcpServers?.length > 0) {
1256
+ const mcpPath = path.join(targetDir, '.mcp.json');
1257
+ if (fs.existsSync(mcpPath)) {
1258
+ preview.willOverwrite.push('.mcp.json');
1259
+ } else {
1260
+ preview.willCreate.push('.mcp.json');
1261
+ }
1262
+ }
1263
+
1264
+ const opencodeConfigPath = path.join(targetDir, '.opencode/opencode.json');
1265
+ if (fs.existsSync(opencodeConfigPath)) {
1266
+ preview.willOverwrite.push('.opencode/opencode.json');
1115
1267
  } else {
1116
- preview.willCreate.push('.mcp.json');
1268
+ preview.willCreate.push('.opencode/opencode.json');
1117
1269
  }
1118
- preview.summary.mcpServers = template.mcpServers.length;
1270
+
1271
+ preview.summary.mcpServers = template.mcpServers?.length || 0;
1119
1272
  }
1120
1273
 
1121
1274
  // 统计 Plugins(插件不写入文件,只记录数量)