@adversity/coding-tool-x 3.1.0 → 3.1.2
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 +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.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-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +497 -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 +345 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +208 -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 +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +156 -8
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const toml = require('toml');
|
|
5
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
5
6
|
|
|
6
7
|
// Codex 配置文件路径
|
|
7
8
|
function getConfigPath() {
|
|
@@ -35,6 +36,167 @@ function hasBackup() {
|
|
|
35
36
|
return fs.existsSync(getConfigBackupPath()) || fs.existsSync(getAuthBackupPath());
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const INVALID_ENV_NAME_PATTERN = /[\r\n]/;
|
|
40
|
+
const SHELL_MARKER_PREFIX = '# Added by Coding-Tool for Codex';
|
|
41
|
+
|
|
42
|
+
function normalizeEnvName(envName) {
|
|
43
|
+
const normalized = String(envName || '').trim();
|
|
44
|
+
if (!normalized || INVALID_ENV_NAME_PATTERN.test(normalized)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureParentDir(filePath) {
|
|
51
|
+
const dirPath = path.dirname(filePath);
|
|
52
|
+
if (!fs.existsSync(dirPath)) {
|
|
53
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeFileAtomic(filePath, content) {
|
|
58
|
+
ensureParentDir(filePath);
|
|
59
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(tempPath, content, 'utf8');
|
|
63
|
+
fs.renameSync(tempPath, filePath);
|
|
64
|
+
} finally {
|
|
65
|
+
if (fs.existsSync(tempPath)) {
|
|
66
|
+
try {
|
|
67
|
+
fs.unlinkSync(tempPath);
|
|
68
|
+
} catch (cleanupErr) {
|
|
69
|
+
// ignore cleanup errors
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeHomePath(filePath) {
|
|
76
|
+
const normalizedPath = String(filePath || '').replace(/\\/g, '/');
|
|
77
|
+
const normalizedHome = os.homedir().replace(/\\/g, '/');
|
|
78
|
+
if (normalizedPath.startsWith(normalizedHome)) {
|
|
79
|
+
return `~${normalizedPath.slice(normalizedHome.length)}`;
|
|
80
|
+
}
|
|
81
|
+
return filePath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function compactBlankLines(lines) {
|
|
85
|
+
const compacted = [];
|
|
86
|
+
let previousIsBlank = false;
|
|
87
|
+
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const isBlank = line.trim() === '';
|
|
90
|
+
if (isBlank) {
|
|
91
|
+
if (!previousIsBlank) {
|
|
92
|
+
compacted.push('');
|
|
93
|
+
}
|
|
94
|
+
previousIsBlank = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
compacted.push(line);
|
|
99
|
+
previousIsBlank = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
while (compacted.length > 0 && compacted[compacted.length - 1].trim() === '') {
|
|
103
|
+
compacted.pop();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return compacted;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPowerShellProfile(filePath) {
|
|
110
|
+
return String(filePath || '').toLowerCase().endsWith('.ps1');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getShellConfigCandidates() {
|
|
114
|
+
const homeDir = os.homedir();
|
|
115
|
+
const shell = String(process.env.SHELL || '').toLowerCase();
|
|
116
|
+
const candidates = [];
|
|
117
|
+
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
if (shell.includes('zsh')) {
|
|
120
|
+
candidates.push(path.join(homeDir, '.zshrc'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (shell.includes('bash')) {
|
|
124
|
+
candidates.push(path.join(homeDir, '.bashrc'));
|
|
125
|
+
candidates.push(path.join(homeDir, '.bash_profile'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
candidates.push(path.join(homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
|
|
129
|
+
candidates.push(path.join(homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
|
|
130
|
+
candidates.push(path.join(homeDir, '.bashrc'));
|
|
131
|
+
candidates.push(path.join(homeDir, '.profile'));
|
|
132
|
+
} else if (shell.includes('zsh')) {
|
|
133
|
+
candidates.push(path.join(homeDir, '.zshrc'));
|
|
134
|
+
candidates.push(path.join(homeDir, '.zprofile'));
|
|
135
|
+
candidates.push(path.join(homeDir, '.profile'));
|
|
136
|
+
} else if (shell.includes('bash')) {
|
|
137
|
+
if (process.platform === 'darwin') {
|
|
138
|
+
candidates.push(path.join(homeDir, '.bash_profile'));
|
|
139
|
+
candidates.push(path.join(homeDir, '.bashrc'));
|
|
140
|
+
} else {
|
|
141
|
+
candidates.push(path.join(homeDir, '.bashrc'));
|
|
142
|
+
candidates.push(path.join(homeDir, '.bash_profile'));
|
|
143
|
+
}
|
|
144
|
+
candidates.push(path.join(homeDir, '.profile'));
|
|
145
|
+
} else {
|
|
146
|
+
candidates.push(path.join(homeDir, '.zshrc'));
|
|
147
|
+
candidates.push(path.join(homeDir, '.bashrc'));
|
|
148
|
+
candidates.push(path.join(homeDir, '.bash_profile'));
|
|
149
|
+
candidates.push(path.join(homeDir, '.profile'));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return [...new Set(candidates)];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getShellReloadCommand(configPath) {
|
|
156
|
+
if (!configPath) {
|
|
157
|
+
return process.platform === 'win32' ? '重启终端' : 'source ~/.zshrc';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const displayPath = normalizeHomePath(configPath);
|
|
161
|
+
const normalized = String(displayPath || '').replace(/\\/g, '/').toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (normalized.endsWith('microsoft.powershell_profile.ps1')) {
|
|
164
|
+
return '. $PROFILE';
|
|
165
|
+
}
|
|
166
|
+
if (normalized.endsWith('/.zshrc')) {
|
|
167
|
+
return 'source ~/.zshrc';
|
|
168
|
+
}
|
|
169
|
+
if (normalized.endsWith('/.bash_profile')) {
|
|
170
|
+
return 'source ~/.bash_profile';
|
|
171
|
+
}
|
|
172
|
+
if (normalized.endsWith('/.bashrc')) {
|
|
173
|
+
return 'source ~/.bashrc';
|
|
174
|
+
}
|
|
175
|
+
if (normalized.endsWith('/.profile')) {
|
|
176
|
+
return 'source ~/.profile';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (process.platform === 'win32') {
|
|
180
|
+
return '. $PROFILE';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return `source ${displayPath}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function escapeShellValue(value) {
|
|
187
|
+
return String(value ?? '')
|
|
188
|
+
.replace(/\\/g, '\\\\')
|
|
189
|
+
.replace(/"/g, '\\"')
|
|
190
|
+
.replace(/\$/g, '\\$')
|
|
191
|
+
.replace(/`/g, '\\`');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function escapePowerShellValue(value) {
|
|
195
|
+
return String(value ?? '')
|
|
196
|
+
.replace(/`/g, '``')
|
|
197
|
+
.replace(/"/g, '`"');
|
|
198
|
+
}
|
|
199
|
+
|
|
38
200
|
// 读取 config.toml
|
|
39
201
|
function readConfig() {
|
|
40
202
|
try {
|
|
@@ -89,7 +251,8 @@ function configToToml(config) {
|
|
|
89
251
|
// 写入 config.toml
|
|
90
252
|
function writeConfig(config) {
|
|
91
253
|
try {
|
|
92
|
-
const
|
|
254
|
+
const safeConfig = JSON.parse(JSON.stringify(config || {}));
|
|
255
|
+
const content = tomlStringify(safeConfig);
|
|
93
256
|
fs.writeFileSync(getConfigPath(), content, 'utf8');
|
|
94
257
|
} catch (err) {
|
|
95
258
|
throw new Error('Failed to write config.toml: ' + err.message);
|
|
@@ -182,28 +345,28 @@ function restoreSettings() {
|
|
|
182
345
|
|
|
183
346
|
// 获取用户的 shell 配置文件路径
|
|
184
347
|
function getShellConfigPath() {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} else if (shell.includes('bash')) {
|
|
189
|
-
// macOS 使用 .bash_profile,Linux 使用 .bashrc
|
|
190
|
-
const bashProfile = path.join(os.homedir(), '.bash_profile');
|
|
191
|
-
const bashrc = path.join(os.homedir(), '.bashrc');
|
|
192
|
-
if (fs.existsSync(bashProfile)) {
|
|
193
|
-
return bashProfile;
|
|
194
|
-
}
|
|
195
|
-
return bashrc;
|
|
196
|
-
}
|
|
197
|
-
// 默认使用 .zshrc (macOS 默认)
|
|
198
|
-
return path.join(os.homedir(), '.zshrc');
|
|
348
|
+
const candidates = getShellConfigCandidates();
|
|
349
|
+
const existing = candidates.find(filePath => fs.existsSync(filePath));
|
|
350
|
+
return existing || candidates[0];
|
|
199
351
|
}
|
|
200
352
|
|
|
201
353
|
// 注入环境变量到 shell 配置文件
|
|
202
354
|
function injectEnvToShell(envName, envValue) {
|
|
355
|
+
const normalizedEnvName = normalizeEnvName(envName);
|
|
356
|
+
if (!normalizedEnvName) {
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: `Invalid environment variable name: ${envName}`,
|
|
360
|
+
isFirstTime: false
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
203
364
|
const configPath = getShellConfigPath();
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
const
|
|
365
|
+
const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
|
|
366
|
+
const usePowerShell = isPowerShellProfile(configPath);
|
|
367
|
+
const exportLine = usePowerShell
|
|
368
|
+
? `$env:${normalizedEnvName} = "${escapePowerShellValue(envValue)}"`
|
|
369
|
+
: `export ${normalizedEnvName}="${escapeShellValue(envValue)}"`;
|
|
207
370
|
|
|
208
371
|
try {
|
|
209
372
|
let content = '';
|
|
@@ -211,23 +374,52 @@ function injectEnvToShell(envName, envValue) {
|
|
|
211
374
|
content = fs.readFileSync(configPath, 'utf8');
|
|
212
375
|
}
|
|
213
376
|
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
377
|
+
const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
378
|
+
const envLineRegex = usePowerShell
|
|
379
|
+
? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
|
|
380
|
+
: new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
|
|
217
381
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
382
|
+
const originalLines = content ? content.split(/\r?\n/) : [];
|
|
383
|
+
const cleanedLines = [];
|
|
384
|
+
let existed = false;
|
|
385
|
+
|
|
386
|
+
for (let i = 0; i < originalLines.length; i++) {
|
|
387
|
+
const currentLine = originalLines[i];
|
|
388
|
+
const trimmedLine = currentLine.trim();
|
|
389
|
+
|
|
390
|
+
if (trimmedLine === marker) {
|
|
391
|
+
const nextLine = originalLines[i + 1] || '';
|
|
392
|
+
if (envLineRegex.test(nextLine.trim())) {
|
|
393
|
+
i += 1;
|
|
394
|
+
}
|
|
395
|
+
existed = true;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (envLineRegex.test(trimmedLine)) {
|
|
400
|
+
existed = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
cleanedLines.push(currentLine);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
|
|
408
|
+
cleanedLines.pop();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (cleanedLines.length > 0) {
|
|
412
|
+
cleanedLines.push('');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cleanedLines.push(marker, exportLine);
|
|
416
|
+
|
|
417
|
+
const nextContent = `${cleanedLines.join('\n')}\n`;
|
|
418
|
+
if (nextContent !== content) {
|
|
419
|
+
writeFileAtomic(configPath, nextContent);
|
|
227
420
|
}
|
|
228
421
|
|
|
229
|
-
|
|
230
|
-
return { success: true, path: configPath, isFirstTime: !alreadyExists };
|
|
422
|
+
return { success: true, path: configPath, isFirstTime: !existed };
|
|
231
423
|
} catch (err) {
|
|
232
424
|
// 不抛出错误,只是警告,因为这不是致命问题
|
|
233
425
|
console.warn(`[Codex] Failed to inject env to shell config: ${err.message}`);
|
|
@@ -237,6 +429,14 @@ function injectEnvToShell(envName, envValue) {
|
|
|
237
429
|
|
|
238
430
|
// 从 shell 配置文件移除环境变量
|
|
239
431
|
function removeEnvFromShell(envName) {
|
|
432
|
+
const normalizedEnvName = normalizeEnvName(envName);
|
|
433
|
+
if (!normalizedEnvName) {
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
error: `Invalid environment variable name: ${envName}`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
240
440
|
const configPath = getShellConfigPath();
|
|
241
441
|
|
|
242
442
|
try {
|
|
@@ -244,24 +444,46 @@ function removeEnvFromShell(envName) {
|
|
|
244
444
|
return { success: true };
|
|
245
445
|
}
|
|
246
446
|
|
|
247
|
-
|
|
447
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
448
|
+
const usePowerShell = isPowerShellProfile(configPath);
|
|
449
|
+
const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
|
|
450
|
+
const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
451
|
+
const envLineRegex = usePowerShell
|
|
452
|
+
? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
|
|
453
|
+
: new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
|
|
454
|
+
|
|
455
|
+
const originalLines = content ? content.split(/\r?\n/) : [];
|
|
456
|
+
const cleanedLines = [];
|
|
457
|
+
let changed = false;
|
|
458
|
+
|
|
459
|
+
for (let i = 0; i < originalLines.length; i++) {
|
|
460
|
+
const currentLine = originalLines[i];
|
|
461
|
+
const trimmedLine = currentLine.trim();
|
|
462
|
+
|
|
463
|
+
if (trimmedLine === marker) {
|
|
464
|
+
const nextLine = originalLines[i + 1] || '';
|
|
465
|
+
if (envLineRegex.test(nextLine.trim())) {
|
|
466
|
+
i += 1;
|
|
467
|
+
}
|
|
468
|
+
changed = true;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
248
471
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
);
|
|
472
|
+
if (envLineRegex.test(trimmedLine)) {
|
|
473
|
+
changed = true;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
254
476
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
new RegExp(`^export ${envName}=.*\\n?`, 'gm'),
|
|
258
|
-
''
|
|
259
|
-
);
|
|
477
|
+
cleanedLines.push(currentLine);
|
|
478
|
+
}
|
|
260
479
|
|
|
261
|
-
|
|
262
|
-
|
|
480
|
+
if (!changed) {
|
|
481
|
+
return { success: true };
|
|
482
|
+
}
|
|
263
483
|
|
|
264
|
-
|
|
484
|
+
const normalized = compactBlankLines(cleanedLines);
|
|
485
|
+
const nextContent = normalized.length > 0 ? `${normalized.join('\n')}\n` : '';
|
|
486
|
+
writeFileAtomic(configPath, nextContent);
|
|
265
487
|
return { success: true };
|
|
266
488
|
} catch (err) {
|
|
267
489
|
console.warn(`[Codex] Failed to remove env from shell config: ${err.message}`);
|
|
@@ -306,8 +528,8 @@ function setProxyConfig(proxyPort) {
|
|
|
306
528
|
const shellInjectResult = injectEnvToShell('CC_PROXY_KEY', 'PROXY_KEY');
|
|
307
529
|
|
|
308
530
|
// 获取 shell 配置文件路径用于提示信息
|
|
309
|
-
const shellConfigPath = getShellConfigPath();
|
|
310
|
-
const sourceCommand =
|
|
531
|
+
const shellConfigPath = shellInjectResult.path || getShellConfigPath();
|
|
532
|
+
const sourceCommand = getShellReloadCommand(shellConfigPath);
|
|
311
533
|
|
|
312
534
|
console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
|
|
313
535
|
return {
|
|
@@ -341,7 +563,7 @@ function isProxyConfig() {
|
|
|
341
563
|
const currentProvider = config.model_provider;
|
|
342
564
|
if (currentProvider && config.model_providers && config.model_providers[currentProvider]) {
|
|
343
565
|
const baseUrl = config.model_providers[currentProvider].base_url || '';
|
|
344
|
-
if (baseUrl.includes('127.0.0.1')
|
|
566
|
+
if (baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost')) {
|
|
345
567
|
return true;
|
|
346
568
|
}
|
|
347
569
|
}
|
|
@@ -6,7 +6,7 @@ const os = require('os');
|
|
|
6
6
|
* Codex 统计服务 - 数据采集和存储
|
|
7
7
|
*
|
|
8
8
|
* 文件结构:
|
|
9
|
-
* ~/.
|
|
9
|
+
* ~/.cc-tool/
|
|
10
10
|
* ├── codex-statistics.json # Codex 总体统计
|
|
11
11
|
* └── codex-daily-stats/
|
|
12
12
|
* ├── 2025-12-05.json # 每日汇总统计
|
|
@@ -15,7 +15,7 @@ const os = require('os');
|
|
|
15
15
|
|
|
16
16
|
// 获取基础目录
|
|
17
17
|
function getBaseDir() {
|
|
18
|
-
const dir = path.join(os.homedir(), '.
|
|
18
|
+
const dir = path.join(os.homedir(), '.cc-tool');
|
|
19
19
|
if (!fs.existsSync(dir)) {
|
|
20
20
|
fs.mkdirSync(dir, { recursive: true });
|
|
21
21
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commands 服务
|
|
3
3
|
*
|
|
4
|
-
* 管理 Claude
|
|
5
|
-
* 命令目录:
|
|
6
|
-
* - 用户级: ~/.claude/commands/
|
|
7
|
-
* - 项目级: .claude/commands/
|
|
8
|
-
*
|
|
4
|
+
* 管理 Claude/OpenCode 自定义命令的 CRUD 操作
|
|
9
5
|
* 支持从 GitHub 仓库扫描和安装命令
|
|
10
6
|
*/
|
|
11
7
|
|
|
@@ -13,6 +9,7 @@ const fs = require('fs');
|
|
|
13
9
|
const path = require('path');
|
|
14
10
|
const os = require('os');
|
|
15
11
|
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
12
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
16
13
|
const {
|
|
17
14
|
parseCommandContent,
|
|
18
15
|
detectCommandFormat,
|
|
@@ -21,11 +18,35 @@ const {
|
|
|
21
18
|
parseFrontmatter
|
|
22
19
|
} = require('./format-converter');
|
|
23
20
|
|
|
24
|
-
// 命令目录路径
|
|
25
|
-
const USER_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
|
|
26
|
-
|
|
27
21
|
// 默认仓库源
|
|
28
22
|
const DEFAULT_REPOS = [];
|
|
23
|
+
const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
|
|
24
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
25
|
+
|
|
26
|
+
const PLATFORM_CONFIG = {
|
|
27
|
+
claude: {
|
|
28
|
+
userCommandsDir: path.join(os.homedir(), '.claude', 'commands'),
|
|
29
|
+
projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
|
|
30
|
+
repoType: 'commands'
|
|
31
|
+
},
|
|
32
|
+
opencode: {
|
|
33
|
+
userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
|
|
34
|
+
legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
|
|
35
|
+
projectCommandsDir: (projectPath) => {
|
|
36
|
+
const modern = path.join(projectPath, '.opencode', 'commands');
|
|
37
|
+
const legacy = path.join(projectPath, '.opencode', 'command');
|
|
38
|
+
if (fs.existsSync(legacy) && !fs.existsSync(modern)) {
|
|
39
|
+
return legacy;
|
|
40
|
+
}
|
|
41
|
+
return modern;
|
|
42
|
+
},
|
|
43
|
+
repoType: 'opencode-commands'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function normalizePlatform(platform) {
|
|
48
|
+
return SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
|
|
49
|
+
}
|
|
29
50
|
|
|
30
51
|
/**
|
|
31
52
|
* 确保目录存在
|
|
@@ -60,6 +81,9 @@ function generateCommandFrontmatter(data) {
|
|
|
60
81
|
if (data.agent) {
|
|
61
82
|
lines.push(`agent: ${data.agent}`);
|
|
62
83
|
}
|
|
84
|
+
if (typeof data.subtask === 'boolean') {
|
|
85
|
+
lines.push(`subtask: ${data.subtask}`);
|
|
86
|
+
}
|
|
63
87
|
|
|
64
88
|
lines.push('---');
|
|
65
89
|
return lines.join('\n');
|
|
@@ -105,6 +129,9 @@ function scanCommandsDir(dir, basePath, scope) {
|
|
|
105
129
|
description: frontmatter.description || '',
|
|
106
130
|
allowedTools: frontmatter['allowed-tools'] || '',
|
|
107
131
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
132
|
+
agent: frontmatter.agent || '',
|
|
133
|
+
model: frontmatter.model || '',
|
|
134
|
+
subtask: frontmatter.subtask || '',
|
|
108
135
|
body,
|
|
109
136
|
fullContent: content,
|
|
110
137
|
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
@@ -125,10 +152,10 @@ function scanCommandsDir(dir, basePath, scope) {
|
|
|
125
152
|
* Commands 仓库扫描器
|
|
126
153
|
*/
|
|
127
154
|
class CommandsRepoScanner extends RepoScannerBase {
|
|
128
|
-
constructor() {
|
|
155
|
+
constructor(platform, installDir) {
|
|
129
156
|
super({
|
|
130
|
-
type: 'commands',
|
|
131
|
-
installDir
|
|
157
|
+
type: PLATFORM_CONFIG[platform]?.repoType || 'commands',
|
|
158
|
+
installDir,
|
|
132
159
|
markerFile: null, // 直接扫描 .md 文件
|
|
133
160
|
fileExtension: '.md',
|
|
134
161
|
defaultRepos: DEFAULT_REPOS
|
|
@@ -159,6 +186,9 @@ class CommandsRepoScanner extends RepoScannerBase {
|
|
|
159
186
|
description: frontmatter.description || '',
|
|
160
187
|
allowedTools: frontmatter['allowed-tools'] || '',
|
|
161
188
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
189
|
+
agent: frontmatter.agent || '',
|
|
190
|
+
model: frontmatter.model || '',
|
|
191
|
+
subtask: frontmatter.subtask || '',
|
|
162
192
|
body,
|
|
163
193
|
fullContent: content,
|
|
164
194
|
installed: this.isInstalled(relativePath),
|
|
@@ -208,12 +238,28 @@ class CommandsRepoScanner extends RepoScannerBase {
|
|
|
208
238
|
* Commands 服务类
|
|
209
239
|
*/
|
|
210
240
|
class CommandsService {
|
|
211
|
-
constructor() {
|
|
212
|
-
this.
|
|
213
|
-
|
|
241
|
+
constructor(platform = 'claude') {
|
|
242
|
+
this.platform = normalizePlatform(platform);
|
|
243
|
+
const config = PLATFORM_CONFIG[this.platform];
|
|
244
|
+
|
|
245
|
+
this.userCommandsDir = config.userCommandsDir;
|
|
246
|
+
if (this.platform === 'opencode') {
|
|
247
|
+
const legacyUserDir = config.legacyUserCommandsDir;
|
|
248
|
+
if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userCommandsDir)) {
|
|
249
|
+
this.userCommandsDir = legacyUserDir;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.projectCommandsDir = config.projectCommandsDir;
|
|
254
|
+
this.repoScanner = new CommandsRepoScanner(this.platform, this.userCommandsDir);
|
|
214
255
|
ensureDir(this.userCommandsDir);
|
|
215
256
|
}
|
|
216
257
|
|
|
258
|
+
getProjectCommandsDir(projectPath) {
|
|
259
|
+
if (!projectPath) return null;
|
|
260
|
+
return this.projectCommandsDir(projectPath);
|
|
261
|
+
}
|
|
262
|
+
|
|
217
263
|
/**
|
|
218
264
|
* 获取所有命令列表
|
|
219
265
|
* @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
|
|
@@ -227,7 +273,7 @@ class CommandsService {
|
|
|
227
273
|
|
|
228
274
|
// 获取项目级命令(如果提供了项目路径)
|
|
229
275
|
if (projectPath) {
|
|
230
|
-
const projectCommandsDir =
|
|
276
|
+
const projectCommandsDir = this.getProjectCommandsDir(projectPath);
|
|
231
277
|
const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
|
|
232
278
|
commands.push(...projectCommands);
|
|
233
279
|
}
|
|
@@ -295,7 +341,7 @@ class CommandsService {
|
|
|
295
341
|
getCommand(name, scope, projectPath = null, namespace = null) {
|
|
296
342
|
const baseDir = scope === 'user'
|
|
297
343
|
? this.userCommandsDir
|
|
298
|
-
:
|
|
344
|
+
: this.getProjectCommandsDir(projectPath);
|
|
299
345
|
|
|
300
346
|
const relativePath = namespace
|
|
301
347
|
? path.join(namespace, `${name}.md`)
|
|
@@ -319,6 +365,9 @@ class CommandsService {
|
|
|
319
365
|
description: frontmatter.description || '',
|
|
320
366
|
allowedTools: frontmatter['allowed-tools'] || '',
|
|
321
367
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
368
|
+
agent: frontmatter.agent || '',
|
|
369
|
+
model: frontmatter.model || '',
|
|
370
|
+
subtask: frontmatter.subtask || '',
|
|
322
371
|
body,
|
|
323
372
|
fullContent: content,
|
|
324
373
|
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
@@ -328,7 +377,7 @@ class CommandsService {
|
|
|
328
377
|
/**
|
|
329
378
|
* 创建命令
|
|
330
379
|
*/
|
|
331
|
-
createCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, body }) {
|
|
380
|
+
createCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
|
|
332
381
|
if (!name || !name.trim()) {
|
|
333
382
|
throw new Error('命令名称不能为空');
|
|
334
383
|
}
|
|
@@ -340,7 +389,7 @@ class CommandsService {
|
|
|
340
389
|
|
|
341
390
|
const baseDir = scope === 'user'
|
|
342
391
|
? this.userCommandsDir
|
|
343
|
-
:
|
|
392
|
+
: this.getProjectCommandsDir(projectPath);
|
|
344
393
|
|
|
345
394
|
const targetDir = namespace ? path.join(baseDir, namespace) : baseDir;
|
|
346
395
|
ensureDir(targetDir);
|
|
@@ -355,8 +404,13 @@ class CommandsService {
|
|
|
355
404
|
// 生成文件内容
|
|
356
405
|
const frontmatterData = {};
|
|
357
406
|
if (description) frontmatterData.description = description;
|
|
358
|
-
if (
|
|
359
|
-
|
|
407
|
+
if (this.platform !== 'opencode') {
|
|
408
|
+
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
409
|
+
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
410
|
+
}
|
|
411
|
+
if (agent) frontmatterData.agent = agent;
|
|
412
|
+
if (model) frontmatterData.model = model;
|
|
413
|
+
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
360
414
|
|
|
361
415
|
let content = '';
|
|
362
416
|
if (Object.keys(frontmatterData).length > 0) {
|
|
@@ -372,10 +426,10 @@ class CommandsService {
|
|
|
372
426
|
/**
|
|
373
427
|
* 更新命令
|
|
374
428
|
*/
|
|
375
|
-
updateCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, body }) {
|
|
429
|
+
updateCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
|
|
376
430
|
const baseDir = scope === 'user'
|
|
377
431
|
? this.userCommandsDir
|
|
378
|
-
:
|
|
432
|
+
: this.getProjectCommandsDir(projectPath);
|
|
379
433
|
|
|
380
434
|
const relativePath = namespace
|
|
381
435
|
? path.join(namespace, `${name}.md`)
|
|
@@ -390,8 +444,13 @@ class CommandsService {
|
|
|
390
444
|
// 生成文件内容
|
|
391
445
|
const frontmatterData = {};
|
|
392
446
|
if (description) frontmatterData.description = description;
|
|
393
|
-
if (
|
|
394
|
-
|
|
447
|
+
if (this.platform !== 'opencode') {
|
|
448
|
+
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
449
|
+
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
450
|
+
}
|
|
451
|
+
if (agent) frontmatterData.agent = agent;
|
|
452
|
+
if (model) frontmatterData.model = model;
|
|
453
|
+
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
395
454
|
|
|
396
455
|
let content = '';
|
|
397
456
|
if (Object.keys(frontmatterData).length > 0) {
|
|
@@ -410,7 +469,7 @@ class CommandsService {
|
|
|
410
469
|
deleteCommand(name, scope, projectPath = null, namespace = null) {
|
|
411
470
|
const baseDir = scope === 'user'
|
|
412
471
|
? this.userCommandsDir
|
|
413
|
-
:
|
|
472
|
+
: this.getProjectCommandsDir(projectPath);
|
|
414
473
|
|
|
415
474
|
const relativePath = namespace
|
|
416
475
|
? path.join(namespace, `${name}.md`)
|