@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.
- package/dist/auth.d.ts +19 -5
- package/dist/auth.js +83 -45
- package/dist/build-info.json +3 -3
- package/dist/cert.d.ts +1 -1
- package/dist/cert.js +124 -74
- package/dist/config.js +25 -8
- package/dist/express-async.d.ts +6 -0
- package/dist/express-async.js +28 -0
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +215 -76
- package/dist/git-utils.d.ts +4 -0
- package/dist/git-utils.js +60 -11
- package/dist/git-worktree.d.ts +8 -1
- package/dist/git-worktree.js +406 -41
- package/dist/models.d.ts +34 -4
- package/dist/models.js +334 -48
- package/dist/process-manager.d.ts +22 -30
- package/dist/process-manager.js +374 -441
- package/dist/provider-history-scanner.d.ts +54 -0
- package/dist/provider-history-scanner.js +354 -0
- package/dist/request-limits.d.ts +1 -0
- package/dist/request-limits.js +8 -0
- package/dist/resume-policy.d.ts +2 -0
- package/dist/resume-policy.js +5 -0
- package/dist/runtime-config.d.ts +16 -0
- package/dist/runtime-config.js +49 -0
- package/dist/server-file-routes.d.ts +17 -0
- package/dist/server-file-routes.js +653 -0
- package/dist/server-session-routes.d.ts +16 -3
- package/dist/server-session-routes.js +170 -149
- package/dist/server-settings-routes.d.ts +43 -0
- package/dist/server-settings-routes.js +225 -0
- package/dist/server-update-routes.d.ts +61 -0
- package/dist/server-update-routes.js +215 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +350 -1313
- package/dist/session-logger.d.ts +32 -2
- package/dist/session-logger.js +145 -15
- package/dist/session-registry.d.ts +27 -0
- package/dist/session-registry.js +153 -0
- package/dist/session-transport.d.ts +31 -0
- package/dist/session-transport.js +82 -0
- package/dist/storage.d.ts +24 -6
- package/dist/storage.js +291 -44
- package/dist/structured-claude-adapter.d.ts +19 -0
- package/dist/structured-claude-adapter.js +117 -0
- package/dist/structured-codex-adapter.d.ts +3 -0
- package/dist/structured-codex-adapter.js +29 -0
- package/dist/structured-opencode-adapter.d.ts +11 -0
- package/dist/structured-opencode-adapter.js +115 -0
- package/dist/structured-provider-common.d.ts +11 -0
- package/dist/structured-provider-common.js +77 -0
- package/dist/structured-session-manager.d.ts +32 -35
- package/dist/structured-session-manager.js +551 -605
- package/dist/types.d.ts +10 -0
- package/dist/update-helper.js +5 -1
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +16 -1
- package/dist/ws-broadcast.js +124 -58
- package/package.json +2 -1
package/dist/git-worktree.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
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(
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
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>;
|