@co0ontty/wand 3.1.1 → 4.0.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.
Files changed (62) hide show
  1. package/dist/auth.d.ts +19 -5
  2. package/dist/auth.js +83 -45
  3. package/dist/build-info.json +3 -3
  4. package/dist/cert.d.ts +1 -1
  5. package/dist/cert.js +124 -74
  6. package/dist/config.js +25 -8
  7. package/dist/express-async.d.ts +6 -0
  8. package/dist/express-async.js +28 -0
  9. package/dist/git-quick-commit.d.ts +2 -0
  10. package/dist/git-quick-commit.js +215 -76
  11. package/dist/git-utils.d.ts +4 -0
  12. package/dist/git-utils.js +60 -11
  13. package/dist/git-worktree.d.ts +8 -1
  14. package/dist/git-worktree.js +406 -41
  15. package/dist/models.d.ts +34 -4
  16. package/dist/models.js +334 -48
  17. package/dist/process-manager.d.ts +22 -30
  18. package/dist/process-manager.js +374 -441
  19. package/dist/provider-history-scanner.d.ts +54 -0
  20. package/dist/provider-history-scanner.js +354 -0
  21. package/dist/request-limits.d.ts +1 -0
  22. package/dist/request-limits.js +8 -0
  23. package/dist/resume-policy.d.ts +2 -0
  24. package/dist/resume-policy.js +5 -0
  25. package/dist/runtime-config.d.ts +16 -0
  26. package/dist/runtime-config.js +49 -0
  27. package/dist/server-file-routes.d.ts +17 -0
  28. package/dist/server-file-routes.js +653 -0
  29. package/dist/server-session-routes.d.ts +16 -3
  30. package/dist/server-session-routes.js +170 -149
  31. package/dist/server-settings-routes.d.ts +43 -0
  32. package/dist/server-settings-routes.js +225 -0
  33. package/dist/server-update-routes.d.ts +61 -0
  34. package/dist/server-update-routes.js +215 -0
  35. package/dist/server.d.ts +6 -4
  36. package/dist/server.js +350 -1313
  37. package/dist/session-logger.d.ts +32 -2
  38. package/dist/session-logger.js +145 -15
  39. package/dist/session-registry.d.ts +27 -0
  40. package/dist/session-registry.js +153 -0
  41. package/dist/session-transport.d.ts +31 -0
  42. package/dist/session-transport.js +82 -0
  43. package/dist/storage.d.ts +24 -6
  44. package/dist/storage.js +291 -44
  45. package/dist/structured-claude-adapter.d.ts +19 -0
  46. package/dist/structured-claude-adapter.js +117 -0
  47. package/dist/structured-codex-adapter.d.ts +3 -0
  48. package/dist/structured-codex-adapter.js +29 -0
  49. package/dist/structured-opencode-adapter.d.ts +11 -0
  50. package/dist/structured-opencode-adapter.js +115 -0
  51. package/dist/structured-provider-common.d.ts +11 -0
  52. package/dist/structured-provider-common.js +77 -0
  53. package/dist/structured-session-manager.d.ts +32 -35
  54. package/dist/structured-session-manager.js +551 -605
  55. package/dist/types.d.ts +10 -0
  56. package/dist/update-helper.js +5 -1
  57. package/dist/web-ui/content/scripts.js +32 -32
  58. package/dist/web-ui/embedded-assets.d.ts +1 -1
  59. package/dist/web-ui/embedded-assets.js +2 -2
  60. package/dist/ws-broadcast.d.ts +16 -1
  61. package/dist/ws-broadcast.js +124 -58
  62. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { runGit, getGitErrorMessage } from "./git-utils.js";
3
+ import { runGit, runGitAsync, getGitErrorMessage } from "./git-utils.js";
4
4
  const WORKTREE_MERGE_ERROR_CODES = {
5
5
  MISSING: "WORKTREE_MISSING",
6
6
  DIRTY: "WORKTREE_DIRTY",
@@ -19,6 +19,15 @@ export class WorktreeMergeError extends Error {
19
19
  this.name = "WorktreeMergeError";
20
20
  }
21
21
  }
