coding-tool-x 3.6.12 → 3.6.13
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-BuBlsbcI.js → Analytics-CnA75GxO.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DQczxuOX.js → ConfigTemplates-D8XEscQY.js} +1 -1
- package/dist/web/assets/{Home-BvAzzWnB.js → Home-BiaKZMah.js} +1 -1
- package/dist/web/assets/{PluginManager-C8gAlaps.js → PluginManager-BzKZ8TYu.js} +1 -1
- package/dist/web/assets/{ProjectList-nqgi6_cR.js → ProjectList-BPfG_92X.js} +1 -1
- package/dist/web/assets/{SessionList-D8AxMYy6.js → SessionList-BIVtw00v.js} +1 -1
- package/dist/web/assets/{SkillManager-BDbNbNkK.js → SkillManager-fxOvFTYD.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DGGaGW2c.js → WorkspaceManager-Ch2mM8jm.js} +1 -1
- package/dist/web/assets/{index-y-eEayxR.js → index-BpBFF3b2.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/commands/port-config.js +0 -2
- package/src/commands/switch.js +1 -5
- package/src/commands/workspace.js +35 -3
- package/src/config/default.js +0 -6
- package/src/config/loader.js +76 -5
- package/src/server/api/config.js +0 -3
- package/src/server/api/settings.js +0 -2
- package/src/server/api/workspaces.js +39 -1
- package/src/server/services/sessions.js +35 -19
- package/src/server/services/workspace-service.js +220 -98
- package/src/utils/session.js +10 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 多项目工作区服务
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const { execFileSync } = require('child_process');
|
|
5
6
|
const { PATHS } = require('../../config/paths');
|
|
@@ -46,6 +47,206 @@ function resolveCurrentBranch(repoPath) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
function getGitErrorMessage(error) {
|
|
51
|
+
const stderr = error && error.stderr ? error.stderr.toString().trim() : '';
|
|
52
|
+
return stderr || (error && error.message) || String(error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeString(value) {
|
|
56
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeBranchMode(projectConfig = {}) {
|
|
60
|
+
const branchMode = normalizeString(projectConfig.branchMode);
|
|
61
|
+
if (branchMode === 'new' || branchMode === 'existing') {
|
|
62
|
+
return branchMode;
|
|
63
|
+
}
|
|
64
|
+
return normalizeString(projectConfig.baseBranch) ? 'new' : 'existing';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeGitBranchRef(branchName) {
|
|
68
|
+
return normalizeString(branchName).replace(/^refs\/heads\//, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseGitWorktrees(output) {
|
|
72
|
+
const worktrees = [];
|
|
73
|
+
const lines = output.split('\n');
|
|
74
|
+
let current = {};
|
|
75
|
+
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
if (line.startsWith('worktree ')) {
|
|
78
|
+
if (current.path) {
|
|
79
|
+
worktrees.push(current);
|
|
80
|
+
}
|
|
81
|
+
current = { path: line.substring(9) };
|
|
82
|
+
} else if (line.startsWith('branch ')) {
|
|
83
|
+
current.branch = line.substring(7);
|
|
84
|
+
} else if (line.startsWith('HEAD ')) {
|
|
85
|
+
current.head = line.substring(5);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (current.path) {
|
|
90
|
+
worktrees.push(current);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return worktrees;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listGitWorktrees(repoPath) {
|
|
97
|
+
const output = runGitCommand(['worktree', 'list', '--porcelain'], {
|
|
98
|
+
cwd: repoPath
|
|
99
|
+
});
|
|
100
|
+
return parseGitWorktrees(output);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hasGitRef(repoPath, ref) {
|
|
104
|
+
try {
|
|
105
|
+
runGitCommand(['show-ref', '--verify', '--quiet', ref], {
|
|
106
|
+
cwd: repoPath,
|
|
107
|
+
stdio: 'ignore'
|
|
108
|
+
});
|
|
109
|
+
return true;
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findWorktreeForBranch(repoPath, branchName) {
|
|
116
|
+
const normalizedBranch = normalizeGitBranchRef(branchName);
|
|
117
|
+
const refName = `refs/heads/${normalizedBranch}`;
|
|
118
|
+
return listGitWorktrees(repoPath).find(wt => wt.branch === refName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pullBaseBranch(worktreePath, baseBranch) {
|
|
122
|
+
try {
|
|
123
|
+
runGitCommand(['pull', '--ff-only'], { cwd: worktreePath });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
try {
|
|
126
|
+
runGitCommand(['remote', 'get-url', 'origin'], { cwd: worktreePath });
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const normalizedBaseBranch = normalizeGitBranchRef(baseBranch);
|
|
131
|
+
runGitCommand(['pull', '--ff-only', 'origin', normalizedBaseBranch], {
|
|
132
|
+
cwd: worktreePath
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function removeTemporaryWorktree(repoPath, tempWorktreePath) {
|
|
138
|
+
try {
|
|
139
|
+
runGitCommand(['worktree', 'remove', tempWorktreePath, '--force'], {
|
|
140
|
+
cwd: repoPath
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.warn(`移除临时 worktree 失败: ${tempWorktreePath}`, error.message);
|
|
144
|
+
} finally {
|
|
145
|
+
if (fs.existsSync(tempWorktreePath)) {
|
|
146
|
+
fs.rmSync(tempWorktreePath, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function updateBaseBranch(repoPath, baseBranch) {
|
|
152
|
+
const normalizedBaseBranch = normalizeGitBranchRef(baseBranch);
|
|
153
|
+
if (!normalizedBaseBranch) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
runGitCommand(['fetch', '--all', '--prune'], { cwd: repoPath });
|
|
158
|
+
|
|
159
|
+
const localRef = `refs/heads/${normalizedBaseBranch}`;
|
|
160
|
+
const remoteRef = `refs/remotes/origin/${normalizedBaseBranch}`;
|
|
161
|
+
const hasLocalBranch = hasGitRef(repoPath, localRef);
|
|
162
|
+
const hasRemoteBranch = hasGitRef(repoPath, remoteRef);
|
|
163
|
+
if (!hasLocalBranch && !hasRemoteBranch) {
|
|
164
|
+
throw new Error(`基础分支不存在: ${normalizedBaseBranch}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const existingWorktree = hasLocalBranch ? findWorktreeForBranch(repoPath, normalizedBaseBranch) : null;
|
|
168
|
+
if (existingWorktree) {
|
|
169
|
+
pullBaseBranch(existingWorktree.path, normalizedBaseBranch);
|
|
170
|
+
return normalizedBaseBranch;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const tempParent = fs.mkdtempSync(path.join(os.tmpdir(), 'ctx-base-'));
|
|
174
|
+
const tempWorktreePath = path.join(tempParent, 'worktree');
|
|
175
|
+
try {
|
|
176
|
+
if (hasLocalBranch) {
|
|
177
|
+
runGitCommand(['worktree', 'add', tempWorktreePath, normalizedBaseBranch], {
|
|
178
|
+
cwd: repoPath
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
runGitCommand(['worktree', 'add', tempWorktreePath, '-b', normalizedBaseBranch, `origin/${normalizedBaseBranch}`], {
|
|
182
|
+
cwd: repoPath
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
pullBaseBranch(tempWorktreePath, normalizedBaseBranch);
|
|
186
|
+
return normalizedBaseBranch;
|
|
187
|
+
} finally {
|
|
188
|
+
removeTemporaryWorktree(repoPath, tempWorktreePath);
|
|
189
|
+
if (fs.existsSync(tempParent)) {
|
|
190
|
+
fs.rmSync(tempParent, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createGitWorktree({ sourcePath, worktreePath, branch, baseBranch, branchMode, actionLabel }) {
|
|
196
|
+
let targetBranch = normalizeString(branch);
|
|
197
|
+
if (!targetBranch) {
|
|
198
|
+
try {
|
|
199
|
+
targetBranch = resolveCurrentBranch(sourcePath);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
targetBranch = 'main';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const resolvedBranchMode = branchMode === 'new' ? 'new' : 'existing';
|
|
206
|
+
|
|
207
|
+
if (resolvedBranchMode === 'new') {
|
|
208
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
209
|
+
const updatedBaseBranch = updateBaseBranch(sourcePath, baseBranch);
|
|
210
|
+
if (updatedBaseBranch) {
|
|
211
|
+
worktreeArgs.push(updatedBaseBranch);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
runGitCommand(worktreeArgs, { cwd: sourcePath });
|
|
216
|
+
return { branch: targetBranch, path: worktreePath };
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`创建 worktree 失败: ${getGitErrorMessage(error)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
224
|
+
cwd: sourcePath
|
|
225
|
+
});
|
|
226
|
+
return { branch: targetBranch, path: worktreePath };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const message = getGitErrorMessage(error);
|
|
229
|
+
if (/already (checked out|used by worktree)/i.test(message)) {
|
|
230
|
+
try {
|
|
231
|
+
runGitCommand(['worktree', 'add', '--force', worktreePath, targetBranch], {
|
|
232
|
+
cwd: sourcePath
|
|
233
|
+
});
|
|
234
|
+
return { branch: targetBranch, path: worktreePath };
|
|
235
|
+
} catch (forceError) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`无法${actionLabel}:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
238
|
+
`仓库路径: ${sourcePath}\n` +
|
|
239
|
+
`错误详情: ${getGitErrorMessage(forceError)}\n\n` +
|
|
240
|
+
`解决方案:\n` +
|
|
241
|
+
`1. 指定不同的分支名\n` +
|
|
242
|
+
`2. 或者禁用 worktree 模式(设置 createWorktree: false)`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`创建 worktree 失败: ${message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
49
250
|
/**
|
|
50
251
|
* 生成唯一工作区 ID
|
|
51
252
|
*/
|
|
@@ -147,31 +348,7 @@ function getGitWorktrees(repoPath) {
|
|
|
147
348
|
if (!isGitRepo(repoPath)) {
|
|
148
349
|
return [];
|
|
149
350
|
}
|
|
150
|
-
const
|
|
151
|
-
cwd: repoPath
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const worktrees = [];
|
|
155
|
-
const lines = output.split('\n');
|
|
156
|
-
let current = {};
|
|
157
|
-
|
|
158
|
-
for (const line of lines) {
|
|
159
|
-
if (line.startsWith('worktree ')) {
|
|
160
|
-
if (current.path) {
|
|
161
|
-
worktrees.push(current);
|
|
162
|
-
}
|
|
163
|
-
current = { path: line.substring(9) };
|
|
164
|
-
} else if (line.startsWith('branch ')) {
|
|
165
|
-
current.branch = line.substring(7);
|
|
166
|
-
} else if (line.startsWith('HEAD ')) {
|
|
167
|
-
current.head = line.substring(5);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (current.path) {
|
|
172
|
-
worktrees.push(current);
|
|
173
|
-
}
|
|
174
|
-
|
|
351
|
+
const worktrees = listGitWorktrees(repoPath);
|
|
175
352
|
return worktrees.filter(wt => wt.path !== repoPath); // 排除主仓库
|
|
176
353
|
} catch (error) {
|
|
177
354
|
console.error('获取 worktree 列表失败:', error.message);
|
|
@@ -308,48 +485,19 @@ function createWorkspace(options) {
|
|
|
308
485
|
|
|
309
486
|
// Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
|
|
310
487
|
if (isGit && useWorktree) {
|
|
311
|
-
// 获取当前分支作为默认分支
|
|
312
|
-
let targetBranch = branch;
|
|
313
|
-
if (!targetBranch) {
|
|
314
|
-
try {
|
|
315
|
-
targetBranch = resolveCurrentBranch(sourcePath);
|
|
316
|
-
} catch (e) {
|
|
317
|
-
targetBranch = 'main';
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
488
|
// worktree 直接创建到工作区目录内的项目路径
|
|
322
489
|
const worktreePath = symlinkPath;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
333
|
-
if (baseBranch && baseBranch.trim()) {
|
|
334
|
-
worktreeArgs.push(baseBranch.trim());
|
|
335
|
-
}
|
|
336
|
-
runGitCommand(worktreeArgs, {
|
|
337
|
-
cwd: sourcePath
|
|
338
|
-
});
|
|
339
|
-
} catch (err) {
|
|
340
|
-
if (err.message.includes('already checked out')) {
|
|
341
|
-
throw new Error(
|
|
342
|
-
`无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
343
|
-
`错误详情: ${err.message}\n\n` +
|
|
344
|
-
`提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
throw new Error(`创建 worktree 失败: ${err.message}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
490
|
+
const createdWorktree = createGitWorktree({
|
|
491
|
+
sourcePath,
|
|
492
|
+
worktreePath,
|
|
493
|
+
branch,
|
|
494
|
+
baseBranch,
|
|
495
|
+
branchMode: normalizeBranchMode(proj),
|
|
496
|
+
actionLabel: '创建 worktree'
|
|
497
|
+
});
|
|
350
498
|
|
|
351
499
|
targetPath = worktreePath;
|
|
352
|
-
worktrees.push(
|
|
500
|
+
worktrees.push(createdWorktree);
|
|
353
501
|
} else if (isGit) {
|
|
354
502
|
// Git 仓库但不创建 worktree:symlink 指向源路径,记录已有 worktrees
|
|
355
503
|
worktrees = getGitWorktrees(sourcePath);
|
|
@@ -509,44 +657,18 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
509
657
|
|
|
510
658
|
// Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
|
|
511
659
|
if (isGit && useWorktree) {
|
|
512
|
-
let targetBranch = branch;
|
|
513
|
-
if (!targetBranch) {
|
|
514
|
-
try {
|
|
515
|
-
targetBranch = resolveCurrentBranch(sourcePath);
|
|
516
|
-
} catch (e) {
|
|
517
|
-
targetBranch = 'main';
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
660
|
const worktreePath = symlinkPath;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (baseBranch && baseBranch.trim()) {
|
|
531
|
-
worktreeArgs.push(baseBranch.trim());
|
|
532
|
-
}
|
|
533
|
-
runGitCommand(worktreeArgs, { cwd: sourcePath });
|
|
534
|
-
} catch (err) {
|
|
535
|
-
if (err.message && err.message.includes('already checked out')) {
|
|
536
|
-
throw new Error(
|
|
537
|
-
`无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
538
|
-
`仓库路径: ${sourcePath}\n\n` +
|
|
539
|
-
`解决方案:\n` +
|
|
540
|
-
`1. 指定不同的分支名\n` +
|
|
541
|
-
`2. 或者禁用 worktree 模式(设置 createWorktree: false)`
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
throw new Error(`创建 worktree 失败: ${err.message}`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
661
|
+
const createdWorktree = createGitWorktree({
|
|
662
|
+
sourcePath,
|
|
663
|
+
worktreePath,
|
|
664
|
+
branch,
|
|
665
|
+
baseBranch,
|
|
666
|
+
branchMode: normalizeBranchMode(projectConfig),
|
|
667
|
+
actionLabel: '添加项目'
|
|
668
|
+
});
|
|
547
669
|
|
|
548
670
|
targetPath = worktreePath;
|
|
549
|
-
worktrees.push(
|
|
671
|
+
worktrees.push(createdWorktree);
|
|
550
672
|
} else if (isGit) {
|
|
551
673
|
// Git 仓库但不创建 worktree:symlink 指向源路径
|
|
552
674
|
worktrees = getGitWorktrees(sourcePath);
|
package/src/utils/session.js
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const { NATIVE_PATHS } = require('../config/paths');
|
|
6
|
+
|
|
7
|
+
function resolveProjectsDir(config = {}) {
|
|
8
|
+
return config.projectsDir || NATIVE_PATHS.claude.projects;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* 获取会话目录
|
|
8
13
|
*/
|
|
9
14
|
function getSessionsDir(config) {
|
|
10
|
-
return path.join(config
|
|
15
|
+
return path.join(resolveProjectsDir(config), config.currentProject);
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
/**
|
|
@@ -196,7 +201,7 @@ function parseLinesWithTail(headLines, tailLines) {
|
|
|
196
201
|
* 获取所有可用的项目
|
|
197
202
|
*/
|
|
198
203
|
async function getAvailableProjects(config) {
|
|
199
|
-
const projectsDir = config
|
|
204
|
+
const projectsDir = resolveProjectsDir(config);
|
|
200
205
|
if (!fs.existsSync(projectsDir)) {
|
|
201
206
|
console.log(chalk.red(`项目目录不存在: ${projectsDir}`));
|
|
202
207
|
return [];
|
|
@@ -204,8 +209,9 @@ async function getAvailableProjects(config) {
|
|
|
204
209
|
|
|
205
210
|
// 获取项目列表和统计信息(包含解析后的名称)
|
|
206
211
|
const { getProjectsWithStats, getProjectOrder } = require('../server/services/sessions');
|
|
207
|
-
const
|
|
208
|
-
const
|
|
212
|
+
const resolvedConfig = { ...config, projectsDir };
|
|
213
|
+
const projects = await getProjectsWithStats(resolvedConfig);
|
|
214
|
+
const savedOrder = getProjectOrder(resolvedConfig);
|
|
209
215
|
const projectList = Array.isArray(projects) ? projects : [];
|
|
210
216
|
|
|
211
217
|
// 按保存的顺序排列
|