coding-tool-x 3.3.9 → 3.4.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/dist/web/assets/{Analytics-D6LzK9hk.js → Analytics-DEjfL5Jx.js} +4 -4
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-BUDYuxRi.js → ConfigTemplates-DkRL_-tf.js} +1 -1
- package/dist/web/assets/{Home-D7KX7iF8.js → Home-CF-L640I.js} +1 -1
- package/dist/web/assets/{PluginManager-DTgQ--vB.js → PluginManager-BzNYTdNB.js} +1 -1
- package/dist/web/assets/{ProjectList-DMCiGmCT.js → ProjectList-C0-JgHMM.js} +1 -1
- package/dist/web/assets/{SessionList-CRBsdVRe.js → SessionList-CkZUdX5N.js} +1 -1
- package/dist/web/assets/{SkillManager-DMwx2Q4k.js → SkillManager-Cak0-4d4.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DapB4ljL.js → WorkspaceManager-CGDJzwEr.js} +1 -1
- package/dist/web/assets/{index-CL-qpoJ_.js → index-D_WItvHE.js} +2 -2
- package/dist/web/assets/{index-D_5dRFOL.css → index-Dz7v9OM0.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/api/statistics.js +4 -4
- package/src/server/api/workspaces.js +1 -3
- package/src/server/services/codex-channels.js +6 -9
- package/src/server/services/codex-env-manager.js +63 -60
- package/src/server/services/statistics-service.js +5 -1
- package/src/server/services/workspace-service.js +100 -155
- package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>CC-TOOL - ClaudeCode增强工作助手</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D_WItvHE.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-3bf-fPGP.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendors-CKPV1OAU.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-Bdxp09n2.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/icons-B5Pl4lrD.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Dz7v9OM0.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -151,8 +151,8 @@ router.get('/trend', async (req, res) => {
|
|
|
151
151
|
return res.status(400).json({ error: 'Hour granularity is limited to 7 days' });
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
if (diffDays >
|
|
155
|
-
return res.status(400).json({ error: 'Date range cannot exceed
|
|
154
|
+
if (diffDays > 365) {
|
|
155
|
+
return res.status(400).json({ error: 'Date range cannot exceed 365 days' });
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const filters = {
|
|
@@ -204,8 +204,8 @@ router.get('/trend/export', async (req, res) => {
|
|
|
204
204
|
return res.status(400).json({ error: 'Hour granularity is limited to 7 days' });
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
if (diffDays >
|
|
208
|
-
return res.status(400).json({ error: 'Date range cannot exceed
|
|
207
|
+
if (diffDays > 365) {
|
|
208
|
+
return res.status(400).json({ error: 'Date range cannot exceed 365 days' });
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
const result = await getTrendStatistics({ startDate, endDate, granularity, step, groupBy, metric });
|
|
@@ -402,12 +402,10 @@ router.post('/:id/projects', (req, res) => {
|
|
|
402
402
|
router.delete('/:id/projects/:projectName', (req, res) => {
|
|
403
403
|
try {
|
|
404
404
|
const { id, projectName } = req.params;
|
|
405
|
-
const removeWorktrees = req.query.removeWorktrees === 'true';
|
|
406
405
|
|
|
407
406
|
const workspace = workspaceService.removeProjectFromWorkspace(
|
|
408
407
|
id,
|
|
409
|
-
projectName
|
|
410
|
-
removeWorktrees
|
|
408
|
+
projectName
|
|
411
409
|
);
|
|
412
410
|
|
|
413
411
|
res.json({
|
|
@@ -34,18 +34,15 @@ function normalizeGatewaySourceType(value, fallback = 'codex') {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const channel of channels) {
|
|
40
|
-
if (!channel?.envKey || !channel?.apiKey) continue;
|
|
41
|
-
envMap[channel.envKey] = channel.apiKey;
|
|
42
|
-
}
|
|
43
|
-
|
|
37
|
+
// 代理模式:只写代理 Key,不暴露真实 API Key
|
|
44
38
|
if (includeProxyKey) {
|
|
45
|
-
|
|
39
|
+
return { [CODEX_PROXY_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
// 直连模式:只写当前激活渠道的 Key,环境中有且只有一条记录
|
|
43
|
+
const activeChannel = channels.find(ch => ch.enabled && ch.envKey && ch.apiKey);
|
|
44
|
+
if (!activeChannel) return {};
|
|
45
|
+
return { [activeChannel.envKey]: activeChannel.apiKey };
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
// 获取渠道存储文件路径
|
|
@@ -181,81 +181,51 @@ function syncPosixEnvironment(nextValues, previousState, options) {
|
|
|
181
181
|
const {
|
|
182
182
|
runtime,
|
|
183
183
|
homeDir,
|
|
184
|
-
envFilePath,
|
|
185
184
|
stateFilePath,
|
|
186
185
|
shellEnv,
|
|
187
186
|
execSync
|
|
188
187
|
} = options;
|
|
189
|
-
const { preferred, candidates } = getPosixProfileCandidates(homeDir, shellEnv);
|
|
190
188
|
const nextKeys = Object.keys(nextValues).sort();
|
|
191
|
-
const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
|
|
192
|
-
const managedProfiles = new Set(previousProfiles);
|
|
193
|
-
const sourceSnippet = buildSourceSnippet(envFilePath, homeDir);
|
|
194
189
|
let changed = false;
|
|
195
190
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
''
|
|
201
|
-
].join('\n');
|
|
202
|
-
changed = writeTextFileIfChanged(envFilePath, envContent) || changed;
|
|
203
|
-
|
|
204
|
-
const existingProfiles = candidates.filter(filePath => fs.existsSync(filePath));
|
|
205
|
-
if (managedProfiles.size === 0) {
|
|
206
|
-
if (existingProfiles.length > 0) {
|
|
207
|
-
for (const profilePath of existingProfiles) {
|
|
208
|
-
managedProfiles.add(profilePath);
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
managedProfiles.add(preferred);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (const profilePath of managedProfiles) {
|
|
216
|
-
const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
|
|
217
|
-
const nextContent = upsertManagedBlock(currentContent, sourceSnippet);
|
|
218
|
-
changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
if (fs.existsSync(envFilePath)) {
|
|
222
|
-
fs.unlinkSync(envFilePath);
|
|
223
|
-
changed = true;
|
|
224
|
-
}
|
|
225
|
-
|
|
191
|
+
// 清理旧版本遗留的 shell profile 注入(迁移兼容)
|
|
192
|
+
const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
|
|
193
|
+
if (previousProfiles.length > 0) {
|
|
194
|
+
const { candidates } = getPosixProfileCandidates(homeDir, shellEnv);
|
|
226
195
|
const cleanupTargets = new Set([
|
|
227
196
|
...previousProfiles,
|
|
228
197
|
...candidates.filter(filePath => fs.existsSync(filePath))
|
|
229
198
|
]);
|
|
230
|
-
|
|
231
199
|
for (const profilePath of cleanupTargets) {
|
|
232
200
|
if (!fs.existsSync(profilePath)) continue;
|
|
233
201
|
const currentContent = fs.readFileSync(profilePath, 'utf8');
|
|
202
|
+
if (!currentContent.includes(PROFILE_MARKER_START)) continue;
|
|
234
203
|
const nextContent = stripManagedBlock(currentContent);
|
|
235
204
|
const finalContent = nextContent ? `${nextContent}\n` : '';
|
|
236
205
|
changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
|
|
237
206
|
}
|
|
238
|
-
managedProfiles.clear();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const key of Object.keys(previousState.values || {})) {
|
|
242
|
-
if (!Object.prototype.hasOwnProperty.call(nextValues, key)) {
|
|
243
|
-
delete process.env[key];
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
for (const [key, value] of Object.entries(nextValues)) {
|
|
247
|
-
process.env[key] = value;
|
|
248
207
|
}
|
|
249
208
|
|
|
209
|
+
// macOS:用 launchctl 写入全局环境变量,新开终端/进程即生效
|
|
250
210
|
if (runtime === 'darwin') {
|
|
251
211
|
applyLaunchctlEnvironment(previousState.values || {}, nextValues, execSync);
|
|
212
|
+
const prevValues = previousState.values || {};
|
|
213
|
+
const keysChanged =
|
|
214
|
+
Object.keys(nextValues).length !== Object.keys(prevValues).length ||
|
|
215
|
+
Object.entries(nextValues).some(([k, v]) => prevValues[k] !== v);
|
|
216
|
+
changed = changed || keysChanged;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Linux:写 ~/.config/environment.d/(桌面/systemd)+ ~/.profile(SSH/登录shell)
|
|
220
|
+
if (runtime === 'linux') {
|
|
221
|
+
changed = applyLinuxEnvironment(nextValues, homeDir) || changed;
|
|
252
222
|
}
|
|
253
223
|
|
|
254
224
|
if (nextKeys.length > 0) {
|
|
255
225
|
writeJsonFile(stateFilePath, {
|
|
256
226
|
version: 1,
|
|
257
227
|
values: nextValues,
|
|
258
|
-
profiles:
|
|
228
|
+
profiles: []
|
|
259
229
|
});
|
|
260
230
|
} else if (fs.existsSync(stateFilePath)) {
|
|
261
231
|
fs.unlinkSync(stateFilePath);
|
|
@@ -265,14 +235,54 @@ function syncPosixEnvironment(nextValues, previousState, options) {
|
|
|
265
235
|
changed,
|
|
266
236
|
reloadRequired: changed,
|
|
267
237
|
isFirstTime: Object.keys(previousState.values || {}).length === 0 && nextKeys.length > 0,
|
|
268
|
-
sourceCommand:
|
|
269
|
-
shellConfigPath:
|
|
270
|
-
shellConfigPaths:
|
|
271
|
-
envFilePath:
|
|
238
|
+
sourceCommand: null,
|
|
239
|
+
shellConfigPath: null,
|
|
240
|
+
shellConfigPaths: [],
|
|
241
|
+
envFilePath: null,
|
|
272
242
|
managedKeys: nextKeys
|
|
273
243
|
};
|
|
274
244
|
}
|
|
275
245
|
|
|
246
|
+
function applyLinuxEnvironment(nextValues, homeDir) {
|
|
247
|
+
let changed = false;
|
|
248
|
+
|
|
249
|
+
// 1. ~/.config/environment.d/codex-env.conf(systemd 用户环境,桌面终端生效)
|
|
250
|
+
const envdDir = path.join(homeDir, '.config', 'environment.d');
|
|
251
|
+
const envdFile = path.join(envdDir, 'codex-env.conf');
|
|
252
|
+
const nextKeys = Object.keys(nextValues).sort();
|
|
253
|
+
|
|
254
|
+
if (nextKeys.length > 0) {
|
|
255
|
+
const envdContent = [
|
|
256
|
+
'# Managed by Coding-Tool',
|
|
257
|
+
...nextKeys.map(key => `${key}=${nextValues[key]}`),
|
|
258
|
+
''
|
|
259
|
+
].join('\n');
|
|
260
|
+
changed = writeTextFileIfChanged(envdFile, envdContent) || changed;
|
|
261
|
+
} else if (fs.existsSync(envdFile)) {
|
|
262
|
+
fs.unlinkSync(envdFile);
|
|
263
|
+
changed = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2. ~/.profile(登录 shell,SSH 和新终端均生效)
|
|
267
|
+
const profilePath = path.join(homeDir, '.profile');
|
|
268
|
+
if (nextKeys.length > 0) {
|
|
269
|
+
const exportLines = nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`).join('\n');
|
|
270
|
+
const snippet = [PROFILE_MARKER_START, exportLines, PROFILE_MARKER_END].join('\n');
|
|
271
|
+
const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
|
|
272
|
+
const nextContent = upsertManagedBlock(currentContent, snippet);
|
|
273
|
+
changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
|
|
274
|
+
} else if (fs.existsSync(profilePath)) {
|
|
275
|
+
const currentContent = fs.readFileSync(profilePath, 'utf8');
|
|
276
|
+
if (currentContent.includes(PROFILE_MARKER_START)) {
|
|
277
|
+
const nextContent = stripManagedBlock(currentContent);
|
|
278
|
+
const finalContent = nextContent ? `${nextContent}\n` : '';
|
|
279
|
+
changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return changed;
|
|
284
|
+
}
|
|
285
|
+
|
|
276
286
|
function applyLaunchctlEnvironment(previousValues, nextValues, execSync) {
|
|
277
287
|
const previousKeys = new Set(Object.keys(previousValues || {}));
|
|
278
288
|
for (const [key, value] of Object.entries(nextValues || {})) {
|
|
@@ -306,21 +316,14 @@ function syncWindowsEnvironment(nextValues, previousState, options) {
|
|
|
306
316
|
let changed = false;
|
|
307
317
|
|
|
308
318
|
for (const [key, value] of Object.entries(nextValues)) {
|
|
309
|
-
if (previousValues[key] === value)
|
|
310
|
-
process.env[key] = value;
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
319
|
+
if (previousValues[key] === value) continue;
|
|
313
320
|
setWindowsUserEnv(key, value, execSync);
|
|
314
|
-
process.env[key] = value;
|
|
315
321
|
changed = true;
|
|
316
322
|
}
|
|
317
323
|
|
|
318
324
|
for (const key of Object.keys(previousValues)) {
|
|
319
|
-
if (Object.prototype.hasOwnProperty.call(nextValues, key))
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
325
|
+
if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
|
|
322
326
|
removeWindowsUserEnv(key, execSync);
|
|
323
|
-
delete process.env[key];
|
|
324
327
|
changed = true;
|
|
325
328
|
}
|
|
326
329
|
|
|
@@ -920,9 +920,13 @@ async function getTrendStatistics({ startDate, endDate, granularity = 'day', ste
|
|
|
920
920
|
|
|
921
921
|
if (granularity === 'day') {
|
|
922
922
|
labels.push(dateStr);
|
|
923
|
-
|
|
923
|
+
let byDimension = activeFilters
|
|
924
924
|
? readJsonlForDay(year, month, day, groupBy, activeFilters)
|
|
925
925
|
: mergeAllToolsDailyStats(dateStr, groupBy);
|
|
926
|
+
if (!activeFilters && Object.keys(byDimension).length === 0) {
|
|
927
|
+
// Fallback: if daily stats are missing, derive from JSONL logs
|
|
928
|
+
byDimension = readJsonlForDay(year, month, day, groupBy);
|
|
929
|
+
}
|
|
926
930
|
|
|
927
931
|
// Accumulate dimensions seen so far with 0 for this label position
|
|
928
932
|
const labelIdx = labels.length - 1;
|
|
@@ -8,6 +8,24 @@ const configTemplatesService = require('./config-templates-service');
|
|
|
8
8
|
// 工作区配置文件路径
|
|
9
9
|
const WORKSPACES_CONFIG = PATHS.workspaces;
|
|
10
10
|
|
|
11
|
+
function createSymlink(target, linkPath) {
|
|
12
|
+
try {
|
|
13
|
+
fs.symlinkSync(target, linkPath, 'dir');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err.code === 'EPERM' && process.platform === 'win32') {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`创建软链接失败:Windows 需要管理员权限或开启开发者模式。\n` +
|
|
18
|
+
`解决方案:\n` +
|
|
19
|
+
`1. 以管理员身份运行终端后重试\n` +
|
|
20
|
+
`2. 或开启开发者模式:设置 → 系统 → 开发者选项 → 开发者模式\n` +
|
|
21
|
+
`3. 对于 Git 项目,建议保持默认的 worktree 模式(无需软链接)\n` +
|
|
22
|
+
`原始错误: ${err.message}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
function runGitCommand(args, options = {}) {
|
|
12
30
|
const execOptions = {
|
|
13
31
|
encoding: 'utf8',
|
|
@@ -287,7 +305,7 @@ function createWorkspace(options) {
|
|
|
287
305
|
let targetPath = sourcePath;
|
|
288
306
|
let worktrees = [];
|
|
289
307
|
|
|
290
|
-
// Git 仓库且需要创建 worktree
|
|
308
|
+
// Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
|
|
291
309
|
if (isGit && useWorktree) {
|
|
292
310
|
// 获取当前分支作为默认分支
|
|
293
311
|
let targetBranch = branch;
|
|
@@ -299,72 +317,46 @@ function createWorkspace(options) {
|
|
|
299
317
|
}
|
|
300
318
|
}
|
|
301
319
|
|
|
302
|
-
// worktree
|
|
303
|
-
const worktreePath =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (fs.existsSync(worktreePath)) {
|
|
310
|
-
// 已存在则直接使用
|
|
311
|
-
targetPath = worktreePath;
|
|
312
|
-
worktrees.push({
|
|
313
|
-
branch: targetBranch,
|
|
314
|
-
path: worktreePath
|
|
320
|
+
// worktree 直接创建到工作区目录内的项目路径
|
|
321
|
+
const worktreePath = symlinkPath;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// 尝试检出已有分支
|
|
325
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
326
|
+
cwd: sourcePath
|
|
315
327
|
});
|
|
316
|
-
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// 如果分支不存在,尝试创建新分支
|
|
317
330
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
332
|
+
if (baseBranch && baseBranch.trim()) {
|
|
333
|
+
worktreeArgs.push(baseBranch.trim());
|
|
334
|
+
}
|
|
335
|
+
runGitCommand(worktreeArgs, {
|
|
320
336
|
cwd: sourcePath
|
|
321
337
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// 如果分支不存在,尝试创建新分支
|
|
330
|
-
try {
|
|
331
|
-
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
332
|
-
if (baseBranch && baseBranch.trim()) {
|
|
333
|
-
worktreeArgs.push(baseBranch.trim());
|
|
334
|
-
}
|
|
335
|
-
runGitCommand(worktreeArgs, {
|
|
336
|
-
cwd: sourcePath
|
|
337
|
-
});
|
|
338
|
-
targetPath = worktreePath;
|
|
339
|
-
worktrees.push({
|
|
340
|
-
branch: targetBranch,
|
|
341
|
-
path: worktreePath
|
|
342
|
-
});
|
|
343
|
-
} catch (err) {
|
|
344
|
-
// Check if it's a "branch already checked out" error
|
|
345
|
-
if (err.message.includes('already checked out')) {
|
|
346
|
-
throw new Error(
|
|
347
|
-
`无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
348
|
-
`错误详情: ${err.message}\n\n` +
|
|
349
|
-
`提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// For other errors, provide clear message but allow fallback
|
|
354
|
-
console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
|
|
355
|
-
targetPath = sourcePath;
|
|
356
|
-
worktrees = getGitWorktrees(sourcePath);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err.message.includes('already checked out')) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
342
|
+
`错误详情: ${err.message}\n\n` +
|
|
343
|
+
`提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
|
|
344
|
+
);
|
|
357
345
|
}
|
|
346
|
+
throw new Error(`创建 worktree 失败: ${err.message}`);
|
|
358
347
|
}
|
|
359
348
|
}
|
|
349
|
+
|
|
350
|
+
targetPath = worktreePath;
|
|
351
|
+
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
360
352
|
} else if (isGit) {
|
|
361
|
-
// Git 仓库但不创建 worktree
|
|
353
|
+
// Git 仓库但不创建 worktree:symlink 指向源路径,记录已有 worktrees
|
|
362
354
|
worktrees = getGitWorktrees(sourcePath);
|
|
355
|
+
createSymlink(targetPath, symlinkPath);
|
|
356
|
+
} else {
|
|
357
|
+
// 非 Git 仓库:symlink 指向源路径
|
|
358
|
+
createSymlink(targetPath, symlinkPath);
|
|
363
359
|
}
|
|
364
|
-
// 非 Git 仓库:targetPath 保持为 sourcePath,直接软链接
|
|
365
|
-
|
|
366
|
-
// 创建软链接
|
|
367
|
-
fs.symlinkSync(targetPath, symlinkPath, 'dir');
|
|
368
360
|
|
|
369
361
|
workspaceProjects.push({
|
|
370
362
|
name: symlinkName,
|
|
@@ -438,36 +430,17 @@ function deleteWorkspace(id, removeFiles = false) {
|
|
|
438
430
|
|
|
439
431
|
const workspace = data.workspaces[index];
|
|
440
432
|
|
|
441
|
-
//
|
|
433
|
+
// 注销 worktrees(worktree 目录在工作区内,git 引用需先注销)
|
|
442
434
|
for (const proj of workspace.projects) {
|
|
443
|
-
if (proj.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
console.log(`清理 worktree: ${wt.path}`);
|
|
452
|
-
runGitCommand(['worktree', 'remove', wt.path, '--force'], {
|
|
453
|
-
cwd: proj.sourcePath
|
|
454
|
-
});
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
457
|
-
// 如果 git worktree remove 失败,尝试手动删除目录
|
|
458
|
-
if (fs.existsSync(wt.path)) {
|
|
459
|
-
try {
|
|
460
|
-
fs.rmSync(wt.path, { recursive: true, force: true });
|
|
461
|
-
console.log(`手动删除 worktree 目录: ${wt.path}`);
|
|
462
|
-
} catch (rmError) {
|
|
463
|
-
console.error(`手动删除 worktree 目录失败: ${wt.path}`, rmError.message);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
435
|
+
if (proj.useWorktree && proj.sourcePath && proj.targetPath) {
|
|
436
|
+
if (fs.existsSync(proj.sourcePath)) {
|
|
437
|
+
try {
|
|
438
|
+
runGitCommand(['worktree', 'remove', proj.targetPath, '--force'], {
|
|
439
|
+
cwd: proj.sourcePath
|
|
440
|
+
});
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(`注销 worktree 失败: ${proj.targetPath}`, error.message);
|
|
468
443
|
}
|
|
469
|
-
} catch (error) {
|
|
470
|
-
console.error(`扫描 worktree 失败: ${proj.sourcePath}`, error.message);
|
|
471
444
|
}
|
|
472
445
|
}
|
|
473
446
|
}
|
|
@@ -533,9 +506,8 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
533
506
|
let targetPath = sourcePath;
|
|
534
507
|
let worktrees = [];
|
|
535
508
|
|
|
536
|
-
// Git 仓库且需要创建 worktree
|
|
509
|
+
// Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
|
|
537
510
|
if (isGit && useWorktree) {
|
|
538
|
-
// 获取当前分支作为默认分支
|
|
539
511
|
let targetBranch = branch;
|
|
540
512
|
if (!targetBranch) {
|
|
541
513
|
try {
|
|
@@ -545,73 +517,44 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
545
517
|
}
|
|
546
518
|
}
|
|
547
519
|
|
|
548
|
-
const worktreePath =
|
|
549
|
-
path.dirname(sourcePath),
|
|
550
|
-
`${path.basename(sourcePath)}-ws-${targetBranch.replace(/\//g, '-')}`
|
|
551
|
-
);
|
|
520
|
+
const worktreePath = symlinkPath;
|
|
552
521
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
522
|
+
try {
|
|
523
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
524
|
+
cwd: sourcePath
|
|
525
|
+
});
|
|
526
|
+
} catch (error) {
|
|
558
527
|
try {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
} catch (
|
|
565
|
-
|
|
566
|
-
if (error.message && error.message.includes('already checked out')) {
|
|
528
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
529
|
+
if (baseBranch && baseBranch.trim()) {
|
|
530
|
+
worktreeArgs.push(baseBranch.trim());
|
|
531
|
+
}
|
|
532
|
+
runGitCommand(worktreeArgs, { cwd: sourcePath });
|
|
533
|
+
} catch (err) {
|
|
534
|
+
if (err.message && err.message.includes('already checked out')) {
|
|
567
535
|
throw new Error(
|
|
568
536
|
`无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
569
537
|
`仓库路径: ${sourcePath}\n\n` +
|
|
570
|
-
`Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
|
|
571
538
|
`解决方案:\n` +
|
|
572
539
|
`1. 指定不同的分支名\n` +
|
|
573
540
|
`2. 或者禁用 worktree 模式(设置 createWorktree: false)`
|
|
574
541
|
);
|
|
575
542
|
}
|
|
576
|
-
|
|
577
|
-
// Branch doesn't exist, try creating it
|
|
578
|
-
try {
|
|
579
|
-
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
580
|
-
if (baseBranch && baseBranch.trim()) {
|
|
581
|
-
worktreeArgs.push(baseBranch.trim());
|
|
582
|
-
}
|
|
583
|
-
runGitCommand(worktreeArgs, {
|
|
584
|
-
cwd: sourcePath
|
|
585
|
-
});
|
|
586
|
-
targetPath = worktreePath;
|
|
587
|
-
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
588
|
-
} catch (err) {
|
|
589
|
-
// Check for "already checked out" error in create branch attempt
|
|
590
|
-
if (err.message && err.message.includes('already checked out')) {
|
|
591
|
-
throw new Error(
|
|
592
|
-
`无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
593
|
-
`仓库路径: ${sourcePath}\n\n` +
|
|
594
|
-
`Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
|
|
595
|
-
`解决方案:\n` +
|
|
596
|
-
`1. 指定不同的分支名\n` +
|
|
597
|
-
`2. 或者禁用 worktree 模式(设置 createWorktree: false)`
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Other errors: fall back to symlink mode
|
|
602
|
-
console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
|
|
603
|
-
targetPath = sourcePath;
|
|
604
|
-
worktrees = getGitWorktrees(sourcePath);
|
|
605
|
-
}
|
|
543
|
+
throw new Error(`创建 worktree 失败: ${err.message}`);
|
|
606
544
|
}
|
|
607
545
|
}
|
|
546
|
+
|
|
547
|
+
targetPath = worktreePath;
|
|
548
|
+
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
608
549
|
} else if (isGit) {
|
|
550
|
+
// Git 仓库但不创建 worktree:symlink 指向源路径
|
|
609
551
|
worktrees = getGitWorktrees(sourcePath);
|
|
552
|
+
createSymlink(targetPath, symlinkPath);
|
|
553
|
+
} else {
|
|
554
|
+
// 非 Git 仓库:symlink 指向源路径
|
|
555
|
+
createSymlink(targetPath, symlinkPath);
|
|
610
556
|
}
|
|
611
557
|
|
|
612
|
-
// 创建软链接
|
|
613
|
-
fs.symlinkSync(targetPath, symlinkPath, 'dir');
|
|
614
|
-
|
|
615
558
|
// 更新配置
|
|
616
559
|
workspace.projects.push({
|
|
617
560
|
name: symlinkName,
|
|
@@ -629,7 +572,7 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
629
572
|
/**
|
|
630
573
|
* 从工作区移除项目
|
|
631
574
|
*/
|
|
632
|
-
function removeProjectFromWorkspace(workspaceId, projectName
|
|
575
|
+
function removeProjectFromWorkspace(workspaceId, projectName) {
|
|
633
576
|
const data = loadWorkspaces();
|
|
634
577
|
const workspace = data.workspaces.find(ws => ws.id === workspaceId);
|
|
635
578
|
|
|
@@ -644,26 +587,28 @@ function removeProjectFromWorkspace(workspaceId, projectName, removeWorktrees =
|
|
|
644
587
|
}
|
|
645
588
|
|
|
646
589
|
const project = workspace.projects[projectIndex];
|
|
647
|
-
const
|
|
590
|
+
const projectPath = path.join(workspace.path, projectName);
|
|
648
591
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
fs.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
});
|
|
662
|
-
} catch (error) {
|
|
663
|
-
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
592
|
+
if (project.useWorktree) {
|
|
593
|
+
// worktree 项目:用 git worktree remove 注销引用(目录在工作区内)
|
|
594
|
+
if (fs.existsSync(projectPath) && fs.existsSync(project.sourcePath)) {
|
|
595
|
+
try {
|
|
596
|
+
runGitCommand(['worktree', 'remove', projectPath, '--force'], {
|
|
597
|
+
cwd: project.sourcePath
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error(`注销 worktree 失败: ${projectPath}`, error.message);
|
|
601
|
+
// git 注销失败时手动删除目录
|
|
602
|
+
if (fs.existsSync(projectPath)) {
|
|
603
|
+
fs.rmSync(projectPath, { recursive: true, force: true });
|
|
664
604
|
}
|
|
665
605
|
}
|
|
666
606
|
}
|
|
607
|
+
} else {
|
|
608
|
+
// symlink 项目:直接删除软链接
|
|
609
|
+
if (fs.existsSync(projectPath)) {
|
|
610
|
+
fs.unlinkSync(projectPath);
|
|
611
|
+
}
|
|
667
612
|
}
|
|
668
613
|
|
|
669
614
|
// 从配置中移除
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.analytics-page[data-v-7683ab57]{height:100%;background:var(--bg-primary);overflow-y:auto;padding:16px 20px;box-sizing:border-box;display:flex;flex-direction:column;gap:14px}.analytics-header[data-v-7683ab57]{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;flex-wrap:wrap;gap:10px}.page-title[data-v-7683ab57]{margin:0;font-size:18px;font-weight:700;color:var(--text-primary)}.toolbar[data-v-7683ab57]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.range-buttons[data-v-7683ab57]{display:flex;align-items:center;gap:4px}.range-btn[data-v-7683ab57]{padding:4px 10px;font-size:12px;border:1px solid var(--border-color, #e0e0e6);background:transparent;color:var(--text-secondary);border-radius:4px;cursor:pointer;transition:all .15s}.range-btn[data-v-7683ab57]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.range-btn.active[data-v-7683ab57]{background:var(--primary-color, #18a058);border-color:var(--primary-color, #18a058);color:#fff}.custom-range-wrapper[data-v-7683ab57]{margin-left:4px}.loading-overlay[data-v-7683ab57]{display:flex;justify-content:center;padding:20px 0}.summary-cards[data-v-7683ab57]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;flex-shrink:0}.summary-card[data-v-7683ab57]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px}.summary-label[data-v-7683ab57]{font-size:11px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em}.summary-value[data-v-7683ab57]{font-size:22px;font-weight:700;color:var(--text-primary);line-height:1.2}.chart-section[data-v-7683ab57]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px;flex-shrink:0}.chart-section.fullscreen[data-v-7683ab57]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;border-radius:0;padding:20px;background:var(--bg-secondary);overflow:auto}.cumulative-section[data-v-7683ab57]{margin-bottom:8px}.chart-section-header[data-v-7683ab57]{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.chart-title[data-v-7683ab57]{font-size:13px;font-weight:600;color:var(--text-primary)}.chart-controls[data-v-7683ab57]{display:flex;align-items:center;gap:8px}.chart-type-toggle[data-v-7683ab57]{display:flex;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;overflow:hidden}.toggle-btn[data-v-7683ab57]{padding:3px 10px;font-size:12px;border:none;background:transparent;color:var(--text-secondary);cursor:pointer;transition:all .15s}.toggle-btn.active[data-v-7683ab57]{background:var(--primary-color, #18a058);color:#fff}.icon-btn[data-v-7683ab57]{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;background:transparent;cursor:pointer;color:var(--text-secondary);transition:all .15s}.icon-btn[data-v-7683ab57]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.main-chart[data-v-7683ab57]{height:560px;width:100%}.chart-section.fullscreen .main-chart[data-v-7683ab57]{height:calc(100vh - 100px)}.cumulative-chart[data-v-7683ab57]{height:200px;width:100%}.empty-state[data-v-7683ab57]{height:120px;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:13px}@media (max-width: 768px){.analytics-page[data-v-7683ab57]{padding:10px 12px}.summary-cards[data-v-7683ab57]{grid-template-columns:repeat(2,1fr)}.analytics-header[data-v-7683ab57]{flex-direction:column;align-items:flex-start}.toolbar[data-v-7683ab57]{width:100%}}@media (max-width: 480px){.summary-cards[data-v-7683ab57]{grid-template-columns:1fr 1fr}.summary-value[data-v-7683ab57]{font-size:18px}}
|