@co0ontty/wand 4.40.5 → 4.41.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": "f55ec99c626885ac5e3467ecd422b3414f596e4a",
3
- "builtAt": "2026-08-09T07:29:23.083Z",
4
- "version": "4.40.5",
2
+ "commit": "86d7332e9d197173cbabe647abdbbd7c50433814",
3
+ "builtAt": "2026-08-09T08:05:58.283Z",
4
+ "version": "4.41.0",
5
5
  "channel": "stable"
6
6
  }
@@ -32,7 +32,13 @@ export declare class WorktreeMergeError extends Error {
32
32
  readonly result?: Partial<WorktreeMergeResult> | undefined;
33
33
  constructor(code: string, message: string, result?: Partial<WorktreeMergeResult> | undefined);
34
34
  }
35
+ export interface WorktreeTargetBranch {
36
+ repoRoot: string;
37
+ targetBranch: string;
38
+ }
35
39
  export declare function getWorktreeMergeErrorCode(error: unknown): string | undefined;
40
+ /** Resolve one stable merge target for every worktree belonging to a project. */
41
+ export declare function resolveWorktreeTargetBranchAsync(cwd: string, gitTimeoutMs?: number): Promise<WorktreeTargetBranch>;
36
42
  /** Async HTTP-facing worktree check; commands remain deliberately serial. */
37
43
  export declare function checkSessionWorktreeMergeabilityAsync(options: WorktreeMergeOptions): Promise<WorktreeMergeCheckResult>;
38
44
  export declare function cleanupSessionWorktreeAsync(options: WorktreeOperationOptions): Promise<boolean>;
@@ -91,7 +91,7 @@ function ensureWorktreePath(worktree) {
91
91
  }
92
92
  return worktreePath;
93
93
  }
