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,941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Sync Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages file synchronization between cc-tool central storage and CLI directories:
|
|
5
|
+
* - Claude Code: ~/.claude/{skills,commands,agents,plugins}/
|
|
6
|
+
* - Codex CLI: ~/.codex/skills/, ~/.codex/prompts/
|
|
7
|
+
* - Gemini CLI: ~/.gemini/skills/
|
|
8
|
+
* - OpenCode CLI: ~/.config/opencode/{skills,commands,agents,plugins}/
|
|
9
|
+
*
|
|
10
|
+
* Config types:
|
|
11
|
+
* - skills: directory-based (each skill is a dir with SKILL.md)
|
|
12
|
+
* - commands: file-based (.md), may be nested in subdirectories
|
|
13
|
+
* - agents: file-based (.md), flat directory
|
|
14
|
+
* - plugins: directory-based
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const toml = require('toml');
|
|
21
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
22
|
+
const { convertSkillToCodex, convertCommandToCodex } = require('./format-converter');
|
|
23
|
+
const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
24
|
+
|
|
25
|
+
// Paths
|
|
26
|
+
const HOME = os.homedir();
|
|
27
|
+
const CC_TOOL_CONFIGS = path.join(PATHS.base, 'configs');
|
|
28
|
+
const CLAUDE_CODE_DIR = path.join(HOME, '.claude');
|
|
29
|
+
const CODEX_DIR = path.join(HOME, '.codex');
|
|
30
|
+
const GEMINI_DIR = path.join(HOME, '.gemini');
|
|
31
|
+
const OPENCODE_DIR = NATIVE_PATHS.opencode.config;
|
|
32
|
+
const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
|
|
33
|
+
|
|
34
|
+
// Config type definitions
|
|
35
|
+
const CONFIG_TYPES = {
|
|
36
|
+
skills: {
|
|
37
|
+
isDirectory: true,
|
|
38
|
+
markerFile: 'SKILL.md',
|
|
39
|
+
claudeTarget: 'skills',
|
|
40
|
+
codexTarget: 'skills',
|
|
41
|
+
codexSupported: true,
|
|
42
|
+
convertForCodex: true,
|
|
43
|
+
geminiTarget: 'skills',
|
|
44
|
+
geminiSupported: true,
|
|
45
|
+
opencodeTarget: 'skills',
|
|
46
|
+
opencodeLegacyTarget: 'skill',
|
|
47
|
+
opencodeSupported: true
|
|
48
|
+
},
|
|
49
|
+
commands: {
|
|
50
|
+
isDirectory: false,
|
|
51
|
+
extension: '.md',
|
|
52
|
+
claudeTarget: 'commands',
|
|
53
|
+
codexTarget: 'prompts',
|
|
54
|
+
codexSupported: true,
|
|
55
|
+
convertForCodex: true,
|
|
56
|
+
geminiSupported: false,
|
|
57
|
+
opencodeTarget: 'commands',
|
|
58
|
+
opencodeLegacyTarget: 'command',
|
|
59
|
+
opencodeSupported: true
|
|
60
|
+
},
|
|
61
|
+
agents: {
|
|
62
|
+
isDirectory: false,
|
|
63
|
+
extension: '.md',
|
|
64
|
+
claudeTarget: 'agents',
|
|
65
|
+
codexSupported: true,
|
|
66
|
+
geminiSupported: false,
|
|
67
|
+
opencodeTarget: 'agents',
|
|
68
|
+
opencodeLegacyTarget: 'agent',
|
|
69
|
+
opencodeSupported: true
|
|
70
|
+
},
|
|
71
|
+
plugins: {
|
|
72
|
+
isDirectory: true,
|
|
73
|
+
claudeTarget: 'plugins',
|
|
74
|
+
codexSupported: false,
|
|
75
|
+
geminiSupported: false,
|
|
76
|
+
opencodeTarget: 'plugins',
|
|
77
|
+
opencodeLegacyTarget: 'plugin',
|
|
78
|
+
opencodeSupported: true
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
class ConfigSyncManager {
|
|
83
|
+
constructor() {
|
|
84
|
+
ensureStorageDirMigrated();
|
|
85
|
+
this.ccToolConfigs = CC_TOOL_CONFIGS;
|
|
86
|
+
this.claudeDir = CLAUDE_CODE_DIR;
|
|
87
|
+
this.codexDir = CODEX_DIR;
|
|
88
|
+
this.geminiDir = GEMINI_DIR;
|
|
89
|
+
this.opencodeDir = OPENCODE_DIR;
|
|
90
|
+
this.configTypes = CONFIG_TYPES;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Sync a config item to Claude Code
|
|
95
|
+
* @param {string} type - Config type (skills, commands, agents, plugins)
|
|
96
|
+
* @param {string} name - Item name (directory name for skills, file path for others)
|
|
97
|
+
* @returns {Object} Result with success status
|
|
98
|
+
*/
|
|
99
|
+
syncToClaude(type, name) {
|
|
100
|
+
const config = this.configTypes[type];
|
|
101
|
+
if (!config) {
|
|
102
|
+
console.log(`[ConfigSyncManager] Unknown config type: ${type}`);
|
|
103
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
107
|
+
if (!safeName) {
|
|
108
|
+
return { success: false, error: 'Invalid config item name' };
|
|
109
|
+
}
|
|
110
|
+
const sourcePath = path.join(this.ccToolConfigs, type, safeName);
|
|
111
|
+
const targetPath = path.join(this.claudeDir, config.claudeTarget, safeName);
|
|
112
|
+
|
|
113
|
+
// Check if source exists
|
|
114
|
+
if (!fs.existsSync(sourcePath)) {
|
|
115
|
+
console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
|
|
116
|
+
return { success: false, error: 'Source not found' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (config.isDirectory) {
|
|
121
|
+
// Copy entire directory recursively
|
|
122
|
+
this._ensureDir(path.dirname(targetPath));
|
|
123
|
+
this._copyDirRecursive(sourcePath, targetPath);
|
|
124
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (directory)`);
|
|
125
|
+
} else {
|
|
126
|
+
// Copy single file, preserving subdirectory structure
|
|
127
|
+
this._ensureDir(path.dirname(targetPath));
|
|
128
|
+
this._copyFile(sourcePath, targetPath);
|
|
129
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Claude Code (file)`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { success: true, target: targetPath };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(`[ConfigSyncManager] Sync to Claude failed:`, err.message);
|
|
135
|
+
return { success: false, error: err.message };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Remove a config item from Claude Code
|
|
141
|
+
* @param {string} type - Config type
|
|
142
|
+
* @param {string} name - Item name
|
|
143
|
+
* @returns {Object} Result with success status
|
|
144
|
+
*/
|
|
145
|
+
removeFromClaude(type, name) {
|
|
146
|
+
const config = this.configTypes[type];
|
|
147
|
+
if (!config) {
|
|
148
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
152
|
+
if (!safeName) {
|
|
153
|
+
return { success: false, error: 'Invalid config item name' };
|
|
154
|
+
}
|
|
155
|
+
const targetPath = path.join(this.claudeDir, config.claudeTarget, safeName);
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(targetPath)) {
|
|
158
|
+
console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
|
|
159
|
+
return { success: true, message: 'Already removed' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (config.isDirectory) {
|
|
164
|
+
// Remove entire directory
|
|
165
|
+
this._removeRecursive(targetPath);
|
|
166
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (directory)`);
|
|
167
|
+
} else {
|
|
168
|
+
// Remove file
|
|
169
|
+
fs.unlinkSync(targetPath);
|
|
170
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Claude Code (file)`);
|
|
171
|
+
|
|
172
|
+
// Clean up empty parent directories for file-based configs
|
|
173
|
+
this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.claudeDir, config.claudeTarget));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { success: true };
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`[ConfigSyncManager] Remove from Claude failed:`, err.message);
|
|
179
|
+
return { success: false, error: err.message };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sync a config item to Codex CLI
|
|
185
|
+
* Supports skills, commands, agents
|
|
186
|
+
* @param {string} type - Config type
|
|
187
|
+
* @param {string} name - Item name
|
|
188
|
+
* @returns {Object} Result with success status and any warnings
|
|
189
|
+
*/
|
|
190
|
+
syncToCodex(type, name) {
|
|
191
|
+
const config = this.configTypes[type];
|
|
192
|
+
if (!config) {
|
|
193
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!config.codexSupported) {
|
|
197
|
+
console.log(`[ConfigSyncManager] ${type} not supported by Codex, skipping`);
|
|
198
|
+
return { success: true, skipped: true, reason: 'Not supported by Codex' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
202
|
+
if (!safeName) {
|
|
203
|
+
return { success: false, error: 'Invalid config item name' };
|
|
204
|
+
}
|
|
205
|
+
const sourcePath = path.join(this.ccToolConfigs, type, safeName);
|
|
206
|
+
|
|
207
|
+
if (!fs.existsSync(sourcePath)) {
|
|
208
|
+
console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
|
|
209
|
+
return { success: false, error: 'Source not found' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const warnings = [];
|
|
214
|
+
|
|
215
|
+
if (type === 'agents') {
|
|
216
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
217
|
+
const { frontmatter } = this._parseFrontmatter(sourceContent);
|
|
218
|
+
const fileName = path.basename(safeName, path.extname(safeName));
|
|
219
|
+
|
|
220
|
+
const codexConfig = this._readCodexConfigToml();
|
|
221
|
+
codexConfig.features = this._isPlainObject(codexConfig.features) ? codexConfig.features : {};
|
|
222
|
+
codexConfig.features.multi_agent = true;
|
|
223
|
+
codexConfig.agents = this._isPlainObject(codexConfig.agents) ? codexConfig.agents : {};
|
|
224
|
+
|
|
225
|
+
const existing = codexConfig.agents[fileName];
|
|
226
|
+
if (Object.prototype.hasOwnProperty.call(codexConfig.agents, fileName) &&
|
|
227
|
+
!this._isPlainObject(existing)) {
|
|
228
|
+
return { success: false, error: `Agent name "${fileName}" conflicts with global [agents] key` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const entry = this._isPlainObject(existing) ? { ...existing } : {};
|
|
232
|
+
entry.description = (frontmatter.description || fileName).trim();
|
|
233
|
+
|
|
234
|
+
const model = typeof frontmatter.model === 'string' ? frontmatter.model.trim() : '';
|
|
235
|
+
const existingConfigFile = this._normalizeCodexPath(entry.config_file);
|
|
236
|
+
const isExistingManagedConfig = this._isManagedCodexAgentConfigPath(existingConfigFile);
|
|
237
|
+
if (model) {
|
|
238
|
+
const managedConfigPath = isExistingManagedConfig
|
|
239
|
+
? existingConfigFile
|
|
240
|
+
: this._getCodexManagedAgentConfigPath(fileName);
|
|
241
|
+
const parsedConfigFile = this._readCodexAgentConfigFile(managedConfigPath);
|
|
242
|
+
const configData = this._isPlainObject(parsedConfigFile?.data) ? parsedConfigFile.data : {};
|
|
243
|
+
configData.model = model;
|
|
244
|
+
this._writeCodexAgentConfigFile(managedConfigPath, configData);
|
|
245
|
+
entry.config_file = managedConfigPath;
|
|
246
|
+
} else if (isExistingManagedConfig) {
|
|
247
|
+
const resolvedConfigPath = this._resolveCodexPath(existingConfigFile);
|
|
248
|
+
if (resolvedConfigPath && fs.existsSync(resolvedConfigPath)) {
|
|
249
|
+
fs.unlinkSync(resolvedConfigPath);
|
|
250
|
+
}
|
|
251
|
+
delete entry.config_file;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
codexConfig.agents[fileName] = entry;
|
|
255
|
+
|
|
256
|
+
this._writeCodexConfigToml(codexConfig);
|
|
257
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (config.toml agents table)`);
|
|
258
|
+
return { success: true, target: CODEX_CONFIG_PATH, warnings };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (type === 'skills') {
|
|
262
|
+
// Skills: copy directory, convert SKILL.md content
|
|
263
|
+
const targetPath = path.join(this.codexDir, config.codexTarget, safeName);
|
|
264
|
+
this._ensureDir(targetPath);
|
|
265
|
+
|
|
266
|
+
// Copy all files, converting SKILL.md
|
|
267
|
+
this._copyDirWithConversion(sourcePath, targetPath, (filePath, content) => {
|
|
268
|
+
if (path.basename(filePath) === 'SKILL.md') {
|
|
269
|
+
const result = convertSkillToCodex(content);
|
|
270
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
271
|
+
warnings.push(...result.warnings);
|
|
272
|
+
}
|
|
273
|
+
return result.content;
|
|
274
|
+
}
|
|
275
|
+
return content;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (skill directory)`);
|
|
279
|
+
return { success: true, target: targetPath, warnings };
|
|
280
|
+
|
|
281
|
+
} else if (type === 'commands') {
|
|
282
|
+
// Commands: convert and write to prompts directory
|
|
283
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
284
|
+
const result = convertCommandToCodex(content);
|
|
285
|
+
|
|
286
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
287
|
+
warnings.push(...result.warnings);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Target path in codex prompts (same relative path structure)
|
|
291
|
+
const targetPath = path.join(this.codexDir, config.codexTarget, safeName);
|
|
292
|
+
this._ensureDir(path.dirname(targetPath));
|
|
293
|
+
fs.writeFileSync(targetPath, result.content, 'utf-8');
|
|
294
|
+
|
|
295
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Codex (prompt)`);
|
|
296
|
+
return { success: true, target: targetPath, warnings };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { success: false, error: 'Unexpected type' };
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(`[ConfigSyncManager] Sync to Codex failed:`, err.message);
|
|
302
|
+
return { success: false, error: err.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Remove a config item from Codex CLI
|
|
308
|
+
* @param {string} type - Config type
|
|
309
|
+
* @param {string} name - Item name
|
|
310
|
+
* @returns {Object} Result with success status
|
|
311
|
+
*/
|
|
312
|
+
removeFromCodex(type, name) {
|
|
313
|
+
const config = this.configTypes[type];
|
|
314
|
+
if (!config) {
|
|
315
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
319
|
+
if (!safeName) {
|
|
320
|
+
return { success: false, error: 'Invalid config item name' };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!config.codexSupported) {
|
|
324
|
+
return { success: true, skipped: true, reason: 'Not supported by Codex' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (type === 'agents') {
|
|
328
|
+
try {
|
|
329
|
+
const configData = this._readCodexConfigToml();
|
|
330
|
+
const agentsTable = this._isPlainObject(configData.agents) ? configData.agents : {};
|
|
331
|
+
const fileName = path.basename(safeName, path.extname(safeName));
|
|
332
|
+
|
|
333
|
+
const existing = agentsTable[fileName];
|
|
334
|
+
if (!this._isPlainObject(existing)) {
|
|
335
|
+
return { success: true, message: 'Already removed' };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const existingConfigFile = this._normalizeCodexPath(existing.config_file);
|
|
339
|
+
if (existingConfigFile && this._isManagedCodexAgentConfigPath(existingConfigFile)) {
|
|
340
|
+
const resolvedPath = this._resolveCodexPath(existingConfigFile);
|
|
341
|
+
if (resolvedPath && fs.existsSync(resolvedPath)) {
|
|
342
|
+
fs.unlinkSync(resolvedPath);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
delete agentsTable[fileName];
|
|
347
|
+
configData.agents = agentsTable;
|
|
348
|
+
this._writeCodexConfigToml(configData);
|
|
349
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (config.toml agents table)`);
|
|
350
|
+
return { success: true };
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.error(`[ConfigSyncManager] Remove from Codex failed:`, err.message);
|
|
353
|
+
return { success: false, error: err.message };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const targetPath = path.join(this.codexDir, config.codexTarget, safeName);
|
|
358
|
+
|
|
359
|
+
if (!fs.existsSync(targetPath)) {
|
|
360
|
+
console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
|
|
361
|
+
return { success: true, message: 'Already removed' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (type === 'skills') {
|
|
366
|
+
// Remove entire directory
|
|
367
|
+
this._removeRecursive(targetPath);
|
|
368
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (skill directory)`);
|
|
369
|
+
} else {
|
|
370
|
+
// Remove file
|
|
371
|
+
fs.unlinkSync(targetPath);
|
|
372
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Codex (prompt)`);
|
|
373
|
+
|
|
374
|
+
// Clean up empty parent directories
|
|
375
|
+
this._cleanupEmptyParents(path.dirname(targetPath), path.join(this.codexDir, config.codexTarget));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { success: true };
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.error(`[ConfigSyncManager] Remove from Codex failed:`, err.message);
|
|
381
|
+
return { success: false, error: err.message };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Sync a config item to Gemini CLI
|
|
387
|
+
* Currently only skills are supported
|
|
388
|
+
* @param {string} type - Config type
|
|
389
|
+
* @param {string} name - Item name
|
|
390
|
+
* @returns {Object} Result with success status
|
|
391
|
+
*/
|
|
392
|
+
syncToGemini(type, name) {
|
|
393
|
+
const config = this.configTypes[type];
|
|
394
|
+
if (!config) {
|
|
395
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!config.geminiSupported) {
|
|
399
|
+
console.log(`[ConfigSyncManager] ${type} not supported by Gemini, skipping`);
|
|
400
|
+
return { success: true, skipped: true, reason: 'Not supported by Gemini' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
404
|
+
if (!safeName) {
|
|
405
|
+
return { success: false, error: 'Invalid config item name' };
|
|
406
|
+
}
|
|
407
|
+
const sourcePath = path.join(this.ccToolConfigs, type, safeName);
|
|
408
|
+
if (!fs.existsSync(sourcePath)) {
|
|
409
|
+
console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
|
|
410
|
+
return { success: false, error: 'Source not found' };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const targetPath = path.join(this.geminiDir, config.geminiTarget, safeName);
|
|
415
|
+
this._ensureDir(path.dirname(targetPath));
|
|
416
|
+
this._copyDirRecursive(sourcePath, targetPath);
|
|
417
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to Gemini (directory)`);
|
|
418
|
+
return { success: true, target: targetPath };
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error(`[ConfigSyncManager] Sync to Gemini failed:`, err.message);
|
|
421
|
+
return { success: false, error: err.message };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Remove a config item from Gemini CLI
|
|
427
|
+
* @param {string} type - Config type
|
|
428
|
+
* @param {string} name - Item name
|
|
429
|
+
* @returns {Object} Result with success status
|
|
430
|
+
*/
|
|
431
|
+
removeFromGemini(type, name) {
|
|
432
|
+
const config = this.configTypes[type];
|
|
433
|
+
if (!config) {
|
|
434
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
438
|
+
if (!safeName) {
|
|
439
|
+
return { success: false, error: 'Invalid config item name' };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!config.geminiSupported) {
|
|
443
|
+
return { success: true, skipped: true, reason: 'Not supported by Gemini' };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const targetPath = path.join(this.geminiDir, config.geminiTarget, safeName);
|
|
447
|
+
if (!fs.existsSync(targetPath)) {
|
|
448
|
+
console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
|
|
449
|
+
return { success: true, message: 'Already removed' };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
this._removeRecursive(targetPath);
|
|
454
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from Gemini (directory)`);
|
|
455
|
+
return { success: true };
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error(`[ConfigSyncManager] Remove from Gemini failed:`, err.message);
|
|
458
|
+
return { success: false, error: err.message };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Sync a config item to OpenCode CLI
|
|
464
|
+
* Supports skills, commands, agents, plugins
|
|
465
|
+
* @param {string} type - Config type
|
|
466
|
+
* @param {string} name - Item name
|
|
467
|
+
* @returns {Object} Result with success status
|
|
468
|
+
*/
|
|
469
|
+
syncToOpenCode(type, name) {
|
|
470
|
+
const config = this.configTypes[type];
|
|
471
|
+
if (!config) {
|
|
472
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!config.opencodeSupported) {
|
|
476
|
+
console.log(`[ConfigSyncManager] ${type} not supported by OpenCode, skipping`);
|
|
477
|
+
return { success: true, skipped: true, reason: 'Not supported by OpenCode' };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
481
|
+
if (!safeName) {
|
|
482
|
+
return { success: false, error: 'Invalid config item name' };
|
|
483
|
+
}
|
|
484
|
+
const sourcePath = path.join(this.ccToolConfigs, type, safeName);
|
|
485
|
+
if (!fs.existsSync(sourcePath)) {
|
|
486
|
+
console.log(`[ConfigSyncManager] Source not found: ${sourcePath}`);
|
|
487
|
+
return { success: false, error: 'Source not found' };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const targetBaseDir = this._getOpenCodeTypeBaseDir(config);
|
|
492
|
+
const targetPath = path.join(targetBaseDir, safeName);
|
|
493
|
+
|
|
494
|
+
if (config.isDirectory) {
|
|
495
|
+
this._ensureDir(path.dirname(targetPath));
|
|
496
|
+
this._copyDirRecursive(sourcePath, targetPath);
|
|
497
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to OpenCode (directory)`);
|
|
498
|
+
} else {
|
|
499
|
+
this._ensureDir(path.dirname(targetPath));
|
|
500
|
+
this._copyFile(sourcePath, targetPath);
|
|
501
|
+
console.log(`[ConfigSyncManager] Synced ${type}/${name} to OpenCode (file)`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { success: true, target: targetPath };
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error(`[ConfigSyncManager] Sync to OpenCode failed:`, err.message);
|
|
507
|
+
return { success: false, error: err.message };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Remove a config item from OpenCode CLI
|
|
513
|
+
* @param {string} type - Config type
|
|
514
|
+
* @param {string} name - Item name
|
|
515
|
+
* @returns {Object} Result with success status
|
|
516
|
+
*/
|
|
517
|
+
removeFromOpenCode(type, name) {
|
|
518
|
+
const config = this.configTypes[type];
|
|
519
|
+
if (!config) {
|
|
520
|
+
return { success: false, error: `Unknown config type: ${type}` };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const safeName = this._normalizeSafeRelativeName(name);
|
|
524
|
+
if (!safeName) {
|
|
525
|
+
return { success: false, error: 'Invalid config item name' };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!config.opencodeSupported) {
|
|
529
|
+
return { success: true, skipped: true, reason: 'Not supported by OpenCode' };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const targetBaseDir = this._getOpenCodeTypeBaseDir(config);
|
|
533
|
+
const targetPath = path.join(targetBaseDir, safeName);
|
|
534
|
+
|
|
535
|
+
if (!fs.existsSync(targetPath)) {
|
|
536
|
+
console.log(`[ConfigSyncManager] Target not found (already removed): ${targetPath}`);
|
|
537
|
+
return { success: true, message: 'Already removed' };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
if (config.isDirectory) {
|
|
542
|
+
this._removeRecursive(targetPath);
|
|
543
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from OpenCode (directory)`);
|
|
544
|
+
} else {
|
|
545
|
+
fs.unlinkSync(targetPath);
|
|
546
|
+
console.log(`[ConfigSyncManager] Removed ${type}/${name} from OpenCode (file)`);
|
|
547
|
+
this._cleanupEmptyParents(path.dirname(targetPath), targetBaseDir);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return { success: true };
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error(`[ConfigSyncManager] Remove from OpenCode failed:`, err.message);
|
|
553
|
+
return { success: false, error: err.message };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Batch sync based on registry data
|
|
559
|
+
* @param {string} type - Config type
|
|
560
|
+
* @param {Object} registryItems - Registry items { name: { enabled, platforms: { claude, codex, gemini, opencode } } }
|
|
561
|
+
* @returns {Object} Results summary
|
|
562
|
+
*/
|
|
563
|
+
syncAll(type, registryItems) {
|
|
564
|
+
const results = {
|
|
565
|
+
synced: [],
|
|
566
|
+
removed: [],
|
|
567
|
+
errors: [],
|
|
568
|
+
warnings: []
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
if (!registryItems || typeof registryItems !== 'object') {
|
|
572
|
+
return results;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
for (const [name, item] of Object.entries(registryItems)) {
|
|
576
|
+
if (!item || typeof item !== 'object') continue;
|
|
577
|
+
|
|
578
|
+
const { enabled, platforms } = item;
|
|
579
|
+
|
|
580
|
+
if (enabled && platforms) {
|
|
581
|
+
// Sync to enabled platforms
|
|
582
|
+
if (platforms.claude) {
|
|
583
|
+
const result = this.syncToClaude(type, name);
|
|
584
|
+
if (result.success && !result.skipped) {
|
|
585
|
+
results.synced.push({ type, name, platform: 'claude' });
|
|
586
|
+
} else if (!result.success) {
|
|
587
|
+
results.errors.push({ type, name, platform: 'claude', error: result.error });
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
// Platform disabled, remove
|
|
591
|
+
const result = this.removeFromClaude(type, name);
|
|
592
|
+
if (result.success && !result.message) {
|
|
593
|
+
results.removed.push({ type, name, platform: 'claude' });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (platforms.codex) {
|
|
598
|
+
const result = this.syncToCodex(type, name);
|
|
599
|
+
if (result.success && !result.skipped) {
|
|
600
|
+
results.synced.push({ type, name, platform: 'codex' });
|
|
601
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
602
|
+
results.warnings.push({ type, name, platform: 'codex', warnings: result.warnings });
|
|
603
|
+
}
|
|
604
|
+
} else if (!result.success) {
|
|
605
|
+
results.errors.push({ type, name, platform: 'codex', error: result.error });
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
// Platform disabled, remove
|
|
609
|
+
const result = this.removeFromCodex(type, name);
|
|
610
|
+
if (result.success && !result.message && !result.skipped) {
|
|
611
|
+
results.removed.push({ type, name, platform: 'codex' });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (platforms.gemini) {
|
|
616
|
+
const result = this.syncToGemini(type, name);
|
|
617
|
+
if (result.success && !result.skipped) {
|
|
618
|
+
results.synced.push({ type, name, platform: 'gemini' });
|
|
619
|
+
} else if (!result.success) {
|
|
620
|
+
results.errors.push({ type, name, platform: 'gemini', error: result.error });
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
const result = this.removeFromGemini(type, name);
|
|
624
|
+
if (result.success && !result.message && !result.skipped) {
|
|
625
|
+
results.removed.push({ type, name, platform: 'gemini' });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (platforms.opencode) {
|
|
630
|
+
const result = this.syncToOpenCode(type, name);
|
|
631
|
+
if (result.success && !result.skipped) {
|
|
632
|
+
results.synced.push({ type, name, platform: 'opencode' });
|
|
633
|
+
} else if (!result.success) {
|
|
634
|
+
results.errors.push({ type, name, platform: 'opencode', error: result.error });
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
const result = this.removeFromOpenCode(type, name);
|
|
638
|
+
if (result.success && !result.message && !result.skipped) {
|
|
639
|
+
results.removed.push({ type, name, platform: 'opencode' });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
// Item disabled, remove from all platforms
|
|
644
|
+
const claudeResult = this.removeFromClaude(type, name);
|
|
645
|
+
if (claudeResult.success && !claudeResult.message) {
|
|
646
|
+
results.removed.push({ type, name, platform: 'claude' });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const codexResult = this.removeFromCodex(type, name);
|
|
650
|
+
if (codexResult.success && !codexResult.message && !codexResult.skipped) {
|
|
651
|
+
results.removed.push({ type, name, platform: 'codex' });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const geminiResult = this.removeFromGemini(type, name);
|
|
655
|
+
if (geminiResult.success && !geminiResult.message && !geminiResult.skipped) {
|
|
656
|
+
results.removed.push({ type, name, platform: 'gemini' });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const opencodeResult = this.removeFromOpenCode(type, name);
|
|
660
|
+
if (opencodeResult.success && !opencodeResult.message && !opencodeResult.skipped) {
|
|
661
|
+
results.removed.push({ type, name, platform: 'opencode' });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
console.log(`[ConfigSyncManager] syncAll(${type}): synced=${results.synced.length}, removed=${results.removed.length}, errors=${results.errors.length}`);
|
|
667
|
+
return results;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ==================== Helper Methods ====================
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Ensure a directory exists
|
|
674
|
+
*/
|
|
675
|
+
_ensureDir(dir) {
|
|
676
|
+
if (!fs.existsSync(dir)) {
|
|
677
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Recursively copy a directory
|
|
683
|
+
*/
|
|
684
|
+
_copyDirRecursive(src, dest) {
|
|
685
|
+
this._ensureDir(dest);
|
|
686
|
+
|
|
687
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
688
|
+
|
|
689
|
+
for (const entry of entries) {
|
|
690
|
+
const srcPath = path.join(src, entry.name);
|
|
691
|
+
const destPath = path.join(dest, entry.name);
|
|
692
|
+
|
|
693
|
+
if (entry.isDirectory()) {
|
|
694
|
+
this._copyDirRecursive(srcPath, destPath);
|
|
695
|
+
} else {
|
|
696
|
+
fs.copyFileSync(srcPath, destPath);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Copy a directory with content transformation
|
|
703
|
+
* @param {string} src - Source directory
|
|
704
|
+
* @param {string} dest - Destination directory
|
|
705
|
+
* @param {Function} transform - Function(filePath, content) => transformedContent
|
|
706
|
+
*/
|
|
707
|
+
_copyDirWithConversion(src, dest, transform) {
|
|
708
|
+
this._ensureDir(dest);
|
|
709
|
+
|
|
710
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
711
|
+
|
|
712
|
+
for (const entry of entries) {
|
|
713
|
+
const srcPath = path.join(src, entry.name);
|
|
714
|
+
const destPath = path.join(dest, entry.name);
|
|
715
|
+
|
|
716
|
+
if (entry.isDirectory()) {
|
|
717
|
+
this._copyDirWithConversion(srcPath, destPath, transform);
|
|
718
|
+
} else {
|
|
719
|
+
// Check if it's a text file that should be transformed
|
|
720
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
721
|
+
const textExtensions = ['.md', '.txt', '.json', '.js', '.ts', '.py', '.sh', '.yaml', '.yml'];
|
|
722
|
+
|
|
723
|
+
if (textExtensions.includes(ext)) {
|
|
724
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
725
|
+
const transformed = transform(srcPath, content);
|
|
726
|
+
fs.writeFileSync(destPath, transformed, 'utf-8');
|
|
727
|
+
} else {
|
|
728
|
+
// Binary file, copy as-is
|
|
729
|
+
fs.copyFileSync(srcPath, destPath);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Copy a single file
|
|
737
|
+
*/
|
|
738
|
+
_copyFile(src, dest) {
|
|
739
|
+
fs.copyFileSync(src, dest);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Resolve OpenCode base target directory for a config type.
|
|
744
|
+
* OpenCode supports both plural (new) and singular (legacy) folder names.
|
|
745
|
+
*/
|
|
746
|
+
_getOpenCodeTypeBaseDir(config) {
|
|
747
|
+
const modernDir = path.join(this.opencodeDir, config.opencodeTarget);
|
|
748
|
+
// 技能目录强制使用 modern/plural 形式,避免 legacy 目录带来的跨平台历史污染
|
|
749
|
+
if (config === this.configTypes.skills) {
|
|
750
|
+
return modernDir;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!config.opencodeLegacyTarget) {
|
|
754
|
+
return modernDir;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const legacyDir = path.join(this.opencodeDir, config.opencodeLegacyTarget);
|
|
758
|
+
if (fs.existsSync(legacyDir) && !fs.existsSync(modernDir)) {
|
|
759
|
+
return legacyDir;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return modernDir;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Recursively remove a file or directory
|
|
767
|
+
*/
|
|
768
|
+
_removeRecursive(target) {
|
|
769
|
+
if (!fs.existsSync(target)) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Clean up empty parent directories up to the base directory
|
|
778
|
+
*/
|
|
779
|
+
_cleanupEmptyParents(dir, baseDir) {
|
|
780
|
+
// Normalize paths for comparison
|
|
781
|
+
const normalizedDir = path.resolve(dir);
|
|
782
|
+
const normalizedBase = path.resolve(baseDir);
|
|
783
|
+
|
|
784
|
+
// Don't go above base directory
|
|
785
|
+
if (!normalizedDir.startsWith(normalizedBase) || normalizedDir === normalizedBase) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
const entries = fs.readdirSync(dir);
|
|
791
|
+
if (entries.length === 0) {
|
|
792
|
+
fs.rmdirSync(dir);
|
|
793
|
+
console.log(`[ConfigSyncManager] Removed empty directory: ${dir}`);
|
|
794
|
+
// Recurse to parent
|
|
795
|
+
this._cleanupEmptyParents(path.dirname(dir), baseDir);
|
|
796
|
+
}
|
|
797
|
+
} catch (err) {
|
|
798
|
+
// Ignore errors (directory might not exist or permission issues)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
_normalizeSafeRelativeName(name) {
|
|
803
|
+
const raw = String(name || '').replace(/\\/g, '/').trim();
|
|
804
|
+
if (!raw || raw.includes('\0')) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
|
|
809
|
+
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (path.isAbsolute(raw) || raw.startsWith('/')) {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return normalized;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
_parseFrontmatter(content) {
|
|
821
|
+
const result = {
|
|
822
|
+
frontmatter: {},
|
|
823
|
+
body: content
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const normalized = content.trim().replace(/^\uFEFF/, '');
|
|
827
|
+
const match = normalized.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
828
|
+
if (!match) {
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const frontmatterText = match[1];
|
|
833
|
+
result.body = match[2].trim();
|
|
834
|
+
|
|
835
|
+
for (const line of frontmatterText.split('\n')) {
|
|
836
|
+
const colonIndex = line.indexOf(':');
|
|
837
|
+
if (colonIndex === -1) continue;
|
|
838
|
+
const key = line.slice(0, colonIndex).trim();
|
|
839
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
840
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
841
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
842
|
+
value = value.slice(1, -1);
|
|
843
|
+
}
|
|
844
|
+
result.frontmatter[key] = value;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return result;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
_isPlainObject(value) {
|
|
851
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
_normalizeCodexPath(configPath) {
|
|
855
|
+
return typeof configPath === 'string' ? configPath.trim() : '';
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
_resolveCodexPath(configPath) {
|
|
859
|
+
const normalized = this._normalizeCodexPath(configPath);
|
|
860
|
+
if (!normalized) return '';
|
|
861
|
+
|
|
862
|
+
if (normalized.startsWith('~/')) {
|
|
863
|
+
return path.join(HOME, normalized.slice(2));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (path.isAbsolute(normalized)) {
|
|
867
|
+
return normalized;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return path.resolve(path.dirname(CODEX_CONFIG_PATH), normalized);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
_isManagedCodexAgentConfigPath(configPath) {
|
|
874
|
+
const resolved = this._resolveCodexPath(configPath);
|
|
875
|
+
if (!resolved) return false;
|
|
876
|
+
|
|
877
|
+
const managedRoot = path.resolve(this._getCodexManagedAgentConfigDir()) + path.sep;
|
|
878
|
+
return resolved.startsWith(managedRoot) || resolved === path.resolve(this._getCodexManagedAgentConfigDir());
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
_getCodexManagedAgentConfigDir() {
|
|
882
|
+
return path.join(this.codexDir, 'agents');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
_getCodexManagedAgentConfigPath(fileName) {
|
|
886
|
+
return path.join(this._getCodexManagedAgentConfigDir(), `${fileName}.toml`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
_writeCodexAgentConfigFile(configPath, data) {
|
|
890
|
+
const resolved = this._resolveCodexPath(configPath);
|
|
891
|
+
this._ensureDir(path.dirname(resolved));
|
|
892
|
+
const tempPath = `${resolved}.tmp-${process.pid}-${Date.now()}`;
|
|
893
|
+
fs.writeFileSync(tempPath, tomlStringify(data), 'utf-8');
|
|
894
|
+
fs.renameSync(tempPath, resolved);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
_readCodexAgentConfigFile(configPath) {
|
|
898
|
+
const resolved = this._resolveCodexPath(configPath);
|
|
899
|
+
if (!resolved || !fs.existsSync(resolved)) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
905
|
+
return {
|
|
906
|
+
content,
|
|
907
|
+
data: toml.parse(content)
|
|
908
|
+
};
|
|
909
|
+
} catch (err) {
|
|
910
|
+
return {
|
|
911
|
+
content: fs.readFileSync(resolved, 'utf-8'),
|
|
912
|
+
data: null
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
_readCodexConfigToml() {
|
|
918
|
+
if (!fs.existsSync(CODEX_CONFIG_PATH)) {
|
|
919
|
+
return {};
|
|
920
|
+
}
|
|
921
|
+
const content = fs.readFileSync(CODEX_CONFIG_PATH, 'utf-8');
|
|
922
|
+
return toml.parse(content);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_writeCodexConfigToml(config) {
|
|
926
|
+
this._ensureDir(path.dirname(CODEX_CONFIG_PATH));
|
|
927
|
+
const tempPath = `${CODEX_CONFIG_PATH}.tmp-${process.pid}-${Date.now()}`;
|
|
928
|
+
fs.writeFileSync(tempPath, tomlStringify(config), 'utf-8');
|
|
929
|
+
fs.renameSync(tempPath, CODEX_CONFIG_PATH);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
module.exports = {
|
|
934
|
+
ConfigSyncManager,
|
|
935
|
+
CONFIG_TYPES,
|
|
936
|
+
CC_TOOL_CONFIGS,
|
|
937
|
+
CLAUDE_CODE_DIR,
|
|
938
|
+
CODEX_DIR,
|
|
939
|
+
GEMINI_DIR,
|
|
940
|
+
OPENCODE_DIR
|
|
941
|
+
};
|