@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.
- package/dist/build-info.json +3 -3
- package/dist/git-worktree.d.ts +6 -0
- package/dist/git-worktree.js +33 -3
- package/dist/server-workspace-routes.js +91 -5
- package/dist/types.d.ts +7 -0
- package/dist/web-ui/content/scripts.js +70 -69
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "86d7332e9d197173cbabe647abdbbd7c50433814",
|
|
3
|
+
"builtAt": "2026-08-09T08:05:58.283Z",
|
|
4
|
+
"version": "4.41.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/git-worktree.d.ts
CHANGED
|
@@ -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>;
|
package/dist/git-worktree.js
CHANGED
|
@@ -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 ["
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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;
|