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,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 环境变量管理服务
|
|
3
|
+
*
|
|
4
|
+
* 负责备份、删除和恢复环境变量
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
10
|
+
|
|
11
|
+
// 备份目录
|
|
12
|
+
const BACKUP_DIR = PATHS.envBackups;
|
|
13
|
+
|
|
14
|
+
function ensureParentDir(filePath) {
|
|
15
|
+
const dirPath = path.dirname(filePath);
|
|
16
|
+
if (!fs.existsSync(dirPath)) {
|
|
17
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeFileAtomic(filePath, content) {
|
|
22
|
+
ensureParentDir(filePath);
|
|
23
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
fs.writeFileSync(tempPath, content, 'utf-8');
|
|
27
|
+
fs.renameSync(tempPath, filePath);
|
|
28
|
+
} finally {
|
|
29
|
+
if (fs.existsSync(tempPath)) {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(tempPath);
|
|
32
|
+
} catch (cleanupErr) {
|
|
33
|
+
// ignore cleanup errors
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeRegex(value) {
|
|
40
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPowerShellProfile(filePath) {
|
|
44
|
+
return String(filePath || '').toLowerCase().endsWith('.ps1');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function matchesVarAssignment(line, varName, usePowerShell) {
|
|
48
|
+
if (!line || !varName) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const escaped = escapeRegex(varName);
|
|
53
|
+
const regex = usePowerShell
|
|
54
|
+
? new RegExp(`^\\s*\\$env:${escaped}\\s*=`, 'i')
|
|
55
|
+
: new RegExp(`^\\s*(?:export\\s+)?${escaped}=`);
|
|
56
|
+
return regex.test(line.trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatExportLine(varName, value, usePowerShell) {
|
|
60
|
+
if (usePowerShell) {
|
|
61
|
+
const escapedValue = String(value ?? '')
|
|
62
|
+
.replace(/`/g, '``')
|
|
63
|
+
.replace(/"/g, '`"');
|
|
64
|
+
return `$env:${varName} = "${escapedValue}"`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const escapedValue = String(value ?? '')
|
|
68
|
+
.replace(/\\/g, '\\\\')
|
|
69
|
+
.replace(/"/g, '\\"')
|
|
70
|
+
.replace(/\$/g, '\\$')
|
|
71
|
+
.replace(/`/g, '\\`');
|
|
72
|
+
return `export ${varName}="${escapedValue}"`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 确保备份目录存在
|
|
77
|
+
*/
|
|
78
|
+
function ensureBackupDir() {
|
|
79
|
+
ensureStorageDirMigrated();
|
|
80
|
+
if (!fs.existsSync(BACKUP_DIR)) {
|
|
81
|
+
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 删除环境变量(带自动备份)
|
|
87
|
+
* @param {Array} conflicts - 要删除的冲突列表
|
|
88
|
+
* @returns {Object} 备份信息
|
|
89
|
+
*/
|
|
90
|
+
function deleteEnvVars(conflicts) {
|
|
91
|
+
if (!conflicts || conflicts.length === 0) {
|
|
92
|
+
throw new Error('没有选择要删除的环境变量');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 只处理文件类型的环境变量(进程环境变量无法删除)
|
|
96
|
+
const fileConflicts = conflicts.filter(c => c.sourceType === 'file');
|
|
97
|
+
const processConflicts = conflicts.filter(c => c.sourceType === 'process');
|
|
98
|
+
|
|
99
|
+
if (fileConflicts.length === 0 && processConflicts.length > 0) {
|
|
100
|
+
throw new Error('进程环境变量无法直接删除,请手动从配置文件中移除');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 1. 创建备份
|
|
104
|
+
const backupInfo = createBackup(conflicts);
|
|
105
|
+
|
|
106
|
+
// 2. 从文件中删除
|
|
107
|
+
const results = [];
|
|
108
|
+
const fileGroups = groupByFile(fileConflicts);
|
|
109
|
+
|
|
110
|
+
for (const [filePath, vars] of Object.entries(fileGroups)) {
|
|
111
|
+
try {
|
|
112
|
+
removeVarsFromFile(filePath, vars);
|
|
113
|
+
results.push({
|
|
114
|
+
filePath,
|
|
115
|
+
success: true,
|
|
116
|
+
removedVars: vars.map(v => v.varName)
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
results.push({
|
|
120
|
+
filePath,
|
|
121
|
+
success: false,
|
|
122
|
+
error: err.message
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 同步清理当前服务进程中的同名变量,避免“删除后立即复检仍提示冲突”
|
|
128
|
+
const clearedProcessVars = clearProcessEnvVars(conflicts);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
backupPath: backupInfo.backupPath,
|
|
132
|
+
timestamp: backupInfo.timestamp,
|
|
133
|
+
results,
|
|
134
|
+
processConflictsSkipped: processConflicts.length,
|
|
135
|
+
clearedProcessVars
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 创建备份
|
|
141
|
+
*/
|
|
142
|
+
function createBackup(conflicts) {
|
|
143
|
+
ensureBackupDir();
|
|
144
|
+
|
|
145
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
146
|
+
const backupFile = path.join(BACKUP_DIR, `env-backup-${timestamp}.json`);
|
|
147
|
+
|
|
148
|
+
const backupData = {
|
|
149
|
+
timestamp,
|
|
150
|
+
createdAt: Date.now(),
|
|
151
|
+
conflicts: conflicts.map(c => ({
|
|
152
|
+
...c,
|
|
153
|
+
// 存储完整值用于恢复(不遮蔽)
|
|
154
|
+
originalValue: getOriginalValue(c)
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2), 'utf-8');
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
backupPath: backupFile,
|
|
162
|
+
timestamp
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 获取原始值(从文件中重新读取)
|
|
168
|
+
*/
|
|
169
|
+
function getOriginalValue(conflict) {
|
|
170
|
+
if (conflict.sourceType !== 'file' || !conflict.filePath || !conflict.lineNumber) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const content = fs.readFileSync(conflict.filePath, 'utf-8');
|
|
176
|
+
const lines = content.split('\n');
|
|
177
|
+
const line = lines[conflict.lineNumber - 1];
|
|
178
|
+
|
|
179
|
+
if (line) {
|
|
180
|
+
const match = line.match(/^(?:export\s+)?[A-Z_][A-Z0-9_]*=(.*)$/);
|
|
181
|
+
if (match && match[1] !== undefined) {
|
|
182
|
+
return cleanValue(match[1]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const psMatch = line.match(/^\s*\$env:[A-Z_][A-Z0-9_]*\s*=\s*(.*)$/i);
|
|
186
|
+
if (psMatch && psMatch[1] !== undefined) {
|
|
187
|
+
return cleanValue(psMatch[1]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// 忽略
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 清理变量值
|
|
199
|
+
*/
|
|
200
|
+
function cleanValue(value) {
|
|
201
|
+
let cleaned = value.trim();
|
|
202
|
+
if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
|
203
|
+
(cleaned.startsWith("'") && cleaned.endsWith("'"))) {
|
|
204
|
+
cleaned = cleaned.slice(1, -1);
|
|
205
|
+
}
|
|
206
|
+
return cleaned;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 按文件分组
|
|
211
|
+
*/
|
|
212
|
+
function groupByFile(conflicts) {
|
|
213
|
+
const groups = {};
|
|
214
|
+
|
|
215
|
+
for (const conflict of conflicts) {
|
|
216
|
+
if (conflict.filePath) {
|
|
217
|
+
if (!groups[conflict.filePath]) {
|
|
218
|
+
groups[conflict.filePath] = [];
|
|
219
|
+
}
|
|
220
|
+
groups[conflict.filePath].push(conflict);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return groups;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 清理当前 Node 进程中的环境变量
|
|
229
|
+
*/
|
|
230
|
+
function clearProcessEnvVars(conflicts) {
|
|
231
|
+
const names = new Set();
|
|
232
|
+
|
|
233
|
+
for (const conflict of conflicts || []) {
|
|
234
|
+
const varName = String(conflict?.varName || '').trim();
|
|
235
|
+
if (varName) {
|
|
236
|
+
names.add(varName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cleared = [];
|
|
241
|
+
for (const varName of names) {
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(process.env, varName)) {
|
|
243
|
+
delete process.env[varName];
|
|
244
|
+
cleared.push(varName);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return cleared;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 从文件中移除环境变量
|
|
253
|
+
*/
|
|
254
|
+
function removeVarsFromFile(filePath, vars) {
|
|
255
|
+
if (!fs.existsSync(filePath)) {
|
|
256
|
+
throw new Error(`文件不存在: ${filePath}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
260
|
+
const lines = content.split(/\r?\n/);
|
|
261
|
+
const usePowerShell = isPowerShellProfile(filePath);
|
|
262
|
+
|
|
263
|
+
// 收集要删除的行号(优先使用行号,行号不匹配时按变量名回退)
|
|
264
|
+
const lineNumbersToRemove = new Set();
|
|
265
|
+
for (const item of vars) {
|
|
266
|
+
const varName = String(item.varName || '').trim();
|
|
267
|
+
if (!varName) continue;
|
|
268
|
+
|
|
269
|
+
const lineIndex = Number(item.lineNumber) - 1;
|
|
270
|
+
if (
|
|
271
|
+
Number.isInteger(lineIndex) &&
|
|
272
|
+
lineIndex >= 0 &&
|
|
273
|
+
lineIndex < lines.length &&
|
|
274
|
+
matchesVarAssignment(lines[lineIndex], varName, usePowerShell)
|
|
275
|
+
) {
|
|
276
|
+
lineNumbersToRemove.add(lineIndex);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const fallbackIndex = lines.findIndex(line => matchesVarAssignment(line, varName, usePowerShell));
|
|
281
|
+
if (fallbackIndex >= 0) {
|
|
282
|
+
lineNumbersToRemove.add(fallbackIndex);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (lineNumbersToRemove.size === 0) {
|
|
287
|
+
console.log(`[EnvManager] No matching vars found in ${filePath}, skip writing`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const newLines = lines.filter((_, index) => !lineNumbersToRemove.has(index));
|
|
292
|
+
const nextContent = newLines.join('\n');
|
|
293
|
+
if (nextContent !== content) {
|
|
294
|
+
writeFileAtomic(filePath, nextContent);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log(`[EnvManager] Removed ${lineNumbersToRemove.size} var(s) from ${filePath}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 获取备份列表
|
|
302
|
+
*/
|
|
303
|
+
function getBackupList() {
|
|
304
|
+
ensureBackupDir();
|
|
305
|
+
|
|
306
|
+
const files = fs.readdirSync(BACKUP_DIR)
|
|
307
|
+
.filter(f => f.startsWith('env-backup-') && f.endsWith('.json'))
|
|
308
|
+
.sort()
|
|
309
|
+
.reverse(); // 最新的在前
|
|
310
|
+
|
|
311
|
+
return files.map(fileName => {
|
|
312
|
+
const filePath = path.join(BACKUP_DIR, fileName);
|
|
313
|
+
try {
|
|
314
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
315
|
+
const data = JSON.parse(content);
|
|
316
|
+
return {
|
|
317
|
+
fileName,
|
|
318
|
+
filePath,
|
|
319
|
+
timestamp: data.timestamp,
|
|
320
|
+
createdAt: data.createdAt,
|
|
321
|
+
conflictCount: data.conflicts?.length || 0
|
|
322
|
+
};
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return {
|
|
325
|
+
fileName,
|
|
326
|
+
filePath,
|
|
327
|
+
error: err.message
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 从备份恢复
|
|
335
|
+
*/
|
|
336
|
+
function restoreFromBackup(backupPath) {
|
|
337
|
+
if (!fs.existsSync(backupPath)) {
|
|
338
|
+
throw new Error(`备份文件不存在: ${backupPath}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const content = fs.readFileSync(backupPath, 'utf-8');
|
|
342
|
+
const backupData = JSON.parse(content);
|
|
343
|
+
|
|
344
|
+
const results = [];
|
|
345
|
+
|
|
346
|
+
for (const conflict of backupData.conflicts) {
|
|
347
|
+
if (conflict.sourceType !== 'file' || !conflict.filePath || !conflict.originalValue) {
|
|
348
|
+
results.push({
|
|
349
|
+
varName: conflict.varName,
|
|
350
|
+
success: false,
|
|
351
|
+
error: '无法恢复非文件类型的环境变量'
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
restoreVarToFile(conflict.filePath, conflict.varName, conflict.originalValue);
|
|
358
|
+
results.push({
|
|
359
|
+
varName: conflict.varName,
|
|
360
|
+
filePath: conflict.filePath,
|
|
361
|
+
success: true
|
|
362
|
+
});
|
|
363
|
+
} catch (err) {
|
|
364
|
+
results.push({
|
|
365
|
+
varName: conflict.varName,
|
|
366
|
+
success: false,
|
|
367
|
+
error: err.message
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { results };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 恢复环境变量到文件
|
|
377
|
+
*/
|
|
378
|
+
function restoreVarToFile(filePath, varName, value) {
|
|
379
|
+
const usePowerShell = isPowerShellProfile(filePath);
|
|
380
|
+
const exportLine = formatExportLine(varName, value, usePowerShell);
|
|
381
|
+
let content = '';
|
|
382
|
+
|
|
383
|
+
if (fs.existsSync(filePath)) {
|
|
384
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const lines = content ? content.split(/\r?\n/) : [];
|
|
388
|
+
let replaced = false;
|
|
389
|
+
|
|
390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
391
|
+
if (matchesVarAssignment(lines[i], varName, usePowerShell)) {
|
|
392
|
+
lines[i] = exportLine;
|
|
393
|
+
replaced = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!replaced) {
|
|
398
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
399
|
+
lines.pop();
|
|
400
|
+
}
|
|
401
|
+
if (lines.length > 0) {
|
|
402
|
+
lines.push('');
|
|
403
|
+
}
|
|
404
|
+
lines.push(exportLine);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const nextContent = `${lines.join('\n')}\n`;
|
|
408
|
+
writeFileAtomic(filePath, nextContent);
|
|
409
|
+
|
|
410
|
+
console.log(`[EnvManager] Restored ${varName} to ${filePath}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 删除备份文件
|
|
415
|
+
*/
|
|
416
|
+
function deleteBackup(backupPath) {
|
|
417
|
+
if (!fs.existsSync(backupPath)) {
|
|
418
|
+
throw new Error(`备份文件不存在: ${backupPath}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 安全检查:确保是备份目录下的文件
|
|
422
|
+
if (!backupPath.startsWith(BACKUP_DIR)) {
|
|
423
|
+
throw new Error('无效的备份文件路径');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fs.unlinkSync(backupPath);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = {
|
|
431
|
+
deleteEnvVars,
|
|
432
|
+
getBackupList,
|
|
433
|
+
restoreFromBackup,
|
|
434
|
+
deleteBackup,
|
|
435
|
+
BACKUP_DIR
|
|
436
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const FAVORITES_DIR = path.join(os.homedir(), '.cc-tool');
|
|
6
|
+
const FAVORITES_FILE = path.join(FAVORITES_DIR, 'favorites.json');
|
|
7
|
+
|
|
8
|
+
// 内存缓存
|
|
9
|
+
let favoritesCache = null;
|
|
10
|
+
let cacheInitialized = false;
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FAVORITES = {
|
|
13
|
+
claude: [],
|
|
14
|
+
codex: [],
|
|
15
|
+
gemini: [],
|
|
16
|
+
opencode: []
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Ensure favorites directory exists
|
|
20
|
+
function ensureFavoritesDir() {
|
|
21
|
+
if (!fs.existsSync(FAVORITES_DIR)) {
|
|
22
|
+
fs.mkdirSync(FAVORITES_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 从文件读取并缓存
|
|
27
|
+
function readFavoritesFromFile() {
|
|
28
|
+
ensureFavoritesDir();
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(FAVORITES_FILE)) {
|
|
31
|
+
return { ...DEFAULT_FAVORITES };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(FAVORITES_FILE, 'utf8');
|
|
36
|
+
const data = JSON.parse(content);
|
|
37
|
+
return {
|
|
38
|
+
claude: data.claude || [],
|
|
39
|
+
codex: data.codex || [],
|
|
40
|
+
gemini: data.gemini || [],
|
|
41
|
+
opencode: data.opencode || []
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error reading favorites file:', error);
|
|
45
|
+
return { ...DEFAULT_FAVORITES };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 初始化缓存(延迟初始化)
|
|
50
|
+
function initializeCache() {
|
|
51
|
+
if (cacheInitialized) return;
|
|
52
|
+
favoritesCache = readFavoritesFromFile();
|
|
53
|
+
cacheInitialized = true;
|
|
54
|
+
|
|
55
|
+
// 监听文件变化,更新缓存
|
|
56
|
+
try {
|
|
57
|
+
fs.watchFile(FAVORITES_FILE, { persistent: false }, () => {
|
|
58
|
+
favoritesCache = readFavoritesFromFile();
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('Failed to watch favorites file:', err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Load all favorites(使用缓存)
|
|
66
|
+
function loadFavorites() {
|
|
67
|
+
if (!cacheInitialized) {
|
|
68
|
+
initializeCache();
|
|
69
|
+
}
|
|
70
|
+
return JSON.parse(JSON.stringify(favoritesCache)); // 深拷贝返回
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Save favorites(同时更新缓存)
|
|
74
|
+
function saveFavorites(favorites) {
|
|
75
|
+
ensureFavoritesDir();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
fs.writeFileSync(FAVORITES_FILE, JSON.stringify(favorites, null, 2), 'utf8');
|
|
79
|
+
// 同时更新缓存
|
|
80
|
+
favoritesCache = JSON.parse(JSON.stringify(favorites));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error saving favorites:', error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add a favorite
|
|
88
|
+
function addFavorite(channel, sessionData) {
|
|
89
|
+
const favorites = loadFavorites();
|
|
90
|
+
|
|
91
|
+
if (!favorites[channel]) {
|
|
92
|
+
favorites[channel] = [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if already exists
|
|
96
|
+
const exists = favorites[channel].some(
|
|
97
|
+
fav => fav.sessionId === sessionData.sessionId && fav.projectName === sessionData.projectName
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!exists) {
|
|
101
|
+
favorites[channel].push({
|
|
102
|
+
...sessionData,
|
|
103
|
+
addedAt: Date.now()
|
|
104
|
+
});
|
|
105
|
+
saveFavorites(favorites);
|
|
106
|
+
return { success: true, favorites };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { success: false, message: 'Already exists', favorites };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Remove a favorite
|
|
113
|
+
function removeFavorite(channel, projectName, sessionId) {
|
|
114
|
+
const favorites = loadFavorites();
|
|
115
|
+
|
|
116
|
+
if (!favorites[channel]) {
|
|
117
|
+
return { success: false, message: 'Channel not found', favorites };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const index = favorites[channel].findIndex(
|
|
121
|
+
fav => fav.sessionId === sessionId && fav.projectName === projectName
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (index > -1) {
|
|
125
|
+
favorites[channel].splice(index, 1);
|
|
126
|
+
saveFavorites(favorites);
|
|
127
|
+
return { success: true, favorites };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { success: false, message: 'Not found', favorites };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if a session is favorited
|
|
134
|
+
function isFavorite(channel, projectName, sessionId) {
|
|
135
|
+
const favorites = loadFavorites();
|
|
136
|
+
|
|
137
|
+
if (!favorites[channel]) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return favorites[channel].some(
|
|
142
|
+
fav => fav.sessionId === sessionId && fav.projectName === projectName
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get favorites for a specific channel
|
|
147
|
+
function getFavorites(channel) {
|
|
148
|
+
const favorites = loadFavorites();
|
|
149
|
+
return favorites[channel] || [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Get all favorites
|
|
153
|
+
function getAllFavorites() {
|
|
154
|
+
return loadFavorites();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
loadFavorites,
|
|
159
|
+
saveFavorites,
|
|
160
|
+
addFavorite,
|
|
161
|
+
removeFavorite,
|
|
162
|
+
isFavorite,
|
|
163
|
+
getFavorites,
|
|
164
|
+
getAllFavorites
|
|
165
|
+
};
|