@co0ontty/wand 4.26.1 → 4.26.2-beta.gff48fb6
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/build-info.json +4 -4
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -3
- package/dist/git-quick-commit.d.ts +0 -1
- package/dist/git-quick-commit.js +1 -183
- package/dist/git-worktree.d.ts +0 -4
- package/dist/git-worktree.js +0 -298
- package/dist/models.d.ts +0 -5
- package/dist/models.js +0 -8
- package/dist/resume-policy.d.ts +0 -2
- package/dist/resume-policy.js +0 -10
- package/dist/server-session-routes.d.ts +1 -1
- package/dist/server-session-routes.js +1 -1
- package/dist/server-settings-routes.js +1 -1
- package/dist/tui/layout.js +1 -4
- package/dist/web-ui/content/scripts.js +58 -58
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "4.26.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "ff48fb6d343b7941766c08633bc33ac7c772a554",
|
|
3
|
+
"builtAt": "2026-07-26T12:33:59.190Z",
|
|
4
|
+
"version": "4.26.2-beta.gff48fb6",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -56,4 +56,3 @@ export declare function getProviderDefaultModels(config: Pick<WandConfig, "defau
|
|
|
56
56
|
qoder: string;
|
|
57
57
|
};
|
|
58
58
|
export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">, provider: SessionProvider | undefined): string;
|
|
59
|
-
export declare function normalizeMode(input: string | undefined, fallback: ExecutionMode): ExecutionMode;
|
package/dist/config.js
CHANGED
|
@@ -691,9 +691,6 @@ export function getDefaultModelForProvider(config, provider) {
|
|
|
691
691
|
return defaults.qoder;
|
|
692
692
|
return defaults.claude;
|
|
693
693
|
}
|
|
694
|
-
export function normalizeMode(input, fallback) {
|
|
695
|
-
return isExecutionMode(input) ? input : fallback;
|
|
696
|
-
}
|
|
697
694
|
function normalizePresetCommand(command) {
|
|
698
695
|
const trimmed = command.trim();
|
|
699
696
|
if (trimmed === "cloud-code" || trimmed === "cloudcode" || trimmed === "claude code") {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, SessionSnapshot, TagHeadResult } from "./types.js";
|
|
2
2
|
export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "AI_FALLBACK_FAILED" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
|
|
3
|
-
export declare function getGitStatus(cwd: string): GitStatusResult;
|
|
4
3
|
/** Non-blocking status implementation used by HTTP routes. */
|
|
5
4
|
export declare function getGitStatusAsync(cwd: string): Promise<GitStatusResult>;
|
|
6
5
|
interface QuickCommitOptions {
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
4
4
|
import { callSystemAiTextWithFallback } from "./system-ai.js";
|
|
5
5
|
import { buildChildEnv } from "./env-utils.js";
|
|
6
|
-
import {
|
|
6
|
+
import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
|
|
7
7
|
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, } from "./structured-provider-common.js";
|
|
8
8
|
const GIT_TIMEOUT_MS = 1500;
|
|
9
9
|
const GIT_PUSH_TIMEOUT_MS = 30_000;
|
|
@@ -19,34 +19,12 @@ const DIRECT_API_PROFILE_TIMEOUT_MS = 20_000;
|
|
|
19
19
|
const QUICK_COMMIT_CLI_TIMEOUT_MS = 120_000;
|
|
20
20
|
const MAX_DIFF_FOR_AI = 100_000;
|
|
21
21
|
const GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
22
|
-
function runGit(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
23
|
-
return runGitBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
24
|
-
}
|
|
25
|
-
function runGitAllowEmpty(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
26
|
-
return runGitRawBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
27
|
-
}
|
|
28
22
|
function runGitAsync(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
29
23
|
return runGitAsyncBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
30
24
|
}
|
|
31
25
|
function runGitAllowEmptyAsync(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
32
26
|
return runGitRawAsyncBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
33
27
|
}
|
|
34
|
-
/** Throws `QuickCommitError` if `cwd` isn't an existing path inside a git work tree. */
|
|
35
|
-
function assertGitWorkTree(cwd) {
|
|
36
|
-
if (!cwd || !existsSync(cwd)) {
|
|
37
|
-
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
38
|
-
}
|
|
39
|
-
let isInside;
|
|
40
|
-
try {
|
|
41
|
-
isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
|
|
45
|
-
}
|
|
46
|
-
if (isInside !== "true") {
|
|
47
|
-
throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
28
|
async function assertGitWorkTreeAsync(cwd) {
|
|
51
29
|
if (!cwd || !existsSync(cwd))
|
|
52
30
|
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
@@ -61,22 +39,6 @@ async function assertGitWorkTreeAsync(cwd) {
|
|
|
61
39
|
throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
|
|
62
40
|
}
|
|
63
41
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Resolve the remote to push to. Prefers `branch.<name>.remote` config for the
|
|
66
|
-
* current branch, falls back to `origin`. Never throws.
|
|
67
|
-
*/
|
|
68
|
-
function resolvePushRemote(cwd) {
|
|
69
|
-
try {
|
|
70
|
-
const branch = runGit(["branch", "--show-current"], cwd);
|
|
71
|
-
if (branch) {
|
|
72
|
-
return runGit(["config", "--get", `branch.${branch}.remote`], cwd) || "origin";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// ignore — fall through to default
|
|
77
|
-
}
|
|
78
|
-
return "origin";
|
|
79
|
-
}
|
|
80
42
|
async function resolvePushRemoteAsync(cwd) {
|
|
81
43
|
try {
|
|
82
44
|
const branch = await runGitAsync(["branch", "--show-current"], cwd);
|
|
@@ -195,150 +157,6 @@ function parsePorcelainV2(raw) {
|
|
|
195
157
|
}
|
|
196
158
|
return out;
|
|
197
159
|
}
|
|
198
|
-
/**
|
|
199
|
-
* 仓库是否声明了 submodule(读 repo 根的 .gitmodules)。不能只看 `git status`——一个
|
|
200
|
-
* clean 的 submodule 不会出现在 status 里,但「是否提供 Submodule 选项」应基于声明而非当前改动。
|
|
201
|
-
*/
|
|
202
|
-
function repoDeclaresSubmodule(repoRoot) {
|
|
203
|
-
if (!repoRoot)
|
|
204
|
-
return false;
|
|
205
|
-
const gitmodules = `${repoRoot}/.gitmodules`;
|
|
206
|
-
if (!existsSync(gitmodules))
|
|
207
|
-
return false;
|
|
208
|
-
try {
|
|
209
|
-
const out = runGitAllowEmpty(["config", "-f", gitmodules, "--get-regexp", "\\.path$"], repoRoot).trim();
|
|
210
|
-
return out.length > 0;
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
export function getGitStatus(cwd) {
|
|
217
|
-
if (!cwd || !existsSync(cwd)) {
|
|
218
|
-
return { isGit: false, error: "工作目录不存在。" };
|
|
219
|
-
}
|
|
220
|
-
let isInside;
|
|
221
|
-
try {
|
|
222
|
-
isInside = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return { isGit: false };
|
|
226
|
-
}
|
|
227
|
-
if (isInside !== "true") {
|
|
228
|
-
return { isGit: false };
|
|
229
|
-
}
|
|
230
|
-
let repoRoot;
|
|
231
|
-
try {
|
|
232
|
-
repoRoot = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
repoRoot = undefined;
|
|
236
|
-
}
|
|
237
|
-
let branch;
|
|
238
|
-
try {
|
|
239
|
-
branch = runGit(["branch", "--show-current"], cwd);
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
return { isGit: true, repoRoot, error: getGitErrorMessage(error) };
|
|
243
|
-
}
|
|
244
|
-
if (!branch) {
|
|
245
|
-
try {
|
|
246
|
-
branch = `HEAD (${runGit(["rev-parse", "--short", "HEAD"], cwd)})`;
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
branch = "HEAD";
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
let head;
|
|
253
|
-
let initialCommit = false;
|
|
254
|
-
try {
|
|
255
|
-
head = runGit(["rev-parse", "HEAD"], cwd);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
initialCommit = true;
|
|
259
|
-
}
|
|
260
|
-
let porcelain;
|
|
261
|
-
try {
|
|
262
|
-
porcelain = runGitAllowEmpty(["status", "--porcelain=v2", "--untracked-files=all"], cwd);
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
return { isGit: true, branch, repoRoot, head, initialCommit, error: getGitErrorMessage(error) };
|
|
266
|
-
}
|
|
267
|
-
const allEntries = parsePorcelainV2(porcelain);
|
|
268
|
-
const files = allEntries.slice(0, MAX_FILE_ENTRIES);
|
|
269
|
-
let upstream;
|
|
270
|
-
let ahead;
|
|
271
|
-
let behind;
|
|
272
|
-
let lastCommit;
|
|
273
|
-
if (!initialCommit) {
|
|
274
|
-
try {
|
|
275
|
-
upstream = runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd) || undefined;
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
upstream = undefined;
|
|
279
|
-
}
|
|
280
|
-
if (upstream) {
|
|
281
|
-
try {
|
|
282
|
-
const counts = runGit(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], cwd);
|
|
283
|
-
const parts = counts.split(/\s+/).filter(Boolean);
|
|
284
|
-
if (parts.length === 2) {
|
|
285
|
-
const b = Number.parseInt(parts[0], 10);
|
|
286
|
-
const a = Number.parseInt(parts[1], 10);
|
|
287
|
-
if (!Number.isNaN(a))
|
|
288
|
-
ahead = a;
|
|
289
|
-
if (!Number.isNaN(b))
|
|
290
|
-
behind = b;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// ignore — counts stay undefined
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const raw = runGit(["log", "-1", "--pretty=format:%H%x09%h%x09%s"], cwd);
|
|
299
|
-
const parts = raw.split("\t");
|
|
300
|
-
if (parts.length >= 3) {
|
|
301
|
-
lastCommit = { hash: parts[0], shortHash: parts[1], subject: parts.slice(2).join("\t") };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
// ignore
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// NOTE: we intentionally do NOT probe the remote for unpushed tags here.
|
|
309
|
-
// `ls-remote` is a synchronous network call that can block the event loop
|
|
310
|
-
// for seconds. The "unpushed tag" UI chip is best-effort and a separate
|
|
311
|
-
// async endpoint should compute it on demand if reintroduced.
|
|
312
|
-
// Latest tag only. Next tag suggestions are intentionally AI-derived from the
|
|
313
|
-
// diff so releases do not silently follow a fixed patch-bump rule.
|
|
314
|
-
let latestTag;
|
|
315
|
-
if (!initialCommit) {
|
|
316
|
-
try {
|
|
317
|
-
latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
latestTag = undefined;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
isGit: true,
|
|
325
|
-
branch,
|
|
326
|
-
modifiedCount: allEntries.length,
|
|
327
|
-
files,
|
|
328
|
-
head,
|
|
329
|
-
repoRoot,
|
|
330
|
-
initialCommit,
|
|
331
|
-
upstream,
|
|
332
|
-
ahead,
|
|
333
|
-
behind,
|
|
334
|
-
lastCommit,
|
|
335
|
-
latestTag,
|
|
336
|
-
// 供前端决定是否渲染 Submodule 球。status 全量条目(不受 files 的 200 条 slice 影响)
|
|
337
|
-
// 只能看到「有改动」的 submodule,clean submodule 不会出现在 status 里,所以再用
|
|
338
|
-
// .gitmodules 声明兜底——只要仓库声明了 submodule 就提供该选项。
|
|
339
|
-
hasSubmodule: allEntries.some((e) => e.isSubmodule) || repoDeclaresSubmodule(repoRoot),
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
160
|
async function repoDeclaresSubmoduleAsync(repoRoot) {
|
|
343
161
|
if (!repoRoot)
|
|
344
162
|
return false;
|
package/dist/git-worktree.d.ts
CHANGED
|
@@ -21,11 +21,7 @@ export declare class WorktreeMergeError extends Error {
|
|
|
21
21
|
readonly result?: Partial<WorktreeMergeResult> | undefined;
|
|
22
22
|
constructor(code: string, message: string, result?: Partial<WorktreeMergeResult> | undefined);
|
|
23
23
|
}
|
|
24
|
-
export declare function getDefaultBaseBranch(repoRoot: string, timeoutMs?: number): string;
|
|
25
24
|
export declare function getWorktreeMergeErrorCode(error: unknown): string | undefined;
|
|
26
|
-
export declare function checkSessionWorktreeMergeability(options: WorktreeMergeOptions): WorktreeMergeCheckResult;
|
|
27
|
-
export declare function cleanupSessionWorktree(options: WorktreeOperationOptions): boolean;
|
|
28
|
-
export declare function mergeSessionWorktree(options: WorktreeMergeOptions): WorktreeMergeResult;
|
|
29
25
|
/** Async HTTP-facing worktree check; commands remain deliberately serial. */
|
|
30
26
|
export declare function checkSessionWorktreeMergeabilityAsync(options: WorktreeMergeOptions): Promise<WorktreeMergeCheckResult>;
|
|
31
27
|
export declare function cleanupSessionWorktreeAsync(options: WorktreeOperationOptions): Promise<boolean>;
|
package/dist/git-worktree.js
CHANGED
|
@@ -39,25 +39,6 @@ function getCurrentBranch(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS)
|
|
|
39
39
|
const branch = runWorktreeGit(["branch", "--show-current"], repoRoot, timeoutMs);
|
|
40
40
|
return branch || "master";
|
|
41
41
|
}
|
|
42
|
-
function refExists(repoRoot, ref, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
|
|
43
|
-
try {
|
|
44
|
-
runWorktreeGit(["rev-parse", "--verify", ref], repoRoot, timeoutMs);
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function getRepoRootFromWorktree(worktreePath, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
|
|
52
|
-
const repoRoot = runWorktreeGit(["rev-parse", "--show-toplevel"], worktreePath, timeoutMs);
|
|
53
|
-
if (!repoRoot || !existsSync(repoRoot)) {
|
|
54
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 仓库根目录不存在。", {
|
|
55
|
-
cleanupDone: false,
|
|
56
|
-
conflict: false,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return repoRoot;
|
|
60
|
-
}
|
|
61
42
|
function ensureWorktreePath(worktree) {
|
|
62
43
|
const worktreePath = path.resolve(worktree.path);
|
|
63
44
|
if (!existsSync(worktreePath)) {
|
|
@@ -68,97 +49,6 @@ function ensureWorktreePath(worktree) {
|
|
|
68
49
|
}
|
|
69
50
|
return worktreePath;
|
|
70
51
|
}
|
|
71
|
-
function getMainRepoRoot(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
|
|
72
|
-
const commonDir = runWorktreeGit(["rev-parse", "--git-common-dir"], repoRoot, timeoutMs);
|
|
73
|
-
if (commonDir) {
|
|
74
|
-
const maybeRoot = path.resolve(repoRoot, commonDir, "..");
|
|
75
|
-
if (existsSync(maybeRoot)) {
|
|
76
|
-
return maybeRoot;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return repoRoot;
|
|
80
|
-
}
|
|
81
|
-
export function getDefaultBaseBranch(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
|
|
82
|
-
try {
|
|
83
|
-
const symbolicRef = runWorktreeGit(["symbolic-ref", "refs/remotes/origin/HEAD"], repoRoot, timeoutMs);
|
|
84
|
-
const match = symbolicRef.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
85
|
-
if (match && match[1]) {
|
|
86
|
-
return match[1];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
// ignore and fallback below
|
|
91
|
-
}
|
|
92
|
-
const candidates = ["master", "main", getCurrentBranch(repoRoot, timeoutMs)];
|
|
93
|
-
for (const candidate of candidates) {
|
|
94
|
-
if (candidate && refExists(repoRoot, candidate, timeoutMs)) {
|
|
95
|
-
return candidate;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return "master";
|
|
99
|
-
}
|
|
100
|
-
function getMainRepoContext(options) {
|
|
101
|
-
const gitTimeoutMs = resolveGitTimeout(options.gitTimeoutMs);
|
|
102
|
-
const worktreePath = ensureWorktreePath(options.worktree);
|
|
103
|
-
const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath, gitTimeoutMs);
|
|
104
|
-
const repoRoot = getMainRepoRoot(worktreeRepoRoot, gitTimeoutMs);
|
|
105
|
-
const sourceBranch = options.worktree.branch;
|
|
106
|
-
if (!refExists(repoRoot, sourceBranch, gitTimeoutMs)) {
|
|
107
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 分支不存在,可能已被手动删除。", {
|
|
108
|
-
cleanupDone: false,
|
|
109
|
-
conflict: false,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
const targetBranch = options.targetBranch?.trim() || getDefaultBaseBranch(repoRoot, gitTimeoutMs);
|
|
113
|
-
if (!refExists(repoRoot, targetBranch, gitTimeoutMs)) {
|
|
114
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.TARGET_MISSING, `目标分支不存在:${targetBranch}`, {
|
|
115
|
-
cleanupDone: false,
|
|
116
|
-
conflict: false,
|
|
117
|
-
targetBranch,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return { repoRoot, worktreePath, sourceBranch, targetBranch, gitTimeoutMs };
|
|
121
|
-
}
|
|
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;
|
|
125
|
-
}
|
|
126
|
-
function getAheadCount(repoRoot, targetBranch, sourceBranch, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
|
|
127
|
-
const count = runWorktreeGit(["rev-list", "--count", `${targetBranch}..${sourceBranch}`], repoRoot, timeoutMs);
|
|
128
|
-
return Number.parseInt(count || "0", 10) || 0;
|
|
129
|
-
}
|
|
130
|
-
function checkConflicts(repoRoot, targetBranch, sourceBranch, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS) {
|
|
131
|
-
try {
|
|
132
|
-
runWorktreeGit(["merge-tree", targetBranch, sourceBranch], repoRoot, timeoutMs);
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function ensureMergeableContext(options) {
|
|
140
|
-
const context = getMainRepoContext(options);
|
|
141
|
-
if (hasUncommittedChanges(context.worktreePath, context.gitTimeoutMs)) {
|
|
142
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.DIRTY, "Worktree 中仍有未提交改动,请先提交后再合并。", {
|
|
143
|
-
sourceBranch: context.sourceBranch,
|
|
144
|
-
targetBranch: context.targetBranch,
|
|
145
|
-
repoRoot: context.repoRoot,
|
|
146
|
-
cleanupDone: false,
|
|
147
|
-
conflict: false,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs);
|
|
151
|
-
if (aheadCount <= 0) {
|
|
152
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.NOTHING_TO_MERGE, "当前 worktree 没有可合并到主分支的新提交。", {
|
|
153
|
-
sourceBranch: context.sourceBranch,
|
|
154
|
-
targetBranch: context.targetBranch,
|
|
155
|
-
repoRoot: context.repoRoot,
|
|
156
|
-
cleanupDone: false,
|
|
157
|
-
conflict: false,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
return { ...context, aheadCount };
|
|
161
|
-
}
|
|
162
52
|
function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts) {
|
|
163
53
|
const ok = !hasDirtyChanges && aheadCount > 0 && !hasConflicts;
|
|
164
54
|
return {
|
|
@@ -180,201 +70,13 @@ function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts) {
|
|
|
180
70
|
: undefined,
|
|
181
71
|
};
|
|
182
72
|
}
|
|
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
73
|
function describeRollbackError(label, error) {
|
|
209
74
|
const detail = getGitErrorMessage(error);
|
|
210
75
|
return detail ? `${label}:${detail}` : label;
|
|
211
76
|
}
|
|
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;
|
|
288
|
-
}
|
|
289
|
-
function cleanupMergedWorktree(context) {
|
|
290
|
-
runWorktreeGit(["worktree", "remove", context.worktreePath], context.repoRoot, context.gitTimeoutMs);
|
|
291
|
-
runWorktreeGit(["branch", "-d", context.sourceBranch], context.repoRoot, context.gitTimeoutMs);
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
77
|
export function getWorktreeMergeErrorCode(error) {
|
|
295
78
|
return error instanceof WorktreeMergeError ? error.code : undefined;
|
|
296
79
|
}
|
|
297
|
-
export function checkSessionWorktreeMergeability(options) {
|
|
298
|
-
const context = getMainRepoContext(options);
|
|
299
|
-
const hasDirtyChanges = hasUncommittedChanges(context.worktreePath, context.gitTimeoutMs);
|
|
300
|
-
const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs);
|
|
301
|
-
const hasConflicts = !hasDirtyChanges && aheadCount > 0
|
|
302
|
-
? checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs)
|
|
303
|
-
: false;
|
|
304
|
-
return buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts);
|
|
305
|
-
}
|
|
306
|
-
export function cleanupSessionWorktree(options) {
|
|
307
|
-
const context = getMainRepoContext(options);
|
|
308
|
-
return cleanupMergedWorktree(context);
|
|
309
|
-
}
|
|
310
|
-
export function mergeSessionWorktree(options) {
|
|
311
|
-
const context = ensureMergeableContext(options);
|
|
312
|
-
const hasConflicts = checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch, context.gitTimeoutMs);
|
|
313
|
-
if (hasConflicts) {
|
|
314
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, "合并检测到冲突,请先手动处理。", {
|
|
315
|
-
sourceBranch: context.sourceBranch,
|
|
316
|
-
targetBranch: context.targetBranch,
|
|
317
|
-
repoRoot: context.repoRoot,
|
|
318
|
-
cleanupDone: false,
|
|
319
|
-
conflict: true,
|
|
320
|
-
});
|
|
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
|
-
}
|
|
333
|
-
try {
|
|
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);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
const message = getGitErrorMessage(error);
|
|
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}`, {
|
|
344
|
-
sourceBranch: context.sourceBranch,
|
|
345
|
-
targetBranch: context.targetBranch,
|
|
346
|
-
repoRoot: context.repoRoot,
|
|
347
|
-
cleanupDone: false,
|
|
348
|
-
conflict: true,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
const mergedAt = new Date().toISOString();
|
|
352
|
-
const mergeCommit = getHeadCommit(context.repoRoot, context.gitTimeoutMs);
|
|
353
|
-
try {
|
|
354
|
-
cleanupMergedWorktree(context);
|
|
355
|
-
return {
|
|
356
|
-
ok: true,
|
|
357
|
-
sourceBranch: context.sourceBranch,
|
|
358
|
-
targetBranch: context.targetBranch,
|
|
359
|
-
repoRoot: context.repoRoot,
|
|
360
|
-
mergeCommit,
|
|
361
|
-
mergedAt,
|
|
362
|
-
cleanupDone: true,
|
|
363
|
-
conflict: false,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CLEANUP_FAILED, getGitErrorMessage(error) || "已合并,但清理 worktree 失败。", {
|
|
368
|
-
sourceBranch: context.sourceBranch,
|
|
369
|
-
targetBranch: context.targetBranch,
|
|
370
|
-
repoRoot: context.repoRoot,
|
|
371
|
-
mergeCommit,
|
|
372
|
-
mergedAt,
|
|
373
|
-
cleanupDone: false,
|
|
374
|
-
conflict: false,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
80
|
function runWorktreeGitAsync(args, cwd, timeoutMs) {
|
|
379
81
|
return runGitAsync(args, cwd, { timeout: timeoutMs });
|
|
380
82
|
}
|
package/dist/models.d.ts
CHANGED
|
@@ -102,11 +102,6 @@ export declare class ModelCatalogService {
|
|
|
102
102
|
refresh(request?: ModelCatalogRefreshRequest): Promise<ModelCatalogRefreshResult>;
|
|
103
103
|
private performRefresh;
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Compatibility helper for callers that only need a synchronous fallback
|
|
107
|
-
* catalog. It intentionally does not own or mutate process-global state.
|
|
108
|
-
*/
|
|
109
|
-
export declare function getCachedModels(options?: ModelRefreshOptions): ModelCache;
|
|
110
105
|
/**
|
|
111
106
|
* Compatibility helper for direct callers and unit tests. Server code should
|
|
112
107
|
* use `ModelCatalogService` so the result is persisted and diffed.
|
package/dist/models.js
CHANGED
|
@@ -767,14 +767,6 @@ export class ModelCatalogService {
|
|
|
767
767
|
return { ...this.snapshot(), changed, checkedAt };
|
|
768
768
|
}
|
|
769
769
|
}
|
|
770
|
-
/**
|
|
771
|
-
* Compatibility helper for callers that only need a synchronous fallback
|
|
772
|
-
* catalog. It intentionally does not own or mutate process-global state.
|
|
773
|
-
*/
|
|
774
|
-
export function getCachedModels(options = {}) {
|
|
775
|
-
const persisted = loadPersistedModelCatalog(options.storage);
|
|
776
|
-
return cloneCache(persisted?.catalog ?? createInitialCache(options));
|
|
777
|
-
}
|
|
778
770
|
/**
|
|
779
771
|
* Compatibility helper for direct callers and unit tests. Server code should
|
|
780
772
|
* use `ModelCatalogService` so the result is persisted and diffed.
|
package/dist/resume-policy.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { SessionProvider } from "./types.js";
|
|
2
2
|
/** Claude session IDs and Codex thread IDs are UUID-shaped identifiers. */
|
|
3
3
|
export declare function isProviderSessionId(value: unknown): value is string;
|
|
4
|
-
export declare function getResumeCommandSessionId(command: string): string | null;
|
|
5
|
-
export declare function getCodexResumeCommandSessionId(command: string): string | null;
|
|
6
4
|
/**
|
|
7
5
|
* Provider-native IDs are not uniformly UUIDs: OpenCode uses `ses_*` and
|
|
8
6
|
* older Qoder releases also emitted `qs_*`. Keep the accepted alphabet shell
|
package/dist/resume-policy.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
2
2
|
const PROVIDER_SESSION_ID_PATTERN = new RegExp(`^${UUID_PATTERN}$`, "i");
|
|
3
|
-
const RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)--resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
4
|
-
const CODEX_RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
5
3
|
const SAFE_PROVIDER_SESSION_ID_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,199}$/i;
|
|
6
4
|
const SAFE_PROVIDER_SESSION_ID_SOURCE = "([a-z0-9][a-z0-9._:-]{0,199})";
|
|
7
5
|
/** Claude session IDs and Codex thread IDs are UUID-shaped identifiers. */
|
|
8
6
|
export function isProviderSessionId(value) {
|
|
9
7
|
return typeof value === "string" && PROVIDER_SESSION_ID_PATTERN.test(value);
|
|
10
8
|
}
|
|
11
|
-
export function getResumeCommandSessionId(command) {
|
|
12
|
-
const match = RESUME_COMMAND_ID_PATTERN.exec(command);
|
|
13
|
-
return match?.[1] ?? null;
|
|
14
|
-
}
|
|
15
|
-
export function getCodexResumeCommandSessionId(command) {
|
|
16
|
-
const match = CODEX_RESUME_COMMAND_ID_PATTERN.exec(command);
|
|
17
|
-
return match?.[1] ?? null;
|
|
18
|
-
}
|
|
19
9
|
/**
|
|
20
10
|
* Provider-native IDs are not uniformly UUIDs: OpenCode uses `ses_*` and
|
|
21
11
|
* older Qoder releases also emitted `qs_*`. Keep the accepted alphabet shell
|
|
@@ -59,5 +59,5 @@ export declare function buildSessionListPage(sessions: SessionSnapshot[], claude
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function markManagedProviderHistory<T extends ProviderHistorySession>(history: T[], sessions: SessionSnapshot[], provider: "claude" | "codex" | "opencode" | "qoder"): T[];
|
|
61
61
|
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
|
|
62
|
-
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager,
|
|
62
|
+
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, sessionRegistry: SessionRegistry): void;
|
|
63
63
|
export {};
|
|
@@ -1332,7 +1332,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1332
1332
|
}
|
|
1333
1333
|
});
|
|
1334
1334
|
}
|
|
1335
|
-
export function registerClaudeHistoryRoutes(app, processes,
|
|
1335
|
+
export function registerClaudeHistoryRoutes(app, processes, _structured, storage, sessionRegistry) {
|
|
1336
1336
|
app.get("/api/claude-history", (_req, res) => {
|
|
1337
1337
|
try {
|
|
1338
1338
|
const history = markManagedProviderHistory(processes.listClaudeHistorySessions(), sessionRegistry.listSlim(), "claude");
|