@cerbur/clutch-dsh-worktree 0.1.3 → 0.1.4
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/README.md +247 -107
- package/README.zh.md +297 -0
- package/lib/client/WorktreeSurface.d.ts +2 -30
- package/lib/client/WorktreeSurface.d.ts.map +1 -1
- package/lib/client/WorktreeSurface.js +31 -318
- package/lib/client/WorktreeSurface.js.map +1 -1
- package/lib/client/worktree-surface-dialogs.d.ts +7 -0
- package/lib/client/worktree-surface-dialogs.d.ts.map +1 -0
- package/lib/client/worktree-surface-dialogs.js +86 -0
- package/lib/client/worktree-surface-dialogs.js.map +1 -0
- package/lib/client/worktree-surface-rows.d.ts +9 -0
- package/lib/client/worktree-surface-rows.d.ts.map +1 -0
- package/lib/client/worktree-surface-rows.js +223 -0
- package/lib/client/worktree-surface-rows.js.map +1 -0
- package/lib/client/worktree-surface-selectors.d.ts +9 -0
- package/lib/client/worktree-surface-selectors.d.ts.map +1 -0
- package/lib/client/worktree-surface-selectors.js +28 -0
- package/lib/client/worktree-surface-selectors.js.map +1 -0
- package/lib/client/worktree-surface-types.d.ts +233 -0
- package/lib/client/worktree-surface-types.d.ts.map +1 -0
- package/lib/client/worktree-surface-types.js +2 -0
- package/lib/client/worktree-surface-types.js.map +1 -0
- package/lib/client/worktree-view-actions.d.ts +34 -0
- package/lib/client/worktree-view-actions.d.ts.map +1 -0
- package/lib/client/worktree-view-actions.js +69 -0
- package/lib/client/worktree-view-actions.js.map +1 -0
- package/lib/client/worktree-view-errors.d.ts +20 -0
- package/lib/client/worktree-view-errors.d.ts.map +1 -0
- package/lib/client/worktree-view-errors.js +65 -0
- package/lib/client/worktree-view-errors.js.map +1 -0
- package/lib/client/worktree-view-read.d.ts +48 -0
- package/lib/client/worktree-view-read.d.ts.map +1 -0
- package/lib/client/worktree-view-read.js +134 -0
- package/lib/client/worktree-view-read.js.map +1 -0
- package/lib/client/worktree-view.d.ts +4 -97
- package/lib/client/worktree-view.d.ts.map +1 -1
- package/lib/client/worktree-view.js +4 -263
- package/lib/client/worktree-view.js.map +1 -1
- package/lib/client.js +404 -324
- package/lib/client.js.map +1 -1
- package/lib/manage/manager-context.d.ts +11 -0
- package/lib/manage/manager-context.d.ts.map +1 -0
- package/lib/manage/manager-context.js +2 -0
- package/lib/manage/manager-context.js.map +1 -0
- package/lib/manage/manager-sessions.d.ts +18 -0
- package/lib/manage/manager-sessions.d.ts.map +1 -0
- package/lib/manage/manager-sessions.js +91 -0
- package/lib/manage/manager-sessions.js.map +1 -0
- package/lib/manage/manager-support.d.ts +18 -0
- package/lib/manage/manager-support.d.ts.map +1 -0
- package/lib/manage/manager-support.js +174 -0
- package/lib/manage/manager-support.js.map +1 -0
- package/lib/manage/manager-worktrees.d.ts +27 -0
- package/lib/manage/manager-worktrees.d.ts.map +1 -0
- package/lib/manage/manager-worktrees.js +259 -0
- package/lib/manage/manager-worktrees.js.map +1 -0
- package/lib/manage/manager.d.ts +23 -65
- package/lib/manage/manager.d.ts.map +1 -1
- package/lib/manage/manager.js +32 -565
- package/lib/manage/manager.js.map +1 -1
- package/lib/provider/sidecar-persistence.d.ts +18 -0
- package/lib/provider/sidecar-persistence.d.ts.map +1 -0
- package/lib/provider/sidecar-persistence.js +162 -0
- package/lib/provider/sidecar-persistence.js.map +1 -0
- package/lib/provider/sidecar-schema.d.ts +6 -0
- package/lib/provider/sidecar-schema.d.ts.map +1 -0
- package/lib/provider/sidecar-schema.js +123 -0
- package/lib/provider/sidecar-schema.js.map +1 -0
- package/lib/provider/sidecar.d.ts +10 -67
- package/lib/provider/sidecar.d.ts.map +1 -1
- package/lib/provider/sidecar.js +21 -346
- package/lib/provider/sidecar.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorktreeManagerOptions } from './types.js';
|
|
2
|
+
import type { GitWorktreeAdapter, SidecarStore } from '../provider/types.js';
|
|
3
|
+
/** Internal Manage composition context shared by the responsibility-specific use-case modules. */
|
|
4
|
+
export interface WorktreeManagerContext {
|
|
5
|
+
readonly dsh: WorktreeManagerOptions['dsh'];
|
|
6
|
+
readonly dshHome: string;
|
|
7
|
+
readonly git: GitWorktreeAdapter;
|
|
8
|
+
readonly sidecar: SidecarStore;
|
|
9
|
+
readonly idFactory: () => string;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=manager-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-context.d.ts","sourceRoot":"","sources":["../../src/manage/manager-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE7E,kGAAkG;AAClG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,GAAG,EAAE,sBAAsB,CAAC,KAAK,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,kBAAkB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,MAAM,CAAC;CAClC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-context.js","sourceRoot":"","sources":["../../src/manage/manager-context.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SessionBinding, WorkspaceId } from '../contract/index.js';
|
|
2
|
+
import type { WorktreeManagerContext } from './manager-context.js';
|
|
3
|
+
/** Return active/detached Session relations for one Workspace. */
|
|
4
|
+
export declare function listBindings(context: WorktreeManagerContext, input: {
|
|
5
|
+
readonly workspaceId: WorkspaceId;
|
|
6
|
+
}): Promise<readonly SessionBinding[]>;
|
|
7
|
+
/** Bind a DSH-created Session whose cwd already targets the Worktree. */
|
|
8
|
+
export declare function bindSession(context: WorktreeManagerContext, input: {
|
|
9
|
+
readonly workspaceId: WorkspaceId;
|
|
10
|
+
readonly worktreeId: string;
|
|
11
|
+
readonly sessionId: string;
|
|
12
|
+
}): Promise<SessionBinding>;
|
|
13
|
+
/** Derive one execution cwd from the current Workspace and binding projection. */
|
|
14
|
+
export declare function resolveRuntimeCwd(context: WorktreeManagerContext, input: {
|
|
15
|
+
readonly workspaceId: WorkspaceId;
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
}): Promise<string>;
|
|
18
|
+
//# sourceMappingURL=manager-sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-sessions.d.ts","sourceRoot":"","sources":["../../src/manage/manager-sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,kEAAkE;AAClE,wBAAsB,YAAY,CAChC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE,GAC3C,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CAGpC;AAED,yEAAyE;AACzE,wBAAsB,WAAW,CAC/B,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpG,OAAO,CAAC,cAAc,CAAC,CAoDzB;AAED,kFAAkF;AAClF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvE,OAAO,CAAC,MAAM,CAAC,CAgCjB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { providerError } from '../provider/types.js';
|
|
2
|
+
import { assertSessionMatchesWorkspace, isDirectory, requireWorkspace } from './manager-support.js';
|
|
3
|
+
/** Return active/detached Session relations for one Workspace. */
|
|
4
|
+
export async function listBindings(context, input) {
|
|
5
|
+
await requireWorkspace(context, input.workspaceId);
|
|
6
|
+
return (await context.sidecar.read(input.workspaceId)).bindings;
|
|
7
|
+
}
|
|
8
|
+
/** Bind a DSH-created Session whose cwd already targets the Worktree. */
|
|
9
|
+
export async function bindSession(context, input) {
|
|
10
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
11
|
+
// Duplicate checks, DSH fact validation, and append happen in one serialized mutation.
|
|
12
|
+
return context.sidecar.mutate(input.workspaceId, async (snapshot) => {
|
|
13
|
+
const worktree = snapshot.worktrees.find((candidate) => candidate.worktreeId === input.worktreeId);
|
|
14
|
+
if (!worktree) {
|
|
15
|
+
throw providerError('WORKTREE_NOT_FOUND', `Worktree not found: ${input.worktreeId}`, {
|
|
16
|
+
workspaceId: input.workspaceId,
|
|
17
|
+
worktreeId: input.worktreeId,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (worktree.status === 'removed') {
|
|
21
|
+
throw providerError('WORKTREE_REMOVED', `Worktree has been removed: ${input.worktreeId}`, {
|
|
22
|
+
workspaceId: input.workspaceId,
|
|
23
|
+
worktreeId: input.worktreeId,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const existing = snapshot.bindings.find((binding) => binding.sessionId === input.sessionId);
|
|
27
|
+
if (existing) {
|
|
28
|
+
if (existing.worktreeId === input.worktreeId && existing.status === 'active') {
|
|
29
|
+
// Exact retries do not write; detached history is never implicitly reactivated.
|
|
30
|
+
return { result: existing, snapshot, changed: false };
|
|
31
|
+
}
|
|
32
|
+
throw providerError('SESSION_ALREADY_BOUND', `Session is already bound to Worktree ${existing.worktreeId}`, {
|
|
33
|
+
sessionId: input.sessionId,
|
|
34
|
+
worktreeId: existing.worktreeId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Session identity, Workspace ownership, and cwd come from DSH; validation never writes corrections back.
|
|
38
|
+
const session = await context.dsh.getSession(input.sessionId);
|
|
39
|
+
if (!session) {
|
|
40
|
+
throw providerError('SESSION_NOT_FOUND', `Session not found: ${input.sessionId}`, {
|
|
41
|
+
sessionId: input.sessionId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
assertSessionMatchesWorkspace(session, workspace, worktree);
|
|
45
|
+
const binding = {
|
|
46
|
+
workspaceId: input.workspaceId,
|
|
47
|
+
worktreeId: input.worktreeId,
|
|
48
|
+
sessionId: input.sessionId,
|
|
49
|
+
status: 'active',
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
result: binding,
|
|
53
|
+
// Worktree Session groups render bindings in sidecar order; prepend the newly created Session.
|
|
54
|
+
snapshot: { ...snapshot, bindings: [binding, ...snapshot.bindings] },
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Derive one execution cwd from the current Workspace and binding projection. */
|
|
59
|
+
export async function resolveRuntimeCwd(context, input) {
|
|
60
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
61
|
+
const snapshot = await context.sidecar.read(input.workspaceId);
|
|
62
|
+
const binding = snapshot.bindings.find((candidate) => candidate.sessionId === input.sessionId);
|
|
63
|
+
// Detached is a safe main fallback; a broken active relation fails explicitly below.
|
|
64
|
+
if (!binding || binding.status === 'detached')
|
|
65
|
+
return workspace.rootPath;
|
|
66
|
+
const worktree = snapshot.worktrees.find((candidate) => candidate.worktreeId === binding.worktreeId);
|
|
67
|
+
if (!worktree) {
|
|
68
|
+
throw providerError('WORKTREE_NOT_FOUND', `Active binding points to a missing Worktree: ${binding.worktreeId}`, {
|
|
69
|
+
workspaceId: input.workspaceId,
|
|
70
|
+
worktreeId: binding.worktreeId,
|
|
71
|
+
sessionId: input.sessionId,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (worktree.status !== 'active') {
|
|
75
|
+
throw providerError('WORKTREE_REMOVED', `Active binding points to a removed Worktree: ${worktree.worktreeId}`, {
|
|
76
|
+
workspaceId: input.workspaceId,
|
|
77
|
+
worktreeId: worktree.worktreeId,
|
|
78
|
+
sessionId: input.sessionId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!(await isDirectory(worktree.absolutePath))) {
|
|
82
|
+
throw providerError('WORKTREE_NOT_FOUND', `Active Worktree path does not exist: ${worktree.absolutePath}`, {
|
|
83
|
+
workspaceId: input.workspaceId,
|
|
84
|
+
worktreeId: worktree.worktreeId,
|
|
85
|
+
sessionId: input.sessionId,
|
|
86
|
+
absolutePath: worktree.absolutePath,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return worktree.absolutePath;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=manager-sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-sessions.js","sourceRoot":"","sources":["../../src/manage/manager-sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorktreeRecord } from '../contract/index.js';
|
|
2
|
+
import { type DshSessionSummary, type DshWorkspaceSummary, type SidecarSnapshot, WorktreeProviderError } from '../provider/types.js';
|
|
3
|
+
import type { WorktreeManagerContext } from './manager-context.js';
|
|
4
|
+
export declare function isSameOrInside(parent: string, candidate: string): boolean;
|
|
5
|
+
export declare function isDirectory(filePath: string): Promise<boolean>;
|
|
6
|
+
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
7
|
+
export declare function samePhysicalPath(left: string, right: string): Promise<boolean>;
|
|
8
|
+
export declare function rejectSymlink(filePath: string, label: string): Promise<void>;
|
|
9
|
+
export declare function asGitError(operation: string, workspaceRoot: string, targetPath: string | undefined, error: unknown): WorktreeProviderError;
|
|
10
|
+
export declare function asSidecarError(error: unknown, workspaceId: string): WorktreeProviderError;
|
|
11
|
+
export declare function describeError(error: unknown): string;
|
|
12
|
+
export declare function generatedId(idFactory: () => string): string;
|
|
13
|
+
export declare function requireWorkspace(context: WorktreeManagerContext, workspaceId: string): Promise<DshWorkspaceSummary>;
|
|
14
|
+
export declare function validateGeneratedPath(context: WorktreeManagerContext, workspaceRoot: string, targetPath: string, worktreeId: string): void;
|
|
15
|
+
export declare function validatePhysicalGeneratedPath(context: WorktreeManagerContext, workspaceRoot: string, targetPath: string): Promise<void>;
|
|
16
|
+
export declare function assertSessionMatchesWorkspace(session: DshSessionSummary, workspace: DshWorkspaceSummary, worktree: WorktreeRecord): void;
|
|
17
|
+
export type { SidecarSnapshot };
|
|
18
|
+
//# sourceMappingURL=manager-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-support.d.ts","sourceRoot":"","sources":["../../src/manage/manager-support.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,qBAAqB,EAGtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAInE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAGzE;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOpE;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnE;AAID,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMpF;AAID,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAelF;AAED,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,KAAK,EAAE,OAAO,GACb,qBAAqB,CAOvB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,qBAAqB,CAMzF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,MAAM,GAAG,MAAM,CAM3D;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAyB9B;AAID,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,EAC/B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,IAAI,CASN;AAID,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,sBAAsB,EAC/B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAwBf;AAID,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,SAAS,EAAE,mBAAmB,EAC9B,QAAQ,EAAE,cAAc,GACvB,IAAI,CAkBN;AAED,YAAY,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { lstat, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isWorktreeProviderError, providerError, } from '../provider/types.js';
|
|
4
|
+
// 这是词法边界检查,既接受 parent 本身也接受其后代;物理路径边界会在后续单独校验。
|
|
5
|
+
// This is a lexical boundary check that accepts parent itself and descendants; physical boundaries are validated separately later.
|
|
6
|
+
export function isSameOrInside(parent, candidate) {
|
|
7
|
+
const relative = path.relative(parent, candidate);
|
|
8
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
9
|
+
}
|
|
10
|
+
export async function isDirectory(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
return (await stat(filePath)).isDirectory();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT')
|
|
16
|
+
return false;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function pathExists(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
await stat(filePath);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT')
|
|
27
|
+
return false;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 优先比较 canonical path 以覆盖符号链接/路径别名;无法 canonicalize 时退回绝对词法比较。
|
|
32
|
+
// Prefer canonical paths to cover symlinks and aliases; fall back to absolute lexical comparison when canonicalization is unavailable.
|
|
33
|
+
export async function samePhysicalPath(left, right) {
|
|
34
|
+
try {
|
|
35
|
+
return (await realpath(left)) === (await realpath(right));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return path.resolve(left) === path.resolve(right);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 已存在的受信边界不允许是 symlink,避免后续创建绕过 DSH Home 的物理目录约束。
|
|
42
|
+
// Existing trusted boundaries may not be symlinks, preventing later creation from escaping the physical DSH Home boundary.
|
|
43
|
+
export async function rejectSymlink(filePath, label) {
|
|
44
|
+
try {
|
|
45
|
+
if ((await lstat(filePath)).isSymbolicLink()) {
|
|
46
|
+
throw providerError('GIT_OPERATION_FAILED', `${label} must not be a symlink: ${filePath}`, {
|
|
47
|
+
path: filePath,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === 'ENOENT')
|
|
53
|
+
return;
|
|
54
|
+
if (isWorktreeProviderError(error))
|
|
55
|
+
throw error;
|
|
56
|
+
throw providerError('GIT_OPERATION_FAILED', `Unable to inspect ${label}: ${filePath}`, {
|
|
57
|
+
path: filePath,
|
|
58
|
+
cause: String(error),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function asGitError(operation, workspaceRoot, targetPath, error) {
|
|
63
|
+
if (isWorktreeProviderError(error))
|
|
64
|
+
return error;
|
|
65
|
+
return providerError('GIT_OPERATION_FAILED', `Git ${operation} failed: ${String(error)}`, {
|
|
66
|
+
workspaceRoot,
|
|
67
|
+
...(targetPath ? { targetPath } : {}),
|
|
68
|
+
operation,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function asSidecarError(error, workspaceId) {
|
|
72
|
+
if (isWorktreeProviderError(error))
|
|
73
|
+
return error;
|
|
74
|
+
return providerError('SIDECAR_UNAVAILABLE', `Sidecar operation failed for Workspace ${workspaceId}`, {
|
|
75
|
+
workspaceId,
|
|
76
|
+
cause: String(error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export function describeError(error) {
|
|
80
|
+
return error instanceof Error ? error.message : String(error);
|
|
81
|
+
}
|
|
82
|
+
export function generatedId(idFactory) {
|
|
83
|
+
const worktreeId = idFactory();
|
|
84
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(worktreeId)) {
|
|
85
|
+
throw providerError('GIT_OPERATION_FAILED', 'Provider generated an invalid Worktree ID', { worktreeId });
|
|
86
|
+
}
|
|
87
|
+
return worktreeId;
|
|
88
|
+
}
|
|
89
|
+
export async function requireWorkspace(context, workspaceId) {
|
|
90
|
+
const workspace = await context.dsh.getWorkspace(workspaceId);
|
|
91
|
+
if (!workspace || workspace.workspaceId !== workspaceId || !path.isAbsolute(workspace.rootPath)) {
|
|
92
|
+
throw providerError('WORKSPACE_NOT_FOUND', `Workspace is missing or has a non-absolute root: ${workspaceId}`, {
|
|
93
|
+
workspaceId,
|
|
94
|
+
rootPath: workspace?.rootPath ?? '',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const rootPath = path.resolve(workspace.rootPath);
|
|
98
|
+
if (!(await isDirectory(rootPath))) {
|
|
99
|
+
throw providerError('WORKSPACE_NOT_FOUND', `Workspace root does not exist: ${rootPath}`, {
|
|
100
|
+
workspaceId,
|
|
101
|
+
rootPath,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await realpath(rootPath);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw providerError('WORKSPACE_NOT_FOUND', `Unable to resolve Workspace root: ${rootPath}`, {
|
|
109
|
+
workspaceId,
|
|
110
|
+
rootPath,
|
|
111
|
+
cause: String(error),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return { ...workspace, rootPath };
|
|
115
|
+
}
|
|
116
|
+
// 第一层使用未解析路径验证目标属于插件根且不位于 Workspace 内。
|
|
117
|
+
// The first layer uses unresolved paths to ensure the target belongs to the plugin root and is not inside the Workspace.
|
|
118
|
+
export function validateGeneratedPath(context, workspaceRoot, targetPath, worktreeId) {
|
|
119
|
+
const pluginRoot = path.resolve(context.dshHome, 'clutch-dsh-worktree');
|
|
120
|
+
if (!isSameOrInside(pluginRoot, targetPath) || isSameOrInside(workspaceRoot, targetPath)) {
|
|
121
|
+
throw providerError('GIT_OPERATION_FAILED', 'Generated Worktree path is outside the allowed boundary', {
|
|
122
|
+
workspaceRoot,
|
|
123
|
+
targetPath,
|
|
124
|
+
worktreeId,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 第二层拒绝受信祖先 symlink,并以 realpath 后的根重新计算目标,防止物理路径逃逸。
|
|
129
|
+
// The second layer rejects symlinked trusted ancestors and recomputes the target from the realpath root to prevent physical path escape.
|
|
130
|
+
export async function validatePhysicalGeneratedPath(context, workspaceRoot, targetPath) {
|
|
131
|
+
await rejectSymlink(context.dshHome, 'DSH Home');
|
|
132
|
+
await rejectSymlink(path.join(context.dshHome, 'clutch-dsh-worktree'), 'plugin sidecar root');
|
|
133
|
+
await rejectSymlink(path.join(context.dshHome, 'clutch-dsh-worktree', 'worktree'), 'Worktree root');
|
|
134
|
+
let canonicalDshHome;
|
|
135
|
+
try {
|
|
136
|
+
canonicalDshHome = await realpath(context.dshHome);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw providerError('SIDECAR_UNAVAILABLE', `Unable to resolve DSH Home: ${context.dshHome}`, {
|
|
140
|
+
dshHome: context.dshHome,
|
|
141
|
+
cause: String(error),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const targetRelativeToDshHome = path.relative(context.dshHome, targetPath);
|
|
145
|
+
const canonicalTarget = path.resolve(canonicalDshHome, targetRelativeToDshHome);
|
|
146
|
+
const canonicalWorkspace = await realpath(workspaceRoot);
|
|
147
|
+
if (!isSameOrInside(canonicalDshHome, canonicalTarget) || isSameOrInside(canonicalWorkspace, canonicalTarget)) {
|
|
148
|
+
throw providerError('GIT_OPERATION_FAILED', 'Generated Worktree path crosses a physical boundary', {
|
|
149
|
+
workspaceRoot: canonicalWorkspace,
|
|
150
|
+
dshHome: canonicalDshHome,
|
|
151
|
+
targetPath: canonicalTarget,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 绑定只接受 DSH 已按目标 Worktree cwd 创建的 Session;Manager 不替用户迁移或改写 Session。
|
|
156
|
+
// Binding accepts only Sessions already created by DSH with the target Worktree cwd; the Manager never migrates or rewrites a Session.
|
|
157
|
+
export function assertSessionMatchesWorkspace(session, workspace, worktree) {
|
|
158
|
+
if ((session.workspaceId !== undefined && session.workspaceId !== workspace.workspaceId) ||
|
|
159
|
+
(session.projectId !== undefined &&
|
|
160
|
+
workspace.projectId !== undefined &&
|
|
161
|
+
session.projectId !== workspace.projectId) ||
|
|
162
|
+
!path.isAbsolute(session.cwd) ||
|
|
163
|
+
path.resolve(session.cwd) !== path.resolve(worktree.absolutePath)) {
|
|
164
|
+
throw providerError('SESSION_CWD_MISMATCH', `Session cwd or Workspace association does not match Worktree`, {
|
|
165
|
+
sessionId: session.sessionId,
|
|
166
|
+
workspaceId: workspace.workspaceId,
|
|
167
|
+
expectedCwd: worktree.absolutePath,
|
|
168
|
+
actualCwd: session.cwd,
|
|
169
|
+
sessionWorkspaceId: session.workspaceId ?? '',
|
|
170
|
+
sessionProjectId: session.projectId ?? '',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=manager-support.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-support.js","sourceRoot":"","sources":["../../src/manage/manager-support.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BranchRecord, WorktreeId, WorktreeRecord, WorkspaceId } from '../contract/index.js';
|
|
2
|
+
import type { WorktreeManagerContext } from './manager-context.js';
|
|
3
|
+
/** Return the sidecar Worktree projection with runtime Git health attached. */
|
|
4
|
+
export declare function listWorktrees(context: WorktreeManagerContext, input: {
|
|
5
|
+
readonly workspaceId: WorkspaceId;
|
|
6
|
+
}): Promise<readonly WorktreeRecord[]>;
|
|
7
|
+
/** Project local branches against all Git worktrees and mark checkout/current state. */
|
|
8
|
+
export declare function listBranches(context: WorktreeManagerContext, input: {
|
|
9
|
+
readonly workspaceId: WorkspaceId;
|
|
10
|
+
}): Promise<readonly BranchRecord[]>;
|
|
11
|
+
/** Create a Git Worktree first, then commit its external relation to the sidecar. */
|
|
12
|
+
export declare function createWorktree(context: WorktreeManagerContext, input: {
|
|
13
|
+
readonly workspaceId: WorkspaceId;
|
|
14
|
+
readonly branch: string;
|
|
15
|
+
readonly newBranch?: string;
|
|
16
|
+
}): Promise<WorktreeRecord>;
|
|
17
|
+
/** Remove Git state first, then mark the relation removed while preserving detached bindings. */
|
|
18
|
+
export declare function removeWorktree(context: WorktreeManagerContext, input: {
|
|
19
|
+
readonly workspaceId: WorkspaceId;
|
|
20
|
+
readonly worktreeId: string;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
export declare function insertWorktreeBefore(context: WorktreeManagerContext, input: {
|
|
23
|
+
readonly workspaceId: WorkspaceId;
|
|
24
|
+
readonly worktreeId: WorktreeId;
|
|
25
|
+
readonly beforeWorktreeId?: WorktreeId;
|
|
26
|
+
}): Promise<readonly WorktreeId[]>;
|
|
27
|
+
//# sourceMappingURL=manager-worktrees.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager-worktrees.d.ts","sourceRoot":"","sources":["../../src/manage/manager-worktrees.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAElG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAanE,+EAA+E;AAC/E,wBAAsB,aAAa,CACjC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE,GAC3C,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CAoCpC;AAED,wFAAwF;AACxF,wBAAsB,YAAY,CAChC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE,GAC3C,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAsBlC;AAED,qFAAqF;AACrF,wBAAsB,cAAc,CAClC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACjG,OAAO,CAAC,cAAc,CAAC,CAqIzB;AAED,iGAAiG;AACjG,wBAAsB,cAAc,CAClC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACxE,OAAO,CAAC,IAAI,CAAC,CAuEf;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,UAAU,CAAA;CAAE,GACpH,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAWhC"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { providerError } from '../provider/types.js';
|
|
3
|
+
import { asGitError, asSidecarError, describeError, generatedId, pathExists, requireWorkspace, samePhysicalPath, validateGeneratedPath, validatePhysicalGeneratedPath, } from './manager-support.js';
|
|
4
|
+
/** Return the sidecar Worktree projection with runtime Git health attached. */
|
|
5
|
+
export async function listWorktrees(context, input) {
|
|
6
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
7
|
+
const records = (await context.sidecar.read(input.workspaceId)).worktrees;
|
|
8
|
+
let gitWorktrees;
|
|
9
|
+
try {
|
|
10
|
+
gitWorktrees = await context.git.listWorktrees(workspace.rootPath);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return records.map((record) => {
|
|
14
|
+
const { health: _health, ...durableRecord } = record;
|
|
15
|
+
void _health;
|
|
16
|
+
return record.status === 'active'
|
|
17
|
+
? { ...durableRecord, health: 'repair' }
|
|
18
|
+
: durableRecord;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const nextRecords = [];
|
|
22
|
+
for (const record of records) {
|
|
23
|
+
const { health: _health, ...durableRecord } = record;
|
|
24
|
+
void _health;
|
|
25
|
+
if (record.status !== 'active') {
|
|
26
|
+
nextRecords.push(durableRecord);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let ready = false;
|
|
30
|
+
for (const gitWorktree of gitWorktrees) {
|
|
31
|
+
if (path.resolve(gitWorktree.absolutePath) === path.resolve(record.absolutePath) ||
|
|
32
|
+
(await samePhysicalPath(gitWorktree.absolutePath, record.absolutePath))) {
|
|
33
|
+
ready = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
nextRecords.push({ ...durableRecord, health: ready ? 'ready' : 'repair' });
|
|
38
|
+
}
|
|
39
|
+
return nextRecords;
|
|
40
|
+
}
|
|
41
|
+
/** Project local branches against all Git worktrees and mark checkout/current state. */
|
|
42
|
+
export async function listBranches(context, input) {
|
|
43
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
44
|
+
await context.git.validateRepository(workspace.rootPath);
|
|
45
|
+
const [branches, worktrees] = await Promise.all([
|
|
46
|
+
context.git.listBranches(workspace.rootPath),
|
|
47
|
+
context.git.listWorktrees(workspace.rootPath),
|
|
48
|
+
]);
|
|
49
|
+
// `checkedOut` covers the main Workspace and all linked worktree; `isCurrent` only identifies the entry at the Workspace root's physical path.
|
|
50
|
+
const checkedOut = new Set(worktrees.flatMap((worktree) => (worktree.branch ? [worktree.branch] : [])));
|
|
51
|
+
let currentBranch;
|
|
52
|
+
for (const worktree of worktrees) {
|
|
53
|
+
if (await samePhysicalPath(worktree.absolutePath, workspace.rootPath)) {
|
|
54
|
+
currentBranch = worktree.branch;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return branches.map((name) => ({
|
|
59
|
+
name,
|
|
60
|
+
isCurrent: currentBranch === name,
|
|
61
|
+
checkedOut: checkedOut.has(name),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/** Create a Git Worktree first, then commit its external relation to the sidecar. */
|
|
65
|
+
export async function createWorktree(context, input) {
|
|
66
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
67
|
+
await context.git.validateRepository(workspace.rootPath);
|
|
68
|
+
if (typeof input.branch !== 'string' || input.branch.length === 0) {
|
|
69
|
+
throw providerError('GIT_OPERATION_FAILED', 'A local branch is required', {
|
|
70
|
+
workspaceRoot: workspace.rootPath,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const baseBranch = input.branch.trim();
|
|
74
|
+
if (input.newBranch !== undefined && typeof input.newBranch !== 'string') {
|
|
75
|
+
throw providerError('GIT_OPERATION_FAILED', 'A new branch name must be a string', {
|
|
76
|
+
workspaceRoot: workspace.rootPath,
|
|
77
|
+
baseBranch,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const newBranch = input.newBranch?.trim();
|
|
81
|
+
if (baseBranch.length === 0) {
|
|
82
|
+
throw providerError('GIT_OPERATION_FAILED', 'A local base branch is required', {
|
|
83
|
+
workspaceRoot: workspace.rootPath,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (input.newBranch !== undefined && (!newBranch || newBranch === baseBranch)) {
|
|
87
|
+
throw providerError('GIT_OPERATION_FAILED', 'A distinct new branch name is required', {
|
|
88
|
+
workspaceRoot: workspace.rootPath,
|
|
89
|
+
baseBranch,
|
|
90
|
+
newBranch: input.newBranch,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const branches = await context.git.listBranches(workspace.rootPath);
|
|
94
|
+
if (!branches.includes(baseBranch)) {
|
|
95
|
+
throw providerError('GIT_OPERATION_FAILED', `Local branch does not exist: ${baseBranch}`, {
|
|
96
|
+
workspaceRoot: workspace.rootPath,
|
|
97
|
+
branch: baseBranch,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const targetBranch = newBranch ?? baseBranch;
|
|
101
|
+
if (newBranch !== undefined && branches.includes(newBranch)) {
|
|
102
|
+
throw providerError('WORKTREE_BRANCH_CONFLICT', `New branch already exists: ${newBranch}`, {
|
|
103
|
+
workspaceRoot: workspace.rootPath,
|
|
104
|
+
branch: newBranch,
|
|
105
|
+
baseBranch,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const existingGitWorktrees = await context.git.listWorktrees(workspace.rootPath);
|
|
109
|
+
if (existingGitWorktrees.some((worktree) => worktree.branch === targetBranch)) {
|
|
110
|
+
throw providerError('WORKTREE_BRANCH_CONFLICT', `Branch is already checked out: ${targetBranch}`, {
|
|
111
|
+
workspaceRoot: workspace.rootPath,
|
|
112
|
+
branch: targetBranch,
|
|
113
|
+
baseBranch,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const worktreeId = generatedId(context.idFactory);
|
|
117
|
+
const targetPath = path.resolve(context.dshHome, 'clutch-dsh-worktree', 'worktree', worktreeId);
|
|
118
|
+
// 创建前同时检查词法和物理目录边界,阻止配置、ID 或 symlink 将目标引回 Workspace 或带出插件根目录。
|
|
119
|
+
// Check lexical and physical directory boundaries before creation so configuration, IDs, or symlinks cannot redirect the target into the Workspace or outside the plugin root.
|
|
120
|
+
validateGeneratedPath(context, workspace.rootPath, targetPath, worktreeId);
|
|
121
|
+
await validatePhysicalGeneratedPath(context, workspace.rootPath, targetPath);
|
|
122
|
+
if (await pathExists(targetPath)) {
|
|
123
|
+
throw providerError('GIT_OPERATION_FAILED', `Generated Worktree path already exists: ${targetPath}`, {
|
|
124
|
+
workspaceRoot: workspace.rootPath,
|
|
125
|
+
targetPath,
|
|
126
|
+
worktreeId,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Git is the first external side effect; the sidecar mutation below is the relation commit point.
|
|
130
|
+
try {
|
|
131
|
+
await context.git.createWorktree(workspace.rootPath, targetPath, baseBranch, newBranch);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw asGitError('create worktree', workspace.rootPath, targetPath, error);
|
|
135
|
+
}
|
|
136
|
+
const record = {
|
|
137
|
+
worktreeId,
|
|
138
|
+
workspaceId: input.workspaceId,
|
|
139
|
+
absolutePath: targetPath,
|
|
140
|
+
branch: targetBranch,
|
|
141
|
+
status: 'active',
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
return await context.sidecar.mutate(input.workspaceId, (snapshot) => {
|
|
145
|
+
// Recheck ID and branch inside the serialized mutation so concurrent writes cannot create duplicate active records.
|
|
146
|
+
const idConflict = snapshot.worktrees.find((worktree) => worktree.worktreeId === worktreeId);
|
|
147
|
+
if (idConflict) {
|
|
148
|
+
throw providerError('SIDECAR_CORRUPT', `Generated Worktree ID is already recorded: ${worktreeId}`, {
|
|
149
|
+
worktreeId,
|
|
150
|
+
existingStatus: idConflict.status,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const branchConflict = snapshot.worktrees.find((worktree) => worktree.status === 'active' && worktree.branch === targetBranch);
|
|
154
|
+
if (branchConflict) {
|
|
155
|
+
throw providerError('WORKTREE_BRANCH_CONFLICT', `Branch is already recorded as active: ${targetBranch}`, {
|
|
156
|
+
branch: targetBranch,
|
|
157
|
+
worktreeId: branchConflict.worktreeId,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const next = {
|
|
161
|
+
...snapshot,
|
|
162
|
+
worktrees: [...snapshot.worktrees, record],
|
|
163
|
+
};
|
|
164
|
+
return { result: record, snapshot: next };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const sidecarError = asSidecarError(error, input.workspaceId);
|
|
169
|
+
// Remove the new Worktree when sidecar commit fails; expose a sync-required error if compensation fails.
|
|
170
|
+
try {
|
|
171
|
+
await context.git.removeWorktree(workspace.rootPath, targetPath);
|
|
172
|
+
}
|
|
173
|
+
catch (cleanupError) {
|
|
174
|
+
throw providerError('SIDECAR_SYNC_REQUIRED', `Sidecar write failed and the newly created Worktree could not be cleaned up: ${targetPath}`, {
|
|
175
|
+
workspaceId: input.workspaceId,
|
|
176
|
+
workspaceRoot: workspace.rootPath,
|
|
177
|
+
targetPath,
|
|
178
|
+
sidecarError: sidecarError.message,
|
|
179
|
+
cleanupError: describeError(cleanupError),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
throw sidecarError;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Remove Git state first, then mark the relation removed while preserving detached bindings. */
|
|
186
|
+
export async function removeWorktree(context, input) {
|
|
187
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
188
|
+
let gitRemoved = false;
|
|
189
|
+
try {
|
|
190
|
+
await context.sidecar.mutate(input.workspaceId, async (snapshot) => {
|
|
191
|
+
const record = snapshot.worktrees.find((worktree) => worktree.worktreeId === input.worktreeId);
|
|
192
|
+
if (!record) {
|
|
193
|
+
throw providerError('WORKTREE_NOT_FOUND', `Worktree not found: ${input.worktreeId}`, {
|
|
194
|
+
worktreeId: input.worktreeId,
|
|
195
|
+
workspaceId: input.workspaceId,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (record.status === 'removed') {
|
|
199
|
+
throw providerError('WORKTREE_REMOVED', `Worktree has already been removed: ${input.worktreeId}`, {
|
|
200
|
+
worktreeId: input.worktreeId,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
await context.git.removeWorktree(workspace.rootPath, record.absolutePath);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
let stillRegistered;
|
|
208
|
+
try {
|
|
209
|
+
stillRegistered = false;
|
|
210
|
+
for (const worktree of await context.git.listWorktrees(workspace.rootPath)) {
|
|
211
|
+
if (await samePhysicalPath(worktree.absolutePath, record.absolutePath)) {
|
|
212
|
+
stillRegistered = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw asGitError('remove worktree', workspace.rootPath, record.absolutePath, error);
|
|
219
|
+
}
|
|
220
|
+
if (stillRegistered) {
|
|
221
|
+
throw asGitError('remove worktree', workspace.rootPath, record.absolutePath, error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
gitRemoved = true;
|
|
225
|
+
// Preserve Worktree and binding history while changing lifecycle only; DSH Sessions are never read or deleted here.
|
|
226
|
+
const next = {
|
|
227
|
+
...snapshot,
|
|
228
|
+
worktrees: snapshot.worktrees.map((candidate) => candidate.worktreeId === record.worktreeId ? { ...candidate, status: 'removed' } : candidate),
|
|
229
|
+
bindings: snapshot.bindings.map((binding) => binding.worktreeId === record.worktreeId && binding.status === 'active'
|
|
230
|
+
? { ...binding, status: 'detached' }
|
|
231
|
+
: binding),
|
|
232
|
+
};
|
|
233
|
+
return { result: undefined, snapshot: next };
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (gitRemoved) {
|
|
238
|
+
// Git removal cannot be rolled back here, so expose the divergence for a safe retry.
|
|
239
|
+
const sidecarError = asSidecarError(error, input.workspaceId);
|
|
240
|
+
throw providerError('SIDECAR_SYNC_REQUIRED', `Git removed Worktree ${input.worktreeId}, but sidecar synchronization failed`, {
|
|
241
|
+
workspaceId: input.workspaceId,
|
|
242
|
+
worktreeId: input.worktreeId,
|
|
243
|
+
workspaceRoot: workspace.rootPath,
|
|
244
|
+
sidecarError: sidecarError.message,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export async function insertWorktreeBefore(context, input) {
|
|
251
|
+
await requireWorkspace(context, input.workspaceId);
|
|
252
|
+
try {
|
|
253
|
+
return await context.sidecar.insertWorktreeBefore(input.workspaceId, input.worktreeId, input.beforeWorktreeId);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
throw asSidecarError(error, input.workspaceId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=manager-worktrees.js.map
|