@co0ontty/wand 4.41.0 → 4.42.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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "86d7332e9d197173cbabe647abdbbd7c50433814",
3
- "builtAt": "2026-08-09T08:05:58.283Z",
4
- "version": "4.41.0",
2
+ "commit": "0b6a7b8718c885c69a15d7ee14bd7383b4271459",
3
+ "builtAt": "2026-08-09T12:57:20.566Z",
4
+ "version": "4.42.0",
5
5
  "channel": "stable"
6
6
  }
@@ -42,6 +42,19 @@ export declare function resolveWorktreeTargetBranchAsync(cwd: string, gitTimeout
42
42
  /** Async HTTP-facing worktree check; commands remain deliberately serial. */
43
43
  export declare function checkSessionWorktreeMergeabilityAsync(options: WorktreeMergeOptions): Promise<WorktreeMergeCheckResult>;
44
44
  export declare function cleanupSessionWorktreeAsync(options: WorktreeOperationOptions): Promise<boolean>;
45
+ /**
46
+ * Best-effort synchronous cleanup of a session/task worktree directory and its
47
+ * branch. Uses `--force` + `-D` (mirroring workspace-task deletion): deleting a
48
+ * session means discarding everything tied to it, including any uncommitted
49
+ * work in its isolated worktree. Any failure (already removed, locked, or a
50
+ * missing repo root) is swallowed so a stale or half-cleaned worktree can never
51
+ * block session deletion.
52
+ */
53
+ export declare function cleanupWorktreeSync(worktree: {
54
+ path: string;
55
+ branch: string;
56
+ repoRoot?: string;
57
+ } | null | undefined): void;
45
58
  /** Async HTTP-facing merge with serial, non-cancellable rollback. */
46
59
  export declare function mergeSessionWorktreeAsync(options: WorktreeMergeOptions): Promise<WorktreeMergeResult>;
47
60
  export declare function prepareSessionWorktree(options: WorktreeSetupOptions): WorktreeSetupResult;
@@ -330,6 +330,27 @@ export async function checkSessionWorktreeMergeabilityAsync(options) {
330
330
  export async function cleanupSessionWorktreeAsync(options) {
331
331
  return cleanupMergedWorktreeAsync(await getMainRepoContextAsync(options));
332
332
  }
333
+ /**
334
+ * Best-effort synchronous cleanup of a session/task worktree directory and its
335
+ * branch. Uses `--force` + `-D` (mirroring workspace-task deletion): deleting a
336
+ * session means discarding everything tied to it, including any uncommitted
337
+ * work in its isolated worktree. Any failure (already removed, locked, or a
338
+ * missing repo root) is swallowed so a stale or half-cleaned worktree can never
339
+ * block session deletion.
340
+ */
341
+ export function cleanupWorktreeSync(worktree) {
342
+ const repoRoot = worktree?.repoRoot;
343
+ if (!repoRoot)
344
+ return;
345
+ try {
346
+ runGit(["worktree", "remove", "--force", worktree.path], repoRoot);
347
+ }
348
+ catch { /* best effort */ }
349
+ try {
350
+ runGit(["branch", "-D", worktree.branch], repoRoot);
351
+ }
352
+ catch { /* best effort */ }
353
+ }
333
354
  /** Async HTTP-facing merge with serial, non-cancellable rollback. */
334
355
  export async function mergeSessionWorktreeAsync(options) {
335
356
  const context = await getMainRepoContextAsync(options);
@@ -4,8 +4,7 @@ import { existsSync, statSync } from "node:fs";
4
4
  import { asyncRoute } from "./express-async.js";
5
5
  import { getErrorMessage } from "./error-utils.js";
6
6
  import { expandHomePath } from "./middleware/path-safety.js";
7
- import { checkSessionWorktreeMergeabilityAsync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
8
- import { runGit } from "./git-utils.js";
7
+ import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
9
8
  const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
10
9
  function parseDefaultProvider(value) {
11
10
  return typeof value === "string" && PROVIDERS.has(value) ? value : undefined;
@@ -22,18 +21,6 @@ function resolveWorkspaceCwd(raw) {
22
21
  throw new Error(`不是目录:${resolved}`);
23
22
  return resolved;
24
23
  }
25
- function cleanupTaskWorktree(worktree) {
26
- if (!worktree?.repoRoot)
27
- return;
28
- try {
29
- runGit(["worktree", "remove", "--force", worktree.path], worktree.repoRoot);
30
- }
31
- catch { /* best effort */ }
32
- try {
33
- runGit(["branch", "-D", worktree.branch], worktree.repoRoot);
34
- }
35
- catch { /* best effort */ }
36
- }
37
24
  function deleteSessions(storage, sessions, sessionIds) {
38
25
  for (const sessionId of new Set(sessionIds)) {
39
26
  if (sessions)
@@ -252,7 +239,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
252
239
  }
253
240
  for (const task of tasks) {
254
241
  if (cascade || task.worktree)
255
- cleanupTaskWorktree(task.worktree);
242
+ cleanupWorktreeSync(task.worktree);
256
243
  }
257
244
  storage.deleteWorkspace(existing.id, { cascade });
258
245
  res.json({ ok: true });
@@ -440,7 +427,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
440
427
  }
441
428
  // 尽力清理 worktree 与分支;失败不阻塞删除任务行。
442
429
  if (cascade)
443
- cleanupTaskWorktree(existing.worktree);
430
+ cleanupWorktreeSync(existing.worktree);
444
431
  storage.deleteWorkspaceTask(existing.id, { cascade });
445
432
  res.json({ ok: true });
446
433
  });
@@ -1,3 +1,4 @@
1
+ import { cleanupWorktreeSync } from "./git-worktree.js";
1
2
  function slimSnapshot(snapshot) {
2
3
  const { output: _output, messages: _messages, ...slim } = snapshot;
3
4
  return { ...slim, output: "" };
@@ -111,6 +112,18 @@ export class SessionRegistry {
111
112
  const snapshot = this.get(id);
112
113
  if (!snapshot)
113
114
  return null;
115
+ // Best-effort: tear down the session's isolated worktree (directory +
116
+ // branch) before removing the session record. Without this, deleting a
117
+ // session that was never merged leaves an orphan under `.wand-worktrees/`
118
+ // and a dangling `wand/*` branch forever — the merge path is the only
119
+ // other place that cleans them up. Force removal is intentional: deleting
120
+ // a session means discarding everything tied to it.
121
+ if (snapshot.worktree) {
122
+ try {
123
+ cleanupWorktreeSync(snapshot.worktree);
124
+ }
125
+ catch { /* never block deletion */ }
126
+ }
114
127
  const owner = this.ownerOf(id);
115
128
  if (owner === "structured")
116
129
  this.structured.delete(id);