22
+ const DEFAULT_WORKTREE_GIT_TIMEOUT_MS = 30_000;
23
+ function resolveGitTimeout(value) {
24
+ return typeof value === "number" && Number.isFinite(value) && value > 0
25
+ ? Math.floor(value)
26
+ : DEFAULT_WORKTREE_GIT_TIMEOUT_MS;
27
+ }
28
+ function runWorktreeGit(args, cwd, timeoutMs) {
29
+ return runGit(args, cwd, { timeout: timeoutMs });
30
+ }
22
31
  function sanitizeBranchSegment(value) {
23
32
  return value
24
33
  .toLowerCase()
@@ -26,21 +35,21 @@ function sanitizeBranchSegment(value) {
26
35
  .replace(/^-+|-+$/g, "")
27
36
  .slice(0, 48) || "session";
28
37
  }
29
- function getCurrentBranch(repoRoot) {
30
- const branch = runGit(["branch", "--show-current"], repoRoot);
38
+ function getCurrentBranch(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
39
+ const branch = runWorktreeGit(["branch", "--show-current"], repoRoot, timeoutMs);
31
40
  return branch || "master";
32
41
  }
33
- function refExists(repoRoot, ref) {
42
+ function refExists(repoRoot, ref, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
34
43
  try {
35
- runGit(["rev-parse", "--verify", ref], repoRoot);
44
+ runWorktreeGit(["rev-parse", "--verify", ref], repoRoot, timeoutMs);
36
45
  return true;
37
46
  }
38
47
  catch {
39
48
  return false;
40
49
  }
41
50
  }
42
- function getRepoRootFromWorktree(worktreePath) {
43
- const repoRoot = runGit(["rev-parse", "--show-toplevel"], worktreePath);
51
+ function getRepoRootFromWorktree(worktreePath, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
52
+ const repoRoot = runWorktreeGit(["rev-parse", "--show-toplevel"], worktreePath, timeoutMs);
44
53
  if (!repoRoot || !existsSync(repoRoot)) {
45
54
  throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 仓库根目录不存在。", {
46
55
  cleanupDone: false,
@@ -59,8 +68,8 @@ function ensureWorktreePath(worktree) {
59
68
  }
60
69
  return worktreePath;
61
70
  }
62
- function getMainRepoRoot(repoRoot) {
63
- const commonDir = runGit(["rev-parse", "--git-common-dir"], repoRoot);
71
+ function getMainRepoRoot(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
72
+ const commonDir = runWorktreeGit(["rev-parse", "--git-common-dir"], repoRoot, timeoutMs);
64
73
  if (commonDir) {
65
74
  const maybeRoot = path.resolve(repoRoot, commonDir, "..");
66
75
  if (existsSync(maybeRoot)) {
@@ -69,9 +78,9 @@ function getMainRepoRoot(repoRoot) {
69
78
  }
70
79
  return repoRoot;
71
80
  }
72
- export function getDefaultBaseBranch(repoRoot) {
81
+ export function getDefaultBaseBranch(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
73
82
  try {
74
- const symbolicRef = runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], repoRoot);
83
+ const symbolicRef = runWorktreeGit(["symbolic-ref", "refs/remotes/origin/HEAD"], repoRoot, timeoutMs);
75
84
  const match = symbolicRef.match(/^refs\/remotes\/origin\/(.+)$/);
76
85
  if (match && match[1]) {
77
86
  return match[1];
@@ -80,46 +89,47 @@ export function getDefaultBaseBranch(repoRoot) {
80
89
  catch {
81
90
  // ignore and fallback below
82
91
  }
83
- const candidates = ["master", "main", getCurrentBranch(repoRoot)];
92
+ const candidates = ["master", "main", getCurrentBranch(repoRoot, timeoutMs)];
84
93
  for (const candidate of candidates) {
85
- if (candidate && refExists(repoRoot, candidate)) {
94
+ if (candidate && refExists(repoRoot, candidate, timeoutMs)) {
86
95
  return candidate;
87
96
  }
88
97
  }
89
98
  return "master";
90
99
  }
91
100
  function getMainRepoContext(options) {
101
+ const gitTimeoutMs = resolveGitTimeout(options.gitTimeoutMs);
92
102
  const worktreePath = ensureWorktreePath(options.worktree);
93
- const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath);
94
- const repoRoot = getMainRepoRoot(worktreeRepoRoot);
103
+ const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath, gitTimeoutMs);
104
+ const repoRoot = getMainRepoRoot(worktreeRepoRoot, gitTimeoutMs);
95
105
  const sourceBranch = options.worktree.branch;
96
- if (!refExists(repoRoot, sourceBranch)) {
106
+ if (!refExists(repoRoot, sourceBranch, gitTimeoutMs)) {
97
107
  throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 分支不存在,可能已被手动删除。", {
98
108
  cleanupDone: false,
99
109
  conflict: false,
100
110
  });
101
111
  }
102
- const targetBranch = options.targetBranch?.trim() || getDefaultBaseBranch(repoRoot);
103
- if (!refExists(repoRoot, targetBranch)) {
112
+ const targetBranch = options.targetBranch?.trim() || getDefaultBaseBranch(repoRoot, gitTimeoutMs);
113
+ if (!refExists(repoRoot, targetBranch, gitTimeoutMs)) {
104
114
  throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.TARGET_MISSING, `目标分支不存在:${targetBranch}`, {
105
115
  cleanupDone: false,
106
116
  conflict: false,
107
117
  targetBranch,
108
118
  });
109
119
  }
110
- return { repoRoot, worktreePath, sourceBranch, targetBranch };
120
+ return { repoRoot, worktreePath, sourceBranch, targetBranch, gitTimeoutMs };
111
121
  }
112
- function hasUncommittedChanges(worktreePath) {
113
- const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath);
114
- return runGit(["status", "--porcelain"], worktreeRepoRoot).length > 0;
122
+ function hasUncommittedChanges(worktreePath, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
123
+ const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath, timeoutMs);
124
+ return runWorktreeGit(["status", "--porcelain"], worktreeRepoRoot, timeoutMs).length > 0;
115
125
  }
116
- function getAheadCount(repoRoot, targetBranch, sourceBranch) {
117
- const count = runGit(["rev-list", "--count", `${targetBranch}..${sourceBranch}`], repoRoot);
126
+ function getAheadCount(repoRoot, targetBranch, sourceBranch, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
127
+ const count = runWorktreeGit(["rev-list", "--count", `${targetBranch}..${sourceBranch}`], repoRoot, timeoutMs);
118
128
  return Number.parseInt(count || "0", 10) || 0;
119
129
  }
120
- function checkConflicts(repoRoot, targetBranch, sourceBranch) {
130
+ function checkConflicts(repoRoot, targetBranch, sourceBranch, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
121
131
  try {
122
- runGit(["merge-tree", targetBranch, sourceBranch], repoRoot);
132
+ runWorktreeGit(["merge-tree", targetBranch, sourceBranch], repoRoot, timeoutMs);
123
133
  return false;
124
134
  }
125
135
  catch {
@@ -128,7 +138,7 @@ function checkConflicts(repoRoot, targetBranch, sourceBranch) {
128
138
  }
129
139
  function ensureMergeableContext(options) {
130
140
  const context = getMainRepoContext(options);
131
- if (hasUncommittedChanges(context.worktreePath)) {
141
+ if (hasUncommittedChanges(context.worktreePath, context.gitTimeoutMs)) {
132
142
  throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.DIRTY, "Worktree 中仍有未提交改动,请先提交后再合并。", {
133
143
  sourceBranch: context.sourceBranch,
134
144
  targetBranch: context.targetBranch,
@@ -137,7 +147,7 @@ function ensureMergeableContext(options) {
137
147
  conflict: false,
138
148
  });
139
149
  }
140
- const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch);
150
+ const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs);
141
151
  if (aheadCount <= 0) {
142
152
  throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.NOTHING_TO_MERGE, "当前 worktree 没有可合并到主分支的新提交。", {
143
153
  sourceBranch: context.sourceBranch,
@@ -170,12 +180,115 @@ function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts) {
170
180
  : undefined,
171
181
  };
172
182
  }
173
- function getHeadCommit(repoRoot) {
174
- return runGit(["rev-parse", "HEAD"], repoRoot);
183
+ function getHeadCommit(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
184
+ return runWorktreeGit(["rev-parse", "HEAD"], repoRoot, timeoutMs);
185
+ }
186
+ function captureCheckoutState(context) {
187
+ let branch = null;
188
+ try {
189
+ branch = runWorktreeGit(["symbolic-ref", "--quiet", "--short", "HEAD"], context.repoRoot, context.gitTimeoutMs) || null;
190
+ }
191
+ catch {
192
+ // A detached HEAD is a valid state and must be restored as such.
193
+ }
194
+ return {
195
+ branch,
196
+ head: getHeadCommit(context.repoRoot, context.gitTimeoutMs),
197
+ };
198
+ }
199
+ function isMergeInProgress(repoRoot, timeoutMs) {
200
+ try {
201
+ runWorktreeGit(["rev-parse", "--quiet", "--verify", "MERGE_HEAD"], repoRoot, timeoutMs);
202
+ return true;
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
208
+ function describeRollbackError(label, error) {
209
+ const detail = getGitErrorMessage(error);
210
+ return detail ? `${label}:${detail}` : label;
211
+ }
212
+ function rollbackFailedMerge(context, originalState, targetHead) {
213
+ const errors = [];
214
+ // The merge itself may have used a deliberately short request timeout. Give
215
+ // the local, non-networked recovery commands enough time to complete.
216
+ const recoveryTimeoutMs = Math.max(context.gitTimeoutMs, 1_000);
217
+ const attempt = (label, action) => {
218
+ try {
219
+ action();
220
+ }
221
+ catch (error) {
222
+ errors.push(describeRollbackError(label, error));
223
+ }
224
+ };
225
+ if (isMergeInProgress(context.repoRoot, recoveryTimeoutMs)) {
226
+ attempt("git merge --abort 失败", () => {
227
+ runWorktreeGit(["merge", "--abort"], context.repoRoot, recoveryTimeoutMs);
228
+ });
229
+ }
230
+ // A timeout can happen after Git has created the merge commit (for example,
231
+ // in a long-running post-merge hook), in which case MERGE_HEAD is already
232
+ // gone. Reset the target branch explicitly before restoring the old checkout.
233
+ let currentBranch = null;
234
+ try {
235
+ currentBranch = runWorktreeGit(["symbolic-ref", "--quiet", "--short", "HEAD"], context.repoRoot, recoveryTimeoutMs) || null;
236
+ }
237
+ catch {
238
+ // Detached HEAD, or a transient repository error handled by validation.
239
+ }
240
+ if (currentBranch === context.targetBranch) {
241
+ attempt("恢复目标分支 HEAD 失败", () => {
242
+ const currentHead = getHeadCommit(context.repoRoot, recoveryTimeoutMs);
243
+ if (currentHead !== targetHead) {
244
+ runWorktreeGit(["reset", "--merge", targetHead], context.repoRoot, recoveryTimeoutMs);
245
+ }
246
+ });
247
+ }
248
+ if (originalState.branch) {
249
+ attempt("恢复原分支失败", () => {
250
+ const branch = runWorktreeGit(["symbolic-ref", "--quiet", "--short", "HEAD"], context.repoRoot, recoveryTimeoutMs);
251
+ if (branch !== originalState.branch) {
252
+ runWorktreeGit(["checkout", "--quiet", originalState.branch], context.repoRoot, recoveryTimeoutMs);
253
+ }
254
+ });
255
+ attempt("恢复原分支 HEAD 失败", () => {
256
+ const currentHead = getHeadCommit(context.repoRoot, recoveryTimeoutMs);
257
+ if (currentHead !== originalState.head) {
258
+ runWorktreeGit(["reset", "--merge", originalState.head], context.repoRoot, recoveryTimeoutMs);
259
+ }
260
+ });
261
+ }
262
+ else {
263
+ attempt("恢复 detached HEAD 失败", () => {
264
+ const branch = runWorktreeGit(["branch", "--show-current"], context.repoRoot, recoveryTimeoutMs);
265
+ const currentHead = getHeadCommit(context.repoRoot, recoveryTimeoutMs);
266
+ if (branch || currentHead !== originalState.head) {
267
+ runWorktreeGit(["checkout", "--quiet", "--detach", originalState.head], context.repoRoot, recoveryTimeoutMs);
268
+ }
269
+ });
270
+ }
271
+ if (isMergeInProgress(context.repoRoot, recoveryTimeoutMs)) {
272
+ errors.push("恢复后仓库仍存在未完成的 merge");
273
+ }
274
+ attempt("校验目标分支 HEAD 失败", () => {
275
+ const restoredTargetHead = runWorktreeGit(["rev-parse", context.targetBranch], context.repoRoot, recoveryTimeoutMs);
276
+ if (restoredTargetHead !== targetHead) {
277
+ throw new Error(`期望 ${targetHead},实际 ${restoredTargetHead}`);
278
+ }
279
+ });
280
+ attempt("校验原 checkout 状态失败", () => {
281
+ const restored = captureCheckoutState({ ...context, gitTimeoutMs: recoveryTimeoutMs });
282
+ if (restored.branch !== originalState.branch || restored.head !== originalState.head) {
283
+ throw new Error(`期望 ${originalState.branch ?? "detached"}@${originalState.head},` +
284
+ `实际 ${restored.branch ?? "detached"}@${restored.head}`);
285
+ }
286
+ });
287
+ return errors;
175
288
  }
176
289
  function cleanupMergedWorktree(context) {
177
- runGit(["worktree", "remove", context.worktreePath], context.repoRoot);
178
- runGit(["branch", "-d", context.sourceBranch], context.repoRoot);
290
+ runWorktreeGit(["worktree", "remove", context.worktreePath], context.repoRoot, context.gitTimeoutMs);
291
+ runWorktreeGit(["branch", "-d", context.sourceBranch], context.repoRoot, context.gitTimeoutMs);
179
292
  return true;
180
293
  }
181
294
  export function getWorktreeMergeErrorCode(error) {
@@ -183,20 +296,20 @@ export function getWorktreeMergeErrorCode(error) {
183
296
  }
184
297
  export function checkSessionWorktreeMergeability(options) {
185
298
  const context = getMainRepoContext(options);
186
- const hasDirtyChanges = hasUncommittedChanges(context.worktreePath);
187
- const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch);
299
+ const hasDirtyChanges = hasUncommittedChanges(context.worktreePath, context.gitTimeoutMs);
300
+ const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs);
188
301
  const hasConflicts = !hasDirtyChanges && aheadCount > 0
189
- ? checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch)
302
+ ? checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs)
190
303
  : false;
191
304
  return buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts);
192
305
  }
193
306
  export function cleanupSessionWorktree(options) {
194
- const context = getMainRepoContext({ worktree: options.worktree });
307
+ const context = getMainRepoContext(options);
195
308
  return cleanupMergedWorktree(context);
196
309
  }
197
310
  export function mergeSessionWorktree(options) {
198
311
  const context = ensureMergeableContext(options);
199
- const hasConflicts = checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch);
312
+ const hasConflicts = checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs);
200
313
  if (hasConflicts) {
201
314
  throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, "合并检测到冲突,请先手动处理。", {
202
315
  sourceBranch: context.sourceBranch,
@@ -206,13 +319,28 @@ export function mergeSessionWorktree(options) {
206
319
  conflict: true,
207
320
  });
208
321
  }
322
+ const originalState = captureCheckoutState(context);
323
+ const targetHead = runWorktreeGit(["rev-parse", context.targetBranch], context.repoRoot, context.gitTimeoutMs);
324
+ if (isMergeInProgress(context.repoRoot, context.gitTimeoutMs)) {
325
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, "主工作区已有未完成的 merge,请先处理后再合并 worktree。", {
326
+ sourceBranch: context.sourceBranch,
327
+ targetBranch: context.targetBranch,
328
+ repoRoot: context.repoRoot,
329
+ cleanupDone: false,
330
+ conflict: true,
331
+ });
332
+ }
209
333
  try {
210
- runGit(["checkout", context.targetBranch], context.repoRoot);
211
- runGit(["merge", "--no-ff", "--no-edit", context.sourceBranch], context.repoRoot);
334
+ runWorktreeGit(["checkout", context.targetBranch], context.repoRoot, context.gitTimeoutMs);
335
+ runWorktreeGit(["merge", "--no-ff", "--no-edit", "--no-gpg-sign", context.sourceBranch], context.repoRoot, context.gitTimeoutMs);
212
336
  }
213
337
  catch (error) {
214
338
  const message = getGitErrorMessage(error);
215
- throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, message || "合并失败,可能存在冲突。", {
339
+ const rollbackErrors = rollbackFailedMerge(context, originalState, targetHead);
340
+ const rollbackMessage = rollbackErrors.length > 0
341
+ ? `\n自动恢复主工作区时仍有问题:${rollbackErrors.join(";")}`
342
+ : "\n主工作区已恢复到合并前状态。";
343
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, `${message || "合并失败,可能存在冲突。"}${rollbackMessage}`, {
216
344
  sourceBranch: context.sourceBranch,
217
345
  targetBranch: context.targetBranch,
218
346
  repoRoot: context.repoRoot,
@@ -221,7 +349,7 @@ export function mergeSessionWorktree(options) {
221
349
  });
222
350
  }
223
351
  const mergedAt = new Date().toISOString();
224
- const mergeCommit = getHeadCommit(context.repoRoot);
352
+ const mergeCommit = getHeadCommit(context.repoRoot, context.gitTimeoutMs);
225
353
  try {
226
354
  cleanupMergedWorktree(context);
227
355
  return {
@@ -247,6 +375,243 @@ export function mergeSessionWorktree(options) {
247
375
  });
248
376
  }
249
377
  }
378
+ function runWorktreeGitAsync(args, cwd, timeoutMs) {
379
+ return runGitAsync(args, cwd, { timeout: timeoutMs });
380
+ }
381
+ async function refExistsAsync(repoRoot, ref, timeoutMs) {
382
+ try {
383
+ await runWorktreeGitAsync(["rev-parse", "--verify", ref], repoRoot, timeoutMs);
384
+ return true;
385
+ }
386
+ catch {
387
+ return false;
388
+ }
389
+ }
390
+ async function getRepoRootFromWorktreeAsync(worktreePath, timeoutMs) {
391
+ const repoRoot = await runWorktreeGitAsync(["rev-parse", "--show-toplevel"], worktreePath, timeoutMs);
392
+ if (!repoRoot || !existsSync(repoRoot)) {
393
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 仓库根目录不存在。", {
394
+ cleanupDone: false,
395
+ conflict: false,
396
+ });
397
+ }
398
+ return repoRoot;
399
+ }
400
+ async function getMainRepoRootAsync(repoRoot, timeoutMs) {
401
+ const commonDir = await runWorktreeGitAsync(["rev-parse", "--git-common-dir"], repoRoot, timeoutMs);
402
+ if (commonDir) {
403
+ const maybeRoot = path.resolve(repoRoot, commonDir, "..");
404
+ if (existsSync(maybeRoot))
405
+ return maybeRoot;
406
+ }
407
+ return repoRoot;
408
+ }
409
+ async function getDefaultBaseBranchAsync(repoRoot, timeoutMs) {
410
+ try {
411
+ const symbolicRef = await runWorktreeGitAsync(["symbolic-ref", "refs/remotes/origin/HEAD"], repoRoot, timeoutMs);
412
+ const match = symbolicRef.match(/^refs\/remotes\/origin\/(.+)$/);
413
+ if (match?.[1])
414
+ return match[1];
415
+ }
416
+ catch { /* use local branches */ }
417
+ let current = "master";
418
+ try {
419
+ current = await runWorktreeGitAsync(["branch", "--show-current"], repoRoot, timeoutMs) || "master";
420
+ }
421
+ catch { /* fallback */ }
422
+ for (const candidate of ["master", "main", current]) {
423
+ if (candidate && await refExistsAsync(repoRoot, candidate, timeoutMs))
424
+ return candidate;
425
+ }
426
+ return "master";
427
+ }
428
+ async function getMainRepoContextAsync(options) {
429
+ const gitTimeoutMs = resolveGitTimeout(options.gitTimeoutMs);
430
+ const worktreePath = ensureWorktreePath(options.worktree);
431
+ const worktreeRepoRoot = await getRepoRootFromWorktreeAsync(worktreePath, gitTimeoutMs);
432
+ const repoRoot = await getMainRepoRootAsync(worktreeRepoRoot, gitTimeoutMs);
433
+ const sourceBranch = options.worktree.branch;
434
+ if (!await refExistsAsync(repoRoot, sourceBranch, gitTimeoutMs)) {
435
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 分支不存在,可能已被手动删除。", {
436
+ cleanupDone: false,
437
+ conflict: false,
438
+ });
439
+ }
440
+ const targetBranch = options.targetBranch?.trim() || await getDefaultBaseBranchAsync(repoRoot, gitTimeoutMs);
441
+ if (!await refExistsAsync(repoRoot, targetBranch, gitTimeoutMs)) {
442
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.TARGET_MISSING, `目标分支不存在:${targetBranch}`, {
443
+ cleanupDone: false,
444
+ conflict: false,
445
+ targetBranch,
446
+ });
447
+ }
448
+ return { repoRoot, worktreePath, sourceBranch, targetBranch, gitTimeoutMs };
449
+ }
450
+ async function checkConflictsAsync(context) {
451
+ try {
452
+ await runWorktreeGitAsync(["merge-tree", context.targetBranch, context.sourceBranch], context.repoRoot, context.gitTimeoutMs);
453
+ return false;
454
+ }
455
+ catch {
456
+ return true;
457
+ }
458
+ }
459
+ async function captureCheckoutStateAsync(context) {
460
+ let branch = null;
461
+ try {
462
+ branch = await runWorktreeGitAsync(["symbolic-ref", "--quiet", "--short", "HEAD"], context.repoRoot, context.gitTimeoutMs) || null;
463
+ }
464
+ catch { /* detached HEAD */ }
465
+ return {
466
+ branch,
467
+ head: await runWorktreeGitAsync(["rev-parse", "HEAD"], context.repoRoot, context.gitTimeoutMs),
468
+ };
469
+ }
470
+ async function isMergeInProgressAsync(repoRoot, timeoutMs) {
471
+ try {
472
+ await runWorktreeGitAsync(["rev-parse", "--quiet", "--verify", "MERGE_HEAD"], repoRoot, timeoutMs);
473
+ return true;
474
+ }
475
+ catch {
476
+ return false;
477
+ }
478
+ }
479
+ async function rollbackFailedMergeAsync(context, originalState, targetHead) {
480
+ const errors = [];
481
+ const timeoutMs = Math.max(context.gitTimeoutMs, 1_000);
482
+ const attempt = async (label, action) => {
483
+ try {
484
+ await action();
485
+ }
486
+ catch (error) {
487
+ errors.push(describeRollbackError(label, error));
488
+ }
489
+ };
490
+ if (await isMergeInProgressAsync(context.repoRoot, timeoutMs)) {
491
+ await attempt("git merge --abort 失败", async () => {
492
+ await runWorktreeGitAsync(["merge", "--abort"], context.repoRoot, timeoutMs);
493
+ });
494
+ }
495
+ let currentBranch = null;
496
+ try {
497
+ currentBranch = await runWorktreeGitAsync(["symbolic-ref", "--quiet", "--short", "HEAD"], context.repoRoot, timeoutMs) || null;
498
+ }
499
+ catch { /* detached */ }
500
+ if (currentBranch === context.targetBranch) {
501
+ await attempt("恢复目标分支 HEAD 失败", async () => {
502
+ const currentHead = await runWorktreeGitAsync(["rev-parse", "HEAD"], context.repoRoot, timeoutMs);
503
+ if (currentHead !== targetHead)
504
+ await runWorktreeGitAsync(["reset", "--merge", targetHead], context.repoRoot, timeoutMs);
505
+ });
506
+ }
507
+ if (originalState.branch) {
508
+ await attempt("恢复原分支失败", async () => {
509
+ const branch = await runWorktreeGitAsync(["symbolic-ref", "--quiet", "--short", "HEAD"], context.repoRoot, timeoutMs);
510
+ if (branch !== originalState.branch)
511
+ await runWorktreeGitAsync(["checkout", "--quiet", originalState.branch], context.repoRoot, timeoutMs);
512
+ });
513
+ await attempt("恢复原分支 HEAD 失败", async () => {
514
+ const currentHead = await runWorktreeGitAsync(["rev-parse", "HEAD"], context.repoRoot, timeoutMs);
515
+ if (currentHead !== originalState.head)
516
+ await runWorktreeGitAsync(["reset", "--merge", originalState.head], context.repoRoot, timeoutMs);
517
+ });
518
+ }
519
+ else {
520
+ await attempt("恢复 detached HEAD 失败", async () => {
521
+ const branch = await runWorktreeGitAsync(["branch", "--show-current"], context.repoRoot, timeoutMs);
522
+ const head = await runWorktreeGitAsync(["rev-parse", "HEAD"], context.repoRoot, timeoutMs);
523
+ if (branch || head !== originalState.head) {
524
+ await runWorktreeGitAsync(["checkout", "--quiet", "--detach", originalState.head], context.repoRoot, timeoutMs);
525
+ }
526
+ });
527
+ }
528
+ if (await isMergeInProgressAsync(context.repoRoot, timeoutMs))
529
+ errors.push("恢复后仓库仍存在未完成的 merge");
530
+ await attempt("校验目标分支 HEAD 失败", async () => {
531
+ const restored = await runWorktreeGitAsync(["rev-parse", context.targetBranch], context.repoRoot, timeoutMs);
532
+ if (restored !== targetHead)
533
+ throw new Error(`期望 ${targetHead},实际 ${restored}`);
534
+ });
535
+ await attempt("校验原 checkout 状态失败", async () => {
536
+ const restored = await captureCheckoutStateAsync({ ...context, gitTimeoutMs: timeoutMs });
537
+ if (restored.branch !== originalState.branch || restored.head !== originalState.head) {
538
+ throw new Error(`期望 ${originalState.branch ?? "detached"}@${originalState.head},实际 ${restored.branch ?? "detached"}@${restored.head}`);
539
+ }
540
+ });
541
+ return errors;
542
+ }
543
+ async function cleanupMergedWorktreeAsync(context) {
544
+ await runWorktreeGitAsync(["worktree", "remove", context.worktreePath], context.repoRoot, context.gitTimeoutMs);
545
+ await runWorktreeGitAsync(["branch", "-d", context.sourceBranch], context.repoRoot, context.gitTimeoutMs);
546
+ return true;
547
+ }
548
+ /** Async HTTP-facing worktree check; commands remain deliberately serial. */
549
+ export async function checkSessionWorktreeMergeabilityAsync(options) {
550
+ const context = await getMainRepoContextAsync(options);
551
+ const dirty = (await runWorktreeGitAsync(["status", "--porcelain"], context.worktreePath, context.gitTimeoutMs)).length > 0;
552
+ const aheadCount = Number.parseInt(await runWorktreeGitAsync(["rev-list", "--count", `${context.targetBranch}..${context.sourceBranch}`], context.repoRoot, context.gitTimeoutMs) || "0", 10) || 0;
553
+ const conflicts = !dirty && aheadCount > 0 ? await checkConflictsAsync(context) : false;
554
+ return buildCheckResult(context, aheadCount, dirty, conflicts);
555
+ }
556
+ export async function cleanupSessionWorktreeAsync(options) {
557
+ return cleanupMergedWorktreeAsync(await getMainRepoContextAsync(options));
558
+ }
559
+ /** Async HTTP-facing merge with serial, non-cancellable rollback. */
560
+ export async function mergeSessionWorktreeAsync(options) {
561
+ const context = await getMainRepoContextAsync(options);
562
+ const dirty = (await runWorktreeGitAsync(["status", "--porcelain"], context.worktreePath, context.gitTimeoutMs)).length > 0;
563
+ if (dirty) {
564
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.DIRTY, "Worktree 中仍有未提交改动,请先提交后再合并。", {
565
+ sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot, cleanupDone: false, conflict: false,
566
+ });
567
+ }
568
+ const aheadCount = Number.parseInt(await runWorktreeGitAsync(["rev-list", "--count", `${context.targetBranch}..${context.sourceBranch}`], context.repoRoot, context.gitTimeoutMs) || "0", 10) || 0;
569
+ if (aheadCount <= 0) {
570
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.NOTHING_TO_MERGE, "当前 worktree 没有可合并到主分支的新提交。", {
571
+ sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot, cleanupDone: false, conflict: false,
572
+ });
573
+ }
574
+ if (await checkConflictsAsync(context)) {
575
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, "合并检测到冲突,请先手动处理。", {
576
+ sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot, cleanupDone: false, conflict: true,
577
+ });
578
+ }
579
+ const originalState = await captureCheckoutStateAsync(context);
580
+ const targetHead = await runWorktreeGitAsync(["rev-parse", context.targetBranch], context.repoRoot, context.gitTimeoutMs);
581
+ if (await isMergeInProgressAsync(context.repoRoot, context.gitTimeoutMs)) {
582
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, "主工作区已有未完成的 merge,请先处理后再合并 worktree。", {
583
+ sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot, cleanupDone: false, conflict: true,
584
+ });
585
+ }
586
+ try {
587
+ await runWorktreeGitAsync(["checkout", context.targetBranch], context.repoRoot, context.gitTimeoutMs);
588
+ await runWorktreeGitAsync(["merge", "--no-ff", "--no-edit", "--no-gpg-sign", context.sourceBranch], context.repoRoot, context.gitTimeoutMs);
589
+ }
590
+ catch (error) {
591
+ const rollbackErrors = await rollbackFailedMergeAsync(context, originalState, targetHead);
592
+ const rollbackMessage = rollbackErrors.length > 0
593
+ ? `\n自动恢复主工作区时仍有问题:${rollbackErrors.join(";")}`
594
+ : "\n主工作区已恢复到合并前状态。";
595
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, `${getGitErrorMessage(error) || "合并失败,可能存在冲突。"}${rollbackMessage}`, {
596
+ sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot, cleanupDone: false, conflict: true,
597
+ });
598
+ }
599
+ const mergedAt = new Date().toISOString();
600
+ const mergeCommit = await runWorktreeGitAsync(["rev-parse", "HEAD"], context.repoRoot, context.gitTimeoutMs);
601
+ try {
602
+ await cleanupMergedWorktreeAsync(context);
603
+ return {
604
+ ok: true, sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot,
605
+ mergeCommit, mergedAt, cleanupDone: true, conflict: false,
606
+ };
607
+ }
608
+ catch (error) {
609
+ throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CLEANUP_FAILED, getGitErrorMessage(error) || "已合并,但清理 worktree 失败。", {
610
+ sourceBranch: context.sourceBranch, targetBranch: context.targetBranch, repoRoot: context.repoRoot,
611
+ mergeCommit, mergedAt, cleanupDone: false, conflict: false,
612
+ });
613
+ }
614
+ }
250
615
  export function prepareSessionWorktree(options) {
251
616
  const resolvedCwd = path.resolve(options.cwd);
252
617
  const repoRoot = runGit(["rev-parse", "--show-toplevel"], resolvedCwd);
package/dist/models.d.ts CHANGED
@@ -1,5 +1,36 @@
1
1
  import { ClaudeModelInfo } from "./types.js";
2
- interface ModelCache {
2
+ export interface ModelCacheStorage {
3
+ getConfigValue(key: string): string | null;
4
+ setConfigValue(key: string, value: string): void;
5
+ }
6
+ export interface ModelCommandOptions {
7
+ env: NodeJS.ProcessEnv;
8
+ timeout: number;
9
+ }
10
+ export interface ModelCommandResult {
11
+ stdout: string;
12
+ stderr: string;
13
+ }
14
+ export type ModelCommandRunner = (file: string, args: string[], options: ModelCommandOptions) => Promise<ModelCommandResult>;
15
+ export interface ClaudeModelsApiEntry {
16
+ id: string;
17
+ display_name?: string;
18
+ }
19
+ export interface ClaudeModelsApi {
20
+ list(): AsyncIterable<ClaudeModelsApiEntry>;
21
+ }
22
+ export interface ModelRefreshOptions {
23
+ storage?: ModelCacheStorage;
24
+ configuredClaudeModels?: readonly (string | null | undefined)[];
25
+ inheritEnv?: boolean;
26
+ env?: NodeJS.ProcessEnv;
27
+ apiKey?: string;
28
+ commandRunner?: ModelCommandRunner;
29
+ modelsApi?: ClaudeModelsApi;
30
+ verifyClaudeCandidates?: boolean;
31
+ now?: () => Date;
32
+ }
33
+ export interface ModelCache {
3
34
  models: ClaudeModelInfo[];
4
35
  codexModels: ClaudeModelInfo[];
5
36
  opencodeModels: ClaudeModelInfo[];
@@ -11,6 +42,5 @@ interface ModelCache {
11
42
  export declare function parseOpenCodeModels(stdout: string): ClaudeModelInfo[];
12
43
  /** Parse the machine-readable model registry emitted by the installed Codex CLI. */
13
44
  export declare function parseCodexModels(stdout: string): ClaudeModelInfo[];
14
- export declare function getCachedModels(): ModelCache;
15
- export declare function refreshModels(): Promise<ModelCache>;
16
- export {};
45
+ export declare function getCachedModels(options?: ModelRefreshOptions): ModelCache;
46
+ export declare function refreshModels(options?: ModelRefreshOptions): Promise<ModelCache>;