@adversity/coding-tool-x 3.0.6 → 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 (133) hide show
  1. package/CHANGELOG.md +38 -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 +92 -13
  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/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/services/permission-templates-service.js +0 -308
@@ -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(插件不写入文件,只记录数量)
@@ -2,7 +2,7 @@
2
2
  * 环境变量检测服务
3
3
  *
4
4
  * 检测系统中可能导致 API 配置冲突的环境变量
5
- * 支持 macOS/Linux 的 shell 配置文件检测
5
+ * 支持 macOS/Linux/Windows 的 shell 配置文件检测
6
6
  */
7
7
 
8
8
  const fs = require('fs');
@@ -43,13 +43,23 @@ const EXACT_SENSITIVE_VARS = [
43
43
  ];
44
44
 
45
45
  // 需要检测的 shell 配置文件
46
- const SHELL_CONFIG_FILES = [
47
- '.bashrc',
48
- '.bash_profile',
49
- '.zshrc',
50
- '.zprofile',
51
- '.profile'
52
- ];
46
+ const SHELL_CONFIG_FILES = process.platform === 'win32'
47
+ ? [
48
+ '.bashrc',
49
+ '.bash_profile',
50
+ '.zshrc',
51
+ '.zprofile',
52
+ '.profile',
53
+ path.join('Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
54
+ path.join('Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
55
+ ]
56
+ : [
57
+ '.bashrc',
58
+ '.bash_profile',
59
+ '.zshrc',
60
+ '.zprofile',
61
+ '.profile'
62
+ ];
53
63
 
54
64
  // 系统级配置文件
55
65
  const SYSTEM_CONFIG_FILES = [
@@ -120,7 +130,7 @@ function checkShellConfigs(keywords) {
120
130
  const homeDir = os.homedir();
121
131
 
122
132
  for (const fileName of SHELL_CONFIG_FILES) {
123
- const filePath = path.join(homeDir, fileName);
133
+ const filePath = path.isAbsolute(fileName) ? fileName : path.join(homeDir, fileName);
124
134
  const fileConflicts = parseConfigFile(filePath, keywords);
125
135
  conflicts.push(...fileConflicts);
126
136
  }
@@ -165,10 +175,14 @@ function parseConfigFile(filePath, keywords) {
165
175
  continue;
166
176
  }
167
177
 
168
- // 匹配 export VAR=value 或 VAR=value 格式
178
+ // 匹配 sh/bash/zsh 的 export VAR=value 或 VAR=value
169
179
  const exportMatch = trimmed.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.*)$/);
170
- if (exportMatch) {
171
- const [, varName, varValue] = exportMatch;
180
+ // 匹配 PowerShell 的 $env:VAR = value
181
+ const psMatch = trimmed.match(/^\$env:([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/i);
182
+ const matched = exportMatch || psMatch;
183
+
184
+ if (matched) {
185
+ const [, varName, varValue] = matched;
172
186
 
173
187
  if (matchesKeywords(varName, keywords)) {
174
188
  conflicts.push({
@@ -6,15 +6,77 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const os = require('os');
9
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
10
10
 
11
11
  // 备份目录
12
- const BACKUP_DIR = path.join(os.homedir(), '.claude', 'cc-tool', 'env-backups');
12
+ const BACKUP_DIR = PATHS.envBackups;
13
+
14
+ function ensureParentDir(filePath) {
15
+ const dirPath = path.dirname(filePath);
16
+ if (!fs.existsSync(dirPath)) {
17
+ fs.mkdirSync(dirPath, { recursive: true });
18
+ }
19
+ }
20
+
21
+ function writeFileAtomic(filePath, content) {
22
+ ensureParentDir(filePath);
23
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
24
+
25
+ try {
26
+ fs.writeFileSync(tempPath, content, 'utf-8');
27
+ fs.renameSync(tempPath, filePath);
28
+ } finally {
29
+ if (fs.existsSync(tempPath)) {
30
+ try {
31
+ fs.unlinkSync(tempPath);
32
+ } catch (cleanupErr) {
33
+ // ignore cleanup errors
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ function escapeRegex(value) {
40
+ return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ }
42
+
43
+ function isPowerShellProfile(filePath) {
44
+ return String(filePath || '').toLowerCase().endsWith('.ps1');
45
+ }
46
+
47
+ function matchesVarAssignment(line, varName, usePowerShell) {
48
+ if (!line || !varName) {
49
+ return false;
50
+ }
51
+
52
+ const escaped = escapeRegex(varName);
53
+ const regex = usePowerShell
54
+ ? new RegExp(`^\\s*\\$env:${escaped}\\s*=`, 'i')
55
+ : new RegExp(`^\\s*(?:export\\s+)?${escaped}=`);
56
+ return regex.test(line.trim());
57
+ }
58
+
59
+ function formatExportLine(varName, value, usePowerShell) {
60
+ if (usePowerShell) {
61
+ const escapedValue = String(value ?? '')
62
+ .replace(/`/g, '``')
63
+ .replace(/"/g, '`"');
64
+ return `$env:${varName} = "${escapedValue}"`;
65
+ }
66
+
67
+ const escapedValue = String(value ?? '')
68
+ .replace(/\\/g, '\\\\')
69
+ .replace(/"/g, '\\"')
70
+ .replace(/\$/g, '\\$')
71
+ .replace(/`/g, '\\`');
72
+ return `export ${varName}="${escapedValue}"`;
73
+ }
13
74
 
14
75
  /**
15
76
  * 确保备份目录存在
16
77
  */
17
78
  function ensureBackupDir() {
79
+ ensureStorageDirMigrated();
18
80
  if (!fs.existsSync(BACKUP_DIR)) {
19
81
  fs.mkdirSync(BACKUP_DIR, { recursive: true });
20
82
  }
@@ -112,9 +174,14 @@ function getOriginalValue(conflict) {
112
174
 
113
175
  if (line) {
114
176
  const match = line.match(/^(?:export\s+)?[A-Z_][A-Z0-9_]*=(.*)$/);
115
- if (match) {
177
+ if (match && match[1] !== undefined) {
116
178
  return cleanValue(match[1]);
117
179
  }
180
+
181
+ const psMatch = line.match(/^\s*\$env:[A-Z_][A-Z0-9_]*\s*=\s*(.*)$/i);
182
+ if (psMatch && psMatch[1] !== undefined) {
183
+ return cleanValue(psMatch[1]);
184
+ }
118
185
  }
119
186
  } catch (err) {
120
187
  // 忽略
@@ -162,18 +229,44 @@ function removeVarsFromFile(filePath, vars) {
162
229
  }
163
230
 
164
231
  const content = fs.readFileSync(filePath, 'utf-8');
165
- const lines = content.split('\n');
232
+ const lines = content.split(/\r?\n/);
233
+ const usePowerShell = isPowerShellProfile(filePath);
234
+
235
+ // 收集要删除的行号(优先使用行号,行号不匹配时按变量名回退)
236
+ const lineNumbersToRemove = new Set();
237
+ for (const item of vars) {
238
+ const varName = String(item.varName || '').trim();
239
+ if (!varName) continue;
240
+
241
+ const lineIndex = Number(item.lineNumber) - 1;
242
+ if (
243
+ Number.isInteger(lineIndex) &&
244
+ lineIndex >= 0 &&
245
+ lineIndex < lines.length &&
246
+ matchesVarAssignment(lines[lineIndex], varName, usePowerShell)
247
+ ) {
248
+ lineNumbersToRemove.add(lineIndex);
249
+ continue;
250
+ }
166
251
 
167
- // 收集要删除的行号
168
- const lineNumbersToRemove = new Set(vars.map(v => v.lineNumber));
252
+ const fallbackIndex = lines.findIndex(line => matchesVarAssignment(line, varName, usePowerShell));
253
+ if (fallbackIndex >= 0) {
254
+ lineNumbersToRemove.add(fallbackIndex);
255
+ }
256
+ }
169
257
 
170
- // 过滤掉要删除的行
171
- const newLines = lines.filter((_, index) => !lineNumbersToRemove.has(index + 1));
258
+ if (lineNumbersToRemove.size === 0) {
259
+ console.log(`[EnvManager] No matching vars found in ${filePath}, skip writing`);
260
+ return;
261
+ }
172
262
 
173
- // 写回文件
174
- fs.writeFileSync(filePath, newLines.join('\n'), 'utf-8');
263
+ const newLines = lines.filter((_, index) => !lineNumbersToRemove.has(index));
264
+ const nextContent = newLines.join('\n');
265
+ if (nextContent !== content) {
266
+ writeFileAtomic(filePath, nextContent);
267
+ }
175
268
 
176
- console.log(`[EnvManager] Removed ${vars.length} var(s) from ${filePath}`);
269
+ console.log(`[EnvManager] Removed ${lineNumbersToRemove.size} var(s) from ${filePath}`);
177
270
  }
178
271
 
179
272
  /**
@@ -255,21 +348,36 @@ function restoreFromBackup(backupPath) {
255
348
  * 恢复环境变量到文件
256
349
  */
257
350
  function restoreVarToFile(filePath, varName, value) {
351
+ const usePowerShell = isPowerShellProfile(filePath);
352
+ const exportLine = formatExportLine(varName, value, usePowerShell);
258
353
  let content = '';
259
354
 
260
355
  if (fs.existsSync(filePath)) {
261
356
  content = fs.readFileSync(filePath, 'utf-8');
262
- // 确保末尾有换行
263
- if (!content.endsWith('\n')) {
264
- content += '\n';
357
+ }
358
+
359
+ const lines = content ? content.split(/\r?\n/) : [];
360
+ let replaced = false;
361
+
362
+ for (let i = 0; i < lines.length; i++) {
363
+ if (matchesVarAssignment(lines[i], varName, usePowerShell)) {
364
+ lines[i] = exportLine;
365
+ replaced = true;
265
366
  }
266
367
  }
267
368
 
268
- // 添加环境变量
269
- const exportLine = `export ${varName}="${value}"`;
270
- content += exportLine + '\n';
369
+ if (!replaced) {
370
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
371
+ lines.pop();
372
+ }
373
+ if (lines.length > 0) {
374
+ lines.push('');
375
+ }
376
+ lines.push(exportLine);
377
+ }
271
378
 
272
- fs.writeFileSync(filePath, content, 'utf-8');
379
+ const nextContent = `${lines.join('\n')}\n`;
380
+ writeFileAtomic(filePath, nextContent);
273
381
 
274
382
  console.log(`[EnvManager] Restored ${varName} to ${filePath}`);
275
383
  }