94
- function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts) {
94
+ function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts, commits) {
95
95
  const ok = !hasDirtyChanges && aheadCount > 0 && !hasConflicts;
96
96
  return {
97
97
  ok,
@@ -103,6 +103,7 @@ function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts) {
103
103
  aheadCount,
104
104
  hasConflicts,
105
105
  recommendedAction: hasConflicts ? "resolve-conflict" : aheadCount > 0 ? "merge" : "noop",
106
+ commits,
106
107
  reason: hasDirtyChanges
107
108
  ? "Worktree 中仍有未提交改动。"
108
109
  : aheadCount <= 0
@@ -163,12 +164,22 @@ async function getDefaultBaseBranchAsync(repoRoot, timeoutMs) {
163
164
  current = await runWorktreeGitAsync(["branch", "--show-current"], repoRoot, timeoutMs) || "master";
164
165
  }
165
166
  catch { /* fallback */ }
166
- for (const candidate of ["master", "main", current]) {
167
+ for (const candidate of ["main", "master", current]) {
167
168
  if (candidate && await refExistsAsync(repoRoot, candidate, timeoutMs))
168
169
  return candidate;
169
170
  }
170
171
  return "master";
171
172
  }
173
+ /** Resolve one stable merge target for every worktree belonging to a project. */
174
+ export async function resolveWorktreeTargetBranchAsync(cwd, gitTimeoutMs) {
175
+ const timeoutMs = resolveGitTimeout(gitTimeoutMs);
176
+ const checkoutRoot = await getRepoRootFromWorktreeAsync(path.resolve(cwd), timeoutMs);
177
+ const repoRoot = await getMainRepoRootAsync(checkoutRoot, timeoutMs);
178
+ return {
179
+ repoRoot,
180
+ targetBranch: await getDefaultBaseBranchAsync(repoRoot, timeoutMs),
181
+ };
182
+ }
172
183
  async function getMainRepoContextAsync(options) {
173
184
  const gitTimeoutMs = resolveGitTimeout(options.gitTimeoutMs);
174
185
  const worktreePath = ensureWorktreePath(options.worktree);
@@ -200,6 +211,24 @@ async function checkConflictsAsync(context) {
200
211
  return true;
201
212
  }
202
213
  }
214
+ async function listAheadCommitsAsync(context) {
215
+ const output = await runWorktreeGitAsync([
216
+ "log",
217
+ "--max-count=50",
218
+ "--format=%H%x1f%h%x1f%s%x1e",
219
+ `${context.targetBranch}..${context.sourceBranch}`,
220
+ ], context.repoRoot, context.gitTimeoutMs);
221
+ return output
222
+ .split("\x1e")
223
+ .map((record) => record.trim())
224
+ .filter(Boolean)
225
+ .flatMap((record) => {
226
+ const [hash = "", shortHash = "", ...subjectParts] = record.split("\x1f");
227
+ if (!hash)
228
+ return [];
229
+ return [{ hash, shortHash: shortHash || hash.slice(0, 7), subject: subjectParts.join("\x1f") }];
230
+ });
231
+ }
203
232
  async function captureCheckoutStateAsync(context) {
204
233
  let branch = null;
205
234
  try {
@@ -295,7 +324,8 @@ export async function checkSessionWorktreeMergeabilityAsync(options) {
295
324
  const dirty = (await runWorktreeGitAsync(["status", "--porcelain"], context.worktreePath, context.gitTimeoutMs)).length > 0;
296
325
  const aheadCount = Number.parseInt(await runWorktreeGitAsync(["rev-list", "--count", `${context.targetBranch}..${context.sourceBranch}`], context.repoRoot, context.gitTimeoutMs) || "0", 10) || 0;
297
326
  const conflicts = !dirty && aheadCount > 0 ? await checkConflictsAsync(context) : false;
298
- return buildCheckResult(context, aheadCount, dirty, conflicts);
327
+ const commits = aheadCount > 0 ? await listAheadCommitsAsync(context) : [];
328
+ return buildCheckResult(context, aheadCount, dirty, conflicts, commits);
299
329
  }
300
330
  export async function cleanupSessionWorktreeAsync(options) {
301
331
  return cleanupMergedWorktreeAsync(await getMainRepoContextAsync(options));
@@ -4,7 +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 { prepareSessionWorktree } from "./git-worktree.js";
7
+ import { checkSessionWorktreeMergeabilityAsync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
8
8
  import { runGit } from "./git-utils.js";
9
9
  const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
10
10
  function parseDefaultProvider(value) {
@@ -42,6 +42,12 @@ function deleteSessions(storage, sessions, sessionIds) {
42
42
  storage.deleteSession(sessionId);
43
43
  }
44
44
  }
45
+ function workspaceWithWorktreeCount(storage, workspace) {
46
+ const worktreeCount = storage.listWorkspaceTasks(workspace.id)
47
+ .filter((task) => task.worktree !== null)
48
+ .length;
49
+ return { ...workspace, worktreeCount };
50
+ }
45
51
  // ── Layout validation / sanitization(前端 PUT 与测试共用)──
46
52
  function sanitizePaneTab(value) {
47
53
  if (!value || typeof value !== "object")
@@ -155,7 +161,7 @@ export function sanitizeTaskLayout(value) {
155
161
  export function registerWorkspaceRoutes(app, storage, sessions) {
156
162
  // 列出所有项目(按最近打开排序)
157
163
  app.get("/api/workspaces", (_req, res) => {
158
- res.json(storage.listWorkspaces());
164
+ res.json(storage.listWorkspaces().map((workspace) => workspaceWithWorktreeCount(storage, workspace)));
159
165
  });
160
166
  // 新建项目:名称 + 目录 + 默认 IDE,不启动会话
161
167
  app.post("/api/workspaces", asyncRoute(async (req, res) => {
@@ -175,7 +181,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
175
181
  }
176
182
  const defaultProvider = parseDefaultProvider(body.defaultProvider);
177
183
  const workspace = storage.createWorkspace({ name, cwd, defaultProvider });
178
- res.status(201).json(workspace);
184
+ res.status(201).json({ ...workspace, worktreeCount: 0 });
179
185
  }));
180
186
  // 项目详情:meta + 会话 + 布局;访问即更新 lastOpenedAt
181
187
  app.get("/api/workspaces/:id", (req, res) => {
@@ -185,7 +191,10 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
185
191
  return;
186
192
  }
187
193
  storage.touchWorkspace(workspace.id);
188
- res.json({ ...workspace, sessions: storage.listSessionsByWorkspace(workspace.id) });
194
+ res.json({
195
+ ...workspaceWithWorktreeCount(storage, workspace),
196
+ sessions: storage.listSessionsByWorkspace(workspace.id),
197
+ });
189
198
  });
190
199
  // 改名 / 目录 / 默认 IDE
191
200
  app.patch("/api/workspaces/:id", (req, res) => {
@@ -216,7 +225,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
216
225
  patch.defaultProvider = parsed;
217
226
  }
218
227
  storage.updateWorkspace(existing.id, patch);
219
- res.json(storage.getWorkspace(existing.id));
228
+ const updated = storage.getWorkspace(existing.id);
229
+ res.json(updated ? workspaceWithWorktreeCount(storage, updated) : null);
220
230
  });
221
231
  // 删除项目;cascade=true 连带删会话,否则仅解绑
222
232
  app.delete("/api/workspaces/:id", (req, res) => {
@@ -269,6 +279,82 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
269
279
  }
270
280
  res.json(storage.listWorkspaceTasks(workspace.id));
271
281
  });
282
+ // 项目级 Worktree 总览:一次解析默认目标分支,再为每个任务读取可合并状态。
283
+ app.get("/api/workspaces/:id/worktrees", asyncRoute(async (req, res) => {
284
+ const workspace = storage.getWorkspace(req.params.id);
285
+ if (!workspace) {
286
+ res.status(404).json({ error: "未找到该项目。" });
287
+ return;
288
+ }
289
+ const tasks = storage.listWorkspaceTasks(workspace.id).filter((task) => task.worktree !== null);
290
+ if (tasks.length === 0) {
291
+ res.json({ workspaceId: workspace.id, repoRoot: workspace.cwd, targetBranch: "", worktrees: [] });
292
+ return;
293
+ }
294
+ let target;
295
+ try {
296
+ target = await resolveWorktreeTargetBranchAsync(workspace.cwd);
297
+ }
298
+ catch (error) {
299
+ res.status(400).json({ error: getErrorMessage(error, "无法识别项目默认分支。") });
300
+ return;
301
+ }
302
+ const worktrees = await Promise.all(tasks.map(async (task) => {
303
+ const worktree = task.worktree;
304
+ try {
305
+ const inspection = await checkSessionWorktreeMergeabilityAsync({
306
+ worktree,
307
+ targetBranch: target.targetBranch,
308
+ });
309
+ const actionable = inspection.hasUncommittedChanges || inspection.aheadCount > 0;
310
+ const state = inspection.hasUncommittedChanges
311
+ ? "dirty"
312
+ : inspection.hasConflicts
313
+ ? "conflict"
314
+ : inspection.aheadCount > 0
315
+ ? "ready"
316
+ : "empty";
317
+ return {
318
+ taskId: task.id,
319
+ taskName: task.name,
320
+ taskStatus: task.status,
321
+ branch: worktree.branch,
322
+ path: worktree.path,
323
+ baseRef: worktree.baseRef ?? "",
324
+ state,
325
+ actionable,
326
+ reason: inspection.reason ?? "",
327
+ aheadCount: inspection.aheadCount,
328
+ hasUncommittedChanges: inspection.hasUncommittedChanges,
329
+ hasConflicts: inspection.hasConflicts,
330
+ commits: inspection.commits,
331
+ };
332
+ }
333
+ catch (error) {
334
+ return {
335
+ taskId: task.id,
336
+ taskName: task.name,
337
+ taskStatus: task.status,
338
+ branch: worktree.branch,
339
+ path: worktree.path,
340
+ baseRef: worktree.baseRef ?? "",
341
+ state: "unavailable",
342
+ actionable: false,
343
+ reason: getErrorMessage(error, "无法读取 Worktree 状态。"),
344
+ aheadCount: 0,
345
+ hasUncommittedChanges: false,
346
+ hasConflicts: false,
347
+ commits: [],
348
+ };
349
+ }
350
+ }));
351
+ res.json({
352
+ workspaceId: workspace.id,
353
+ repoRoot: target.repoRoot,
354
+ targetBranch: target.targetBranch,
355
+ worktrees,
356
+ });
357
+ }));
272
358
  // 新建任务:命名 + 创建独立 worktree(非 git 仓库时退化为直接用项目目录)
273
359
  app.post("/api/workspaces/:id/tasks", asyncRoute(async (req, res) => {
274
360
  const workspace = storage.getWorkspace(req.params.id);
package/dist/types.d.ts CHANGED
@@ -205,6 +205,11 @@ export interface WorktreeMergeInfo {
205
205
  lastError?: string;
206
206
  conflict?: boolean;
207
207
  }
208
+ export interface WorktreeMergeCommit {
209
+ hash: string;
210
+ shortHash: string;
211
+ subject: string;
212
+ }
208
213
  export interface WorktreeMergeCheckResult {
209
214
  ok: boolean;
210
215
  sourceBranch: string;
@@ -216,6 +221,8 @@ export interface WorktreeMergeCheckResult {
216
221
  hasConflicts: boolean;
217
222
  recommendedAction: "merge" | "noop" | "resolve-conflict";
218
223
  reason?: string;
224
+ /** Newest-first commit preview for project-level worktree review. */
225
+ commits: WorktreeMergeCommit[];
219
226
  }
220
227
  export interface WorktreeMergeResult {
221
228
  ok: boolean;