coding-tool-x 3.6.11 → 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.
@@ -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 output = runGitCommand(['worktree', 'list', '--porcelain'], {
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
- try {
325
- // 尝试检出已有分支
326
- runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
327
- cwd: sourcePath
328
- });
329
- } catch (error) {
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({ branch: targetBranch, path: worktreePath });
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
- try {
524
- runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
525
- cwd: sourcePath
526
- });
527
- } catch (error) {
528
- try {
529
- const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
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({ branch: targetBranch, path: worktreePath });
671
+ worktrees.push(createdWorktree);
550
672
  } else if (isGit) {
551
673
  // Git 仓库但不创建 worktree:symlink 指向源路径
552
674
  worktrees = getGitWorktrees(sourcePath);
@@ -0,0 +1,7 @@
1
+ function hasHostFlag(argv = process.argv) {
2
+ return Array.isArray(argv) && (argv.includes('--host') || argv.includes('--hosts'));
3
+ }
4
+
5
+ module.exports = {
6
+ hasHostFlag
7
+ };
@@ -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.projectsDir, config.currentProject);
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.projectsDir;
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 projects = await getProjectsWithStats(config);
208
- const savedOrder = getProjectOrder(config);
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
  // 按保存的顺序排列