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.
- package/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置模板服务
|
|
3
|
+
*
|
|
4
|
+
* 管理工作区/项目的配置模板组合
|
|
5
|
+
* 支持 CLAUDE.md, skills, commands, agents, MCP 等的预设组合
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { PATHS } = require('../../config/paths');
|
|
11
|
+
const { AgentsService } = require('./agents-service');
|
|
12
|
+
const { CommandsService } = require('./commands-service');
|
|
13
|
+
const { SkillService } = require('./skill-service');
|
|
14
|
+
const { PluginsService } = require('./plugins-service');
|
|
15
|
+
const { convertCommandToCodex } = require('./format-converter');
|
|
16
|
+
const mcpService = require('./mcp-service');
|
|
17
|
+
const pluginsService = new PluginsService();
|
|
18
|
+
|
|
19
|
+
// 配置模板文件路径
|
|
20
|
+
const TEMPLATES_FILE = path.join(PATHS.config, 'config-templates.json');
|
|
21
|
+
const AI_CONFIG_MAP = {
|
|
22
|
+
claude: { fileName: 'CLAUDE.md', name: 'Claude' },
|
|
23
|
+
codex: { fileName: 'AGENTS.md', name: 'Codex' },
|
|
24
|
+
gemini: { fileName: 'GEMINI.md', name: 'Gemini' },
|
|
25
|
+
opencode: { fileName: '.opencode/AGENTS.md', name: 'OpenCode' }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const CLI_DEFAULT_AI_TYPE = {
|
|
29
|
+
claude: 'claude',
|
|
30
|
+
codex: 'codex',
|
|
31
|
+
gemini: 'gemini',
|
|
32
|
+
opencode: 'opencode'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 确保目录存在
|
|
37
|
+
*/
|
|
38
|
+
function ensureDir(dirPath) {
|
|
39
|
+
if (!fs.existsSync(dirPath)) {
|
|
40
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pushSkipped(list, type, item, reason) {
|
|
45
|
+
list.push({ type, item, reason });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getTemplateDefaultAiType(template) {
|
|
49
|
+
if (template?.cliType && CLI_DEFAULT_AI_TYPE[template.cliType]) {
|
|
50
|
+
return CLI_DEFAULT_AI_TYPE[template.cliType];
|
|
51
|
+
}
|
|
52
|
+
return 'claude';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeRequestedAiConfigTypes(options = {}, template = null, skipped = []) {
|
|
56
|
+
let aiConfigTypes = options.aiConfigTypes;
|
|
57
|
+
if (!aiConfigTypes) {
|
|
58
|
+
aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : [getTemplateDefaultAiType(template)];
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(aiConfigTypes)) {
|
|
61
|
+
aiConfigTypes = [aiConfigTypes];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalized = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const rawType of aiConfigTypes) {
|
|
67
|
+
if (typeof rawType !== 'string') {
|
|
68
|
+
pushSkipped(skipped, 'aiConfigType', String(rawType), 'AI 配置类型无效,已忽略');
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const aiType = rawType.trim().toLowerCase();
|
|
72
|
+
if (!aiType) continue;
|
|
73
|
+
if (!AI_CONFIG_MAP[aiType]) {
|
|
74
|
+
pushSkipped(skipped, 'aiConfigType', aiType, `不支持的 AI 配置类型: ${aiType}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!seen.has(aiType)) {
|
|
78
|
+
seen.add(aiType);
|
|
79
|
+
normalized.push(aiType);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (normalized.length === 0) {
|
|
84
|
+
const fallbackType = getTemplateDefaultAiType(template);
|
|
85
|
+
normalized.push(fallbackType);
|
|
86
|
+
pushSkipped(skipped, 'aiConfigType', fallbackType, `未提供有效 AI 配置类型,已回退到默认类型: ${fallbackType}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveAiConfig(template, aiConfigType) {
|
|
93
|
+
if (template?.aiConfigs?.[aiConfigType]) {
|
|
94
|
+
return template.aiConfigs[aiConfigType];
|
|
95
|
+
}
|
|
96
|
+
if (aiConfigType === 'claude' && template?.claudeMd) {
|
|
97
|
+
return template.claudeMd;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveItemName(primary, fallback, defaultPrefix) {
|
|
103
|
+
const raw = (primary || fallback || '').toString().trim();
|
|
104
|
+
if (raw) return raw;
|
|
105
|
+
return `${defaultPrefix}-${Date.now()}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeAiConfigs(aiConfigs = {}, claudeMd = null) {
|
|
109
|
+
const normalized = {
|
|
110
|
+
claude: { enabled: false, content: '' },
|
|
111
|
+
codex: { enabled: false, content: '' },
|
|
112
|
+
gemini: { enabled: false, content: '' },
|
|
113
|
+
opencode: { enabled: false, content: '' }
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const key of Object.keys(normalized)) {
|
|
117
|
+
const cfg = aiConfigs?.[key];
|
|
118
|
+
if (cfg && typeof cfg === 'object') {
|
|
119
|
+
normalized[key] = {
|
|
120
|
+
enabled: !!cfg.enabled,
|
|
121
|
+
content: cfg.content || ''
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (claudeMd?.enabled && claudeMd?.content && !normalized.claude.content) {
|
|
127
|
+
normalized.claude = {
|
|
128
|
+
enabled: true,
|
|
129
|
+
content: claudeMd.content
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// OpenCode defaults to Codex profile if not explicitly configured.
|
|
134
|
+
if (!normalized.opencode.content) {
|
|
135
|
+
const fallback = normalized.codex.content ? normalized.codex : normalized.claude;
|
|
136
|
+
normalized.opencode = {
|
|
137
|
+
enabled: !!fallback.enabled,
|
|
138
|
+
content: fallback.content || ''
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeTemplate(template) {
|
|
146
|
+
if (!template || typeof template !== 'object') {
|
|
147
|
+
return template;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalized = { ...template };
|
|
151
|
+
normalized.aiConfigs = normalizeAiConfigs(template.aiConfigs, template.claudeMd);
|
|
152
|
+
if (!normalized.claudeMd) {
|
|
153
|
+
normalized.claudeMd = { enabled: false, content: '' };
|
|
154
|
+
}
|
|
155
|
+
delete normalized.rules;
|
|
156
|
+
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 加载配置模板
|
|
162
|
+
*/
|
|
163
|
+
function loadTemplates() {
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(TEMPLATES_FILE)) {
|
|
166
|
+
const content = fs.readFileSync(TEMPLATES_FILE, 'utf8');
|
|
167
|
+
const data = JSON.parse(content);
|
|
168
|
+
return {
|
|
169
|
+
custom: (data.custom || []).map(normalizeTemplate)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('加载配置模板失败:', error.message);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
custom: []
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 保存用户自定义模板
|
|
183
|
+
*/
|
|
184
|
+
function saveCustomTemplates(customTemplates) {
|
|
185
|
+
try {
|
|
186
|
+
ensureDir(path.dirname(TEMPLATES_FILE));
|
|
187
|
+
const data = {
|
|
188
|
+
custom: customTemplates,
|
|
189
|
+
updatedAt: new Date().toISOString()
|
|
190
|
+
};
|
|
191
|
+
fs.writeFileSync(TEMPLATES_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
192
|
+
return true;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('保存配置模板失败:', error.message);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 获取所有模板
|
|
201
|
+
*/
|
|
202
|
+
function getAllTemplates() {
|
|
203
|
+
const { custom } = loadTemplates();
|
|
204
|
+
return custom;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 根据 ID 获取模板
|
|
209
|
+
*/
|
|
210
|
+
function getTemplateById(id) {
|
|
211
|
+
const templates = getAllTemplates();
|
|
212
|
+
return templates.find(t => t.id === id);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 创建自定义模板
|
|
217
|
+
*/
|
|
218
|
+
function createCustomTemplate(template) {
|
|
219
|
+
const { custom } = loadTemplates();
|
|
220
|
+
|
|
221
|
+
// 验证必填字段
|
|
222
|
+
if (!template.name || !template.name.trim()) {
|
|
223
|
+
throw new Error('模板名称不能为空');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 生成唯一 ID
|
|
227
|
+
const id = `custom-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
228
|
+
|
|
229
|
+
const newTemplate = {
|
|
230
|
+
id,
|
|
231
|
+
name: template.name,
|
|
232
|
+
description: template.description || '',
|
|
233
|
+
cliType: template.cliType || 'claude',
|
|
234
|
+
claudeMd: template.claudeMd || { enabled: false, content: '' },
|
|
235
|
+
aiConfigs: normalizeAiConfigs(template.aiConfigs, template.claudeMd),
|
|
236
|
+
skills: template.skills || [],
|
|
237
|
+
commands: template.commands || [],
|
|
238
|
+
agents: template.agents || [],
|
|
239
|
+
plugins: template.plugins || [],
|
|
240
|
+
mcpServers: template.mcpServers || [],
|
|
241
|
+
createdAt: new Date().toISOString()
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
custom.push(normalizeTemplate(newTemplate));
|
|
245
|
+
saveCustomTemplates(custom);
|
|
246
|
+
|
|
247
|
+
return normalizeTemplate(newTemplate);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 更新自定义模板
|
|
252
|
+
*/
|
|
253
|
+
function updateCustomTemplate(id, updates) {
|
|
254
|
+
const { custom } = loadTemplates();
|
|
255
|
+
const index = custom.findIndex(t => t.id === id);
|
|
256
|
+
|
|
257
|
+
if (index === -1) {
|
|
258
|
+
throw new Error('模板不存在或不可修改');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
custom[index] = {
|
|
262
|
+
...custom[index],
|
|
263
|
+
...updates,
|
|
264
|
+
cliType: updates.cliType !== undefined ? updates.cliType : (custom[index].cliType || 'claude'),
|
|
265
|
+
aiConfigs: normalizeAiConfigs(updates.aiConfigs || custom[index].aiConfigs, updates.claudeMd || custom[index].claudeMd),
|
|
266
|
+
id: custom[index].id, // 保持 ID 不变
|
|
267
|
+
updatedAt: new Date().toISOString()
|
|
268
|
+
};
|
|
269
|
+
delete custom[index].rules;
|
|
270
|
+
|
|
271
|
+
saveCustomTemplates(custom);
|
|
272
|
+
return custom[index];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 删除自定义模板
|
|
277
|
+
*/
|
|
278
|
+
function deleteCustomTemplate(id) {
|
|
279
|
+
const { custom } = loadTemplates();
|
|
280
|
+
const customIndex = custom.findIndex(t => t.id === id);
|
|
281
|
+
|
|
282
|
+
if (customIndex !== -1) {
|
|
283
|
+
const filtered = custom.filter(t => t.id !== id);
|
|
284
|
+
saveCustomTemplates(filtered);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error('模板不存在或不可删除');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 应用模板到指定目录
|
|
293
|
+
* @param {string} targetDir - 目标目录
|
|
294
|
+
* @param {string} templateId - 模板 ID
|
|
295
|
+
*/
|
|
296
|
+
function applyTemplate(targetDir, templateId) {
|
|
297
|
+
const template = getTemplateById(templateId);
|
|
298
|
+
|
|
299
|
+
if (!template) {
|
|
300
|
+
throw new Error('模板不存在');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 确保目标目录存在
|
|
304
|
+
ensureDir(targetDir);
|
|
305
|
+
|
|
306
|
+
const results = {
|
|
307
|
+
claudeMd: false,
|
|
308
|
+
skills: 0,
|
|
309
|
+
commands: 0,
|
|
310
|
+
agents: 0,
|
|
311
|
+
plugins: 0,
|
|
312
|
+
mcpServers: 0
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// 1. 应用 CLAUDE.md
|
|
316
|
+
if (template.claudeMd && template.claudeMd.enabled && template.claudeMd.content) {
|
|
317
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
318
|
+
fs.writeFileSync(claudeMdPath, template.claudeMd.content, 'utf8');
|
|
319
|
+
results.claudeMd = true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 2. 创建配置记录文件(记录应用了哪个模板)
|
|
323
|
+
const configRecord = {
|
|
324
|
+
templateId: template.id,
|
|
325
|
+
templateName: template.name,
|
|
326
|
+
appliedAt: new Date().toISOString(),
|
|
327
|
+
skills: template.skills,
|
|
328
|
+
commands: template.commands,
|
|
329
|
+
agents: template.agents,
|
|
330
|
+
plugins: template.plugins,
|
|
331
|
+
mcpServers: template.mcpServers
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const recordPath = path.join(targetDir, '.ctx-config.json');
|
|
335
|
+
fs.writeFileSync(recordPath, JSON.stringify(configRecord, null, 2), 'utf8');
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
results,
|
|
340
|
+
template: template.name
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 从目录读取当前配置
|
|
346
|
+
*/
|
|
347
|
+
function readCurrentConfig(targetDir) {
|
|
348
|
+
const recordPath = path.join(targetDir, '.ctx-config.json');
|
|
349
|
+
|
|
350
|
+
if (fs.existsSync(recordPath)) {
|
|
351
|
+
try {
|
|
352
|
+
const content = fs.readFileSync(recordPath, 'utf8');
|
|
353
|
+
return JSON.parse(content);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('读取配置记录失败:', error.message);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// 新增方法:获取可用配置、应用模板到项目、预览应用效果
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 获取所有可用配置(用于模板编辑器选择)
|
|
368
|
+
* 返回用户级的 agents, commands, plugins + MCP 服务器列表
|
|
369
|
+
*/
|
|
370
|
+
function getAvailableConfigs() {
|
|
371
|
+
const agentServices = ['claude', 'codex', 'opencode'].map(platform => new AgentsService(platform));
|
|
372
|
+
const commandServices = ['claude', 'opencode'].map(platform => new CommandsService(platform));
|
|
373
|
+
const skillServices = ['claude', 'codex', 'gemini', 'opencode'].map(platform => new SkillService(platform));
|
|
374
|
+
|
|
375
|
+
const agentMap = new Map();
|
|
376
|
+
const commandMap = new Map();
|
|
377
|
+
const skillMap = new Map();
|
|
378
|
+
|
|
379
|
+
for (const service of agentServices) {
|
|
380
|
+
const { agents } = service.listAgents();
|
|
381
|
+
for (const agent of agents || []) {
|
|
382
|
+
if (agent.scope !== 'user') continue;
|
|
383
|
+
const key = `${agent.fileName || agent.name}|${agent.model || ''}|${agent.description || ''}`;
|
|
384
|
+
if (!agentMap.has(key)) {
|
|
385
|
+
agentMap.set(key, agent);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const service of commandServices) {
|
|
391
|
+
const { commands } = service.listCommands();
|
|
392
|
+
for (const command of commands || []) {
|
|
393
|
+
if (command.scope !== 'user') continue;
|
|
394
|
+
const key = command.namespace ? `${command.namespace}/${command.name}` : command.name;
|
|
395
|
+
if (!commandMap.has(key)) {
|
|
396
|
+
commandMap.set(key, command);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
for (const service of skillServices) {
|
|
402
|
+
const installedSkills = service.getInstalledSkills();
|
|
403
|
+
for (const skill of installedSkills || []) {
|
|
404
|
+
const key = skill.directory || skill.name;
|
|
405
|
+
if (!skillMap.has(key)) {
|
|
406
|
+
skillMap.set(key, skill);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 获取已安装的插件和市场插件
|
|
412
|
+
const { plugins: installedPlugins } = pluginsService.listPlugins();
|
|
413
|
+
|
|
414
|
+
// 获取 MCP 服务器
|
|
415
|
+
const mcpServers = mcpService.getAllServers();
|
|
416
|
+
const mcpServerList = Object.values(mcpServers).map(s => ({
|
|
417
|
+
id: s.id,
|
|
418
|
+
name: s.name || s.id,
|
|
419
|
+
description: s.description || ''
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
// 获取 MCP 预设
|
|
423
|
+
const mcpPresets = mcpService.getPresets().map(p => ({
|
|
424
|
+
id: p.id,
|
|
425
|
+
name: p.name,
|
|
426
|
+
description: p.description
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
skills: Array.from(skillMap.values()).map(skill => ({
|
|
431
|
+
directory: skill.directory,
|
|
432
|
+
name: skill.name || skill.directory,
|
|
433
|
+
description: skill.description || '',
|
|
434
|
+
repoOwner: skill.repoOwner || null,
|
|
435
|
+
repoName: skill.repoName || null,
|
|
436
|
+
repoBranch: skill.repoBranch || null
|
|
437
|
+
})),
|
|
438
|
+
agents: Array.from(agentMap.values()).map(a => ({
|
|
439
|
+
fileName: a.fileName,
|
|
440
|
+
name: a.name,
|
|
441
|
+
description: a.description,
|
|
442
|
+
tools: a.tools,
|
|
443
|
+
model: a.model,
|
|
444
|
+
permissionMode: a.permissionMode,
|
|
445
|
+
skills: a.skills,
|
|
446
|
+
systemPrompt: a.systemPrompt
|
|
447
|
+
})),
|
|
448
|
+
commands: Array.from(commandMap.values()).map(c => ({
|
|
449
|
+
name: c.name,
|
|
450
|
+
namespace: c.namespace,
|
|
451
|
+
description: c.description,
|
|
452
|
+
allowedTools: c.allowedTools,
|
|
453
|
+
argumentHint: c.argumentHint,
|
|
454
|
+
body: c.body
|
|
455
|
+
})),
|
|
456
|
+
plugins: installedPlugins.map(p => ({
|
|
457
|
+
name: p.name,
|
|
458
|
+
description: p.description || '',
|
|
459
|
+
version: p.version || '1.0.0',
|
|
460
|
+
marketplace: p.marketplace || null,
|
|
461
|
+
source: p.source || null,
|
|
462
|
+
repoUrl: p.repoUrl || null
|
|
463
|
+
})),
|
|
464
|
+
mcpServers: mcpServerList,
|
|
465
|
+
mcpPresets
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 生成 Agent 文件内容
|
|
471
|
+
*/
|
|
472
|
+
function generateAgentContent(agent) {
|
|
473
|
+
const lines = ['---'];
|
|
474
|
+
if (agent.name) lines.push(`name: ${agent.name}`);
|
|
475
|
+
if (agent.description) lines.push(`description: "${agent.description}"`);
|
|
476
|
+
if (agent.tools) lines.push(`tools: ${agent.tools}`);
|
|
477
|
+
if (agent.model) lines.push(`model: ${agent.model}`);
|
|
478
|
+
if (agent.permissionMode) lines.push(`permissionMode: ${agent.permissionMode}`);
|
|
479
|
+
if (agent.skills) lines.push(`skills: ${agent.skills}`);
|
|
480
|
+
lines.push('---');
|
|
481
|
+
return lines.join('\n') + '\n\n' + (agent.systemPrompt || '');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 生成 Command 文件内容
|
|
486
|
+
*/
|
|
487
|
+
function generateCommandContent(command) {
|
|
488
|
+
const lines = ['---'];
|
|
489
|
+
if (command.description) lines.push(`description: "${command.description}"`);
|
|
490
|
+
if (command.allowedTools) lines.push(`allowed-tools: ${command.allowedTools}`);
|
|
491
|
+
if (command.argumentHint) lines.push(`argument-hint: ${command.argumentHint}`);
|
|
492
|
+
lines.push('---');
|
|
493
|
+
return lines.join('\n') + '\n\n' + (command.body || '');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 转换为 OpenCode MCP 结构(local/remote)
|
|
498
|
+
*/
|
|
499
|
+
function convertToOpenCodeMcpSpec(spec = {}) {
|
|
500
|
+
const type = spec.type || 'stdio';
|
|
501
|
+
|
|
502
|
+
if (type === 'local' || type === 'remote') {
|
|
503
|
+
return { ...spec };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (type === 'stdio') {
|
|
507
|
+
const command = [];
|
|
508
|
+
if (spec.command) command.push(spec.command);
|
|
509
|
+
if (Array.isArray(spec.args)) command.push(...spec.args);
|
|
510
|
+
|
|
511
|
+
const result = {
|
|
512
|
+
type: 'local',
|
|
513
|
+
command
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
if (spec.env && typeof spec.env === 'object') {
|
|
517
|
+
result.environment = spec.env;
|
|
518
|
+
}
|
|
519
|
+
if (spec.cwd) {
|
|
520
|
+
result.cwd = spec.cwd;
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const result = {
|
|
526
|
+
type: 'remote',
|
|
527
|
+
url: spec.url || ''
|
|
528
|
+
};
|
|
529
|
+
if (spec.headers && typeof spec.headers === 'object') {
|
|
530
|
+
result.headers = spec.headers;
|
|
531
|
+
}
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 应用模板到项目目录(完整应用,写入实际文件)
|
|
537
|
+
* @param {string} targetDir - 目标项目目录
|
|
538
|
+
* @param {string} templateId - 模板 ID
|
|
539
|
+
* @param {object} options - 可选配置
|
|
540
|
+
* @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini', 'opencode']
|
|
541
|
+
* @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
|
|
542
|
+
*/
|
|
543
|
+
function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
544
|
+
const template = getTemplateById(templateId);
|
|
545
|
+
if (!template) {
|
|
546
|
+
throw new Error('模板不存在');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
ensureDir(targetDir);
|
|
550
|
+
|
|
551
|
+
const results = {
|
|
552
|
+
aiConfigs: [],
|
|
553
|
+
skills: { applied: template.skills?.length || 0, items: template.skills?.map(s => s.directory || s.name) || [] },
|
|
554
|
+
agents: { applied: 0, files: [] },
|
|
555
|
+
commands: { applied: 0, files: [] },
|
|
556
|
+
plugins: { applied: 0, items: [] },
|
|
557
|
+
mcpServers: { applied: 0 },
|
|
558
|
+
skipped: []
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const aiConfigTypes = normalizeRequestedAiConfigTypes(options, template, results.skipped);
|
|
562
|
+
|
|
563
|
+
for (const aiConfigType of aiConfigTypes) {
|
|
564
|
+
const aiConfig = resolveAiConfig(template, aiConfigType);
|
|
565
|
+
if (aiConfig?.enabled && aiConfig?.content) {
|
|
566
|
+
const configInfo = AI_CONFIG_MAP[aiConfigType];
|
|
567
|
+
const configPath = path.join(targetDir, configInfo.fileName);
|
|
568
|
+
ensureDir(path.dirname(configPath));
|
|
569
|
+
fs.writeFileSync(configPath, aiConfig.content, 'utf-8');
|
|
570
|
+
results.aiConfigs.push({ applied: true, path: configInfo.fileName, type: configInfo.name, key: aiConfigType });
|
|
571
|
+
} else {
|
|
572
|
+
const fileName = AI_CONFIG_MAP[aiConfigType]?.fileName || aiConfigType;
|
|
573
|
+
pushSkipped(results.skipped, 'aiConfig', fileName, `模板未启用 ${fileName},已跳过`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (template.agents?.length > 0) {
|
|
578
|
+
const agentTargets = [];
|
|
579
|
+
if (aiConfigTypes.includes('claude')) {
|
|
580
|
+
agentTargets.push({ baseDir: path.join(targetDir, '.claude', 'agents'), prefix: '.claude/agents' });
|
|
581
|
+
}
|
|
582
|
+
if (aiConfigTypes.includes('opencode')) {
|
|
583
|
+
agentTargets.push({ baseDir: path.join(targetDir, '.opencode', 'agents'), prefix: '.opencode/agents' });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
for (const target of agentTargets) {
|
|
587
|
+
ensureDir(target.baseDir);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for (const agent of template.agents) {
|
|
591
|
+
const fileName = resolveItemName(agent.fileName, agent.name, 'agent').toLowerCase().replace(/\s+/g, '-');
|
|
592
|
+
const content = generateAgentContent(agent);
|
|
593
|
+
let written = false;
|
|
594
|
+
for (const target of agentTargets) {
|
|
595
|
+
const filePath = path.join(target.baseDir, `${fileName}.md`);
|
|
596
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
597
|
+
results.agents.files.push(`${target.prefix}/${fileName}.md`);
|
|
598
|
+
written = true;
|
|
599
|
+
}
|
|
600
|
+
if (aiConfigTypes.includes('codex')) {
|
|
601
|
+
pushSkipped(results.skipped, 'agent', fileName, 'Codex agents 仅支持用户级配置,项目目录应用时已跳过');
|
|
602
|
+
}
|
|
603
|
+
if (aiConfigTypes.includes('gemini')) {
|
|
604
|
+
pushSkipped(results.skipped, 'agent', fileName, 'Gemini 不支持 agents,已跳过');
|
|
605
|
+
}
|
|
606
|
+
if (written) {
|
|
607
|
+
results.agents.applied++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (template.commands?.length > 0) {
|
|
613
|
+
const commandTargets = [];
|
|
614
|
+
if (aiConfigTypes.includes('claude')) {
|
|
615
|
+
commandTargets.push({ baseDir: path.join(targetDir, '.claude', 'commands'), prefix: '.claude/commands', format: 'claude' });
|
|
616
|
+
}
|
|
617
|
+
if (aiConfigTypes.includes('codex')) {
|
|
618
|
+
commandTargets.push({ baseDir: path.join(targetDir, '.codex', 'prompts'), prefix: '.codex/prompts', format: 'codex' });
|
|
619
|
+
}
|
|
620
|
+
if (aiConfigTypes.includes('opencode')) {
|
|
621
|
+
commandTargets.push({ baseDir: path.join(targetDir, '.opencode', 'commands'), prefix: '.opencode/commands', format: 'claude' });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
for (const target of commandTargets) {
|
|
625
|
+
ensureDir(target.baseDir);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
for (const command of template.commands) {
|
|
629
|
+
const commandName = resolveItemName(command.name, null, 'command');
|
|
630
|
+
let written = false;
|
|
631
|
+
for (const target of commandTargets) {
|
|
632
|
+
let content = generateCommandContent(command);
|
|
633
|
+
if (target.format === 'codex') {
|
|
634
|
+
content = convertCommandToCodex(content).content;
|
|
635
|
+
}
|
|
636
|
+
const targetCmdDir = command.namespace
|
|
637
|
+
? path.join(target.baseDir, command.namespace)
|
|
638
|
+
: target.baseDir;
|
|
639
|
+
ensureDir(targetCmdDir);
|
|
640
|
+
const filePath = path.join(targetCmdDir, `${commandName}.md`);
|
|
641
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
642
|
+
const relativePath = command.namespace
|
|
643
|
+
? `${target.prefix}/${command.namespace}/${commandName}.md`
|
|
644
|
+
: `${target.prefix}/${commandName}.md`;
|
|
645
|
+
results.commands.files.push(relativePath);
|
|
646
|
+
written = true;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (aiConfigTypes.includes('gemini')) {
|
|
650
|
+
pushSkipped(results.skipped, 'command', commandName, 'Gemini 不支持 commands,已跳过');
|
|
651
|
+
}
|
|
652
|
+
if (written) {
|
|
653
|
+
results.commands.applied++;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (template.plugins?.length > 0) {
|
|
659
|
+
results.plugins.items = template.plugins.map(p => p.name);
|
|
660
|
+
if (aiConfigTypes.includes('opencode')) {
|
|
661
|
+
results.plugins.applied = template.plugins.length;
|
|
662
|
+
} else {
|
|
663
|
+
for (const plugin of template.plugins) {
|
|
664
|
+
pushSkipped(results.skipped, 'plugin', plugin.name, '当前未选择 OpenCode,已跳过插件写入');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const hasMcp = template.mcpServers?.length > 0;
|
|
670
|
+
const hasPluginsForOpenCode = aiConfigTypes.includes('opencode') && template.plugins?.length > 0;
|
|
671
|
+
if (hasMcp || hasPluginsForOpenCode) {
|
|
672
|
+
const mcpConfig = { mcpServers: {} };
|
|
673
|
+
const opencodeConfig = { mcp: {}, plugin: [] };
|
|
674
|
+
const allServers = mcpService.getAllServers();
|
|
675
|
+
const presets = mcpService.getPresets();
|
|
676
|
+
|
|
677
|
+
for (const serverId of template.mcpServers || []) {
|
|
678
|
+
// 先从已配置的服务器中查找
|
|
679
|
+
let serverSpec = allServers[serverId]?.server;
|
|
680
|
+
// 如果没有,从预设中查找
|
|
681
|
+
if (!serverSpec) {
|
|
682
|
+
const preset = presets.find(p => p.id === serverId);
|
|
683
|
+
if (preset) {
|
|
684
|
+
serverSpec = preset.server;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (serverSpec) {
|
|
688
|
+
mcpConfig.mcpServers[serverId] = serverSpec;
|
|
689
|
+
opencodeConfig.mcp[serverId] = convertToOpenCodeMcpSpec(serverSpec);
|
|
690
|
+
results.mcpServers.applied++;
|
|
691
|
+
} else {
|
|
692
|
+
pushSkipped(results.skipped, 'mcpServer', serverId, '未找到对应 MCP 服务配置,已跳过');
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
697
|
+
const mcpPath = path.join(targetDir, '.mcp.json');
|
|
698
|
+
fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (hasPluginsForOpenCode) {
|
|
702
|
+
opencodeConfig.plugin = (template.plugins || []).map(p => p.name).filter(Boolean);
|
|
703
|
+
}
|
|
704
|
+
if (aiConfigTypes.includes('opencode') && (Object.keys(opencodeConfig.mcp).length > 0 || opencodeConfig.plugin.length > 0)) {
|
|
705
|
+
const opencodeDir = path.join(targetDir, '.opencode');
|
|
706
|
+
ensureDir(opencodeDir);
|
|
707
|
+
const opencodePath = path.join(opencodeDir, 'opencode.json');
|
|
708
|
+
fs.writeFileSync(opencodePath, JSON.stringify(opencodeConfig, null, 2), 'utf-8');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const configRecord = {
|
|
713
|
+
templateId: template.id,
|
|
714
|
+
templateName: template.name,
|
|
715
|
+
appliedAt: new Date().toISOString(),
|
|
716
|
+
aiConfigTypes: aiConfigTypes,
|
|
717
|
+
aiConfigPaths: results.aiConfigs.map(c => c.path),
|
|
718
|
+
skills: template.skills?.map(s => s.directory || s.name) || [],
|
|
719
|
+
agents: template.agents?.map(a => a.fileName || a.name) || [],
|
|
720
|
+
commands: template.commands?.map(c => c.name) || [],
|
|
721
|
+
plugins: template.plugins?.map(p => p.name) || [],
|
|
722
|
+
mcpServers: template.mcpServers || [],
|
|
723
|
+
skipped: results.skipped
|
|
724
|
+
};
|
|
725
|
+
const recordPath = path.join(targetDir, '.ctx-config.json');
|
|
726
|
+
fs.writeFileSync(recordPath, JSON.stringify(configRecord, null, 2), 'utf-8');
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
success: true,
|
|
730
|
+
results,
|
|
731
|
+
template: template.name
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* 预览模板应用效果
|
|
737
|
+
* @param {string} targetDir - 目标项目目录
|
|
738
|
+
* @param {string} templateId - 模板 ID
|
|
739
|
+
* @param {object} options - 可选配置
|
|
740
|
+
* @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini', 'opencode']
|
|
741
|
+
* @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
|
|
742
|
+
*/
|
|
743
|
+
function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
744
|
+
const template = getTemplateById(templateId);
|
|
745
|
+
if (!template) {
|
|
746
|
+
throw new Error('模板不存在');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const preview = {
|
|
750
|
+
willCreate: [],
|
|
751
|
+
willOverwrite: [],
|
|
752
|
+
skipped: [],
|
|
753
|
+
summary: {
|
|
754
|
+
aiConfigs: [],
|
|
755
|
+
skills: 0,
|
|
756
|
+
agents: 0,
|
|
757
|
+
commands: 0,
|
|
758
|
+
plugins: 0,
|
|
759
|
+
mcpServers: 0,
|
|
760
|
+
skipped: 0
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const aiConfigTypes = normalizeRequestedAiConfigTypes(options, template, preview.skipped);
|
|
765
|
+
|
|
766
|
+
for (const aiConfigType of aiConfigTypes) {
|
|
767
|
+
const aiConfig = resolveAiConfig(template, aiConfigType);
|
|
768
|
+
if (aiConfig?.enabled && aiConfig?.content) {
|
|
769
|
+
const configInfo = AI_CONFIG_MAP[aiConfigType];
|
|
770
|
+
const configPath = path.join(targetDir, configInfo.fileName);
|
|
771
|
+
if (fs.existsSync(configPath)) {
|
|
772
|
+
preview.willOverwrite.push(configInfo.fileName);
|
|
773
|
+
} else {
|
|
774
|
+
preview.willCreate.push(configInfo.fileName);
|
|
775
|
+
}
|
|
776
|
+
preview.summary.aiConfigs.push({ type: aiConfigType, fileName: configInfo.fileName, name: configInfo.name });
|
|
777
|
+
} else {
|
|
778
|
+
const fileName = AI_CONFIG_MAP[aiConfigType]?.fileName || aiConfigType;
|
|
779
|
+
pushSkipped(preview.skipped, 'aiConfig', fileName, `模板未启用 ${fileName},已跳过`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Skills 摘要
|
|
784
|
+
if (template.skills?.length > 0) {
|
|
785
|
+
preview.summary.skills = template.skills.length;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (template.agents?.length > 0) {
|
|
789
|
+
const agentPrefixes = [];
|
|
790
|
+
if (aiConfigTypes.includes('claude')) agentPrefixes.push('.claude/agents');
|
|
791
|
+
if (aiConfigTypes.includes('opencode')) agentPrefixes.push('.opencode/agents');
|
|
792
|
+
|
|
793
|
+
for (const agent of template.agents) {
|
|
794
|
+
const fileName = resolveItemName(agent.fileName, agent.name, 'agent').toLowerCase().replace(/\s+/g, '-');
|
|
795
|
+
let applicable = false;
|
|
796
|
+
for (const prefix of agentPrefixes) {
|
|
797
|
+
const relativePath = `${prefix}/${fileName}.md`;
|
|
798
|
+
const fullPath = path.join(targetDir, relativePath);
|
|
799
|
+
if (fs.existsSync(fullPath)) {
|
|
800
|
+
preview.willOverwrite.push(relativePath);
|
|
801
|
+
} else {
|
|
802
|
+
preview.willCreate.push(relativePath);
|
|
803
|
+
}
|
|
804
|
+
applicable = true;
|
|
805
|
+
}
|
|
806
|
+
if (aiConfigTypes.includes('codex')) {
|
|
807
|
+
pushSkipped(preview.skipped, 'agent', fileName, 'Codex agents 仅支持用户级配置,项目目录预览时已跳过');
|
|
808
|
+
}
|
|
809
|
+
if (aiConfigTypes.includes('gemini')) {
|
|
810
|
+
pushSkipped(preview.skipped, 'agent', fileName, 'Gemini 不支持 agents,已跳过');
|
|
811
|
+
}
|
|
812
|
+
if (applicable) {
|
|
813
|
+
preview.summary.agents++;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (template.commands?.length > 0) {
|
|
819
|
+
const commandPrefixes = [];
|
|
820
|
+
if (aiConfigTypes.includes('claude')) commandPrefixes.push('.claude/commands');
|
|
821
|
+
if (aiConfigTypes.includes('codex')) commandPrefixes.push('.codex/prompts');
|
|
822
|
+
if (aiConfigTypes.includes('opencode')) commandPrefixes.push('.opencode/commands');
|
|
823
|
+
|
|
824
|
+
for (const command of template.commands) {
|
|
825
|
+
const commandName = resolveItemName(command.name, null, 'command');
|
|
826
|
+
let applicable = false;
|
|
827
|
+
for (const prefix of commandPrefixes) {
|
|
828
|
+
const relativePath = command.namespace
|
|
829
|
+
? `${prefix}/${command.namespace}/${commandName}.md`
|
|
830
|
+
: `${prefix}/${commandName}.md`;
|
|
831
|
+
const fullPath = path.join(targetDir, relativePath);
|
|
832
|
+
if (fs.existsSync(fullPath)) {
|
|
833
|
+
preview.willOverwrite.push(relativePath);
|
|
834
|
+
} else {
|
|
835
|
+
preview.willCreate.push(relativePath);
|
|
836
|
+
}
|
|
837
|
+
applicable = true;
|
|
838
|
+
}
|
|
839
|
+
if (aiConfigTypes.includes('gemini')) {
|
|
840
|
+
pushSkipped(preview.skipped, 'command', commandName, 'Gemini 不支持 commands,已跳过');
|
|
841
|
+
}
|
|
842
|
+
if (applicable) {
|
|
843
|
+
preview.summary.commands++;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const allServers = mcpService.getAllServers();
|
|
849
|
+
const presets = mcpService.getPresets();
|
|
850
|
+
let resolvableMcpCount = 0;
|
|
851
|
+
for (const serverId of template.mcpServers || []) {
|
|
852
|
+
let serverSpec = allServers[serverId]?.server;
|
|
853
|
+
if (!serverSpec) {
|
|
854
|
+
const preset = presets.find(p => p.id === serverId);
|
|
855
|
+
if (preset) {
|
|
856
|
+
serverSpec = preset.server;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (serverSpec) {
|
|
860
|
+
resolvableMcpCount++;
|
|
861
|
+
} else {
|
|
862
|
+
pushSkipped(preview.skipped, 'mcpServer', serverId, '未找到对应 MCP 服务配置,预览已跳过');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (resolvableMcpCount > 0) {
|
|
867
|
+
const mcpPath = path.join(targetDir, '.mcp.json');
|
|
868
|
+
if (fs.existsSync(mcpPath)) {
|
|
869
|
+
preview.willOverwrite.push('.mcp.json');
|
|
870
|
+
} else {
|
|
871
|
+
preview.willCreate.push('.mcp.json');
|
|
872
|
+
}
|
|
873
|
+
preview.summary.mcpServers = resolvableMcpCount;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (aiConfigTypes.includes('opencode') && (resolvableMcpCount > 0 || template.plugins?.length > 0)) {
|
|
877
|
+
const opencodeConfigPath = path.join(targetDir, '.opencode/opencode.json');
|
|
878
|
+
if (fs.existsSync(opencodeConfigPath)) {
|
|
879
|
+
preview.willOverwrite.push('.opencode/opencode.json');
|
|
880
|
+
} else {
|
|
881
|
+
preview.willCreate.push('.opencode/opencode.json');
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (template.plugins?.length > 0) {
|
|
886
|
+
if (aiConfigTypes.includes('opencode')) {
|
|
887
|
+
preview.summary.plugins = template.plugins.length;
|
|
888
|
+
} else {
|
|
889
|
+
for (const plugin of template.plugins) {
|
|
890
|
+
pushSkipped(preview.skipped, 'plugin', plugin.name, '当前未选择 OpenCode,已跳过插件写入预览');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
preview.willCreate = [...new Set(preview.willCreate)];
|
|
896
|
+
preview.willOverwrite = [...new Set(preview.willOverwrite)];
|
|
897
|
+
preview.summary.skipped = preview.skipped.length;
|
|
898
|
+
|
|
899
|
+
return preview;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
module.exports = {
|
|
903
|
+
getAllTemplates,
|
|
904
|
+
getTemplateById,
|
|
905
|
+
createCustomTemplate,
|
|
906
|
+
updateCustomTemplate,
|
|
907
|
+
deleteCustomTemplate,
|
|
908
|
+
applyTemplate,
|
|
909
|
+
readCurrentConfig,
|
|
910
|
+
getAvailableConfigs,
|
|
911
|
+
applyTemplateToProject,
|
|
912
|
+
previewTemplateApplication
|
|
913
|
+
};
|