@agimon-ai/doompi-git 0.0.1-alpha.11 → 0.0.1-alpha.13
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 +1 -1
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/api-contracts.cjs +7 -0
- package/dist/api-contracts.d.cts +2 -0
- package/dist/api-contracts.d.mts +2 -0
- package/dist/api-contracts.mjs +2 -0
- package/dist/extensions/pi.cjs +39 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +7 -0
- package/dist/extensions/pi.d.cts.map +1 -0
- package/dist/extensions/pi.d.mts +7 -0
- package/dist/extensions/pi.d.mts.map +1 -0
- package/dist/extensions/pi.mjs +34 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/extensions/server.cjs +73 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.cts +5 -0
- package/dist/extensions/server.d.cts.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +68 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +515 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +3 -0
- package/dist/src/constants/git.cjs +8 -0
- package/dist/src/constants/git.cjs.map +1 -0
- package/dist/src/constants/git.mjs +7 -0
- package/dist/src/constants/git.mjs.map +1 -0
- package/dist/src/extensions/(backend)/api/_lib/route.server.cjs +93 -0
- package/dist/src/extensions/(backend)/api/_lib/route.server.cjs.map +1 -0
- package/dist/src/extensions/(backend)/api/_lib/route.server.mjs +92 -0
- package/dist/src/extensions/(backend)/api/_lib/route.server.mjs.map +1 -0
- package/dist/src/extensions/(backend)/api/route.server.cjs +7 -0
- package/dist/src/extensions/(backend)/api/route.server.cjs.map +1 -0
- package/dist/src/extensions/(backend)/api/route.server.mjs +8 -0
- package/dist/src/extensions/(backend)/api/route.server.mjs.map +1 -0
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.cjs +223 -0
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.cjs.map +1 -0
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.mjs +222 -0
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.mjs.map +1 -0
- package/dist/src/extensions/(backend)/channel/git-worktrees.server.cjs +7 -0
- package/dist/src/extensions/(backend)/channel/git-worktrees.server.cjs.map +1 -0
- package/dist/src/extensions/(backend)/channel/git-worktrees.server.mjs +8 -0
- package/dist/src/extensions/(backend)/channel/git-worktrees.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.cli.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.cli.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.server.cjs +73 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.server.mjs +72 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/_lib/root.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/_lib/git.cli.cjs +17 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/_lib/git.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/_lib/git.cli.mjs +16 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/_lib/git.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/git.cli.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/git.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/git.cli.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/command/git.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompi-use-git.cli.cjs +15 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompi-use-git.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompi-use-git.cli.mjs +15 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompi-use-git.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGit.server.cjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGit.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGit.server.mjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGit.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGitReadme.server.cjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGitReadme.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGitReadme.server.mjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiGitReadme.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiUseGit.server.cjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiUseGit.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiUseGit.server.mjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/_lib/doompiUseGit.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompi-use-git.cli.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompi-use-git.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompi-use-git.cli.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompi-use-git.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGit.server.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGit.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGit.server.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGit.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGitReadme.server.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGitReadme.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGitReadme.server.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiGitReadme.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiUseGit.server.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiUseGit.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiUseGit.server.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/resource/doompiUseGit.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/_lib/runWorktree.cli.cjs +38 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/_lib/runWorktree.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/_lib/runWorktree.cli.mjs +36 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/_lib/runWorktree.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.cli.cjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.cli.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.cli.mjs +8 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.cli.mjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.server.cjs +6 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.server.cjs.map +1 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.server.mjs +7 -0
- package/dist/src/extensions/workspaces/sessions/(backend)/tool/runWorktree.server.mjs.map +1 -0
- package/dist/src/schemas/apiContracts.cjs +143 -0
- package/dist/src/schemas/apiContracts.cjs.map +1 -0
- package/dist/src/schemas/apiContracts.d.cts +53 -0
- package/dist/src/schemas/apiContracts.d.cts.map +1 -0
- package/dist/src/schemas/apiContracts.d.mts +53 -0
- package/dist/src/schemas/apiContracts.d.mts.map +1 -0
- package/dist/src/schemas/apiContracts.mjs +140 -0
- package/dist/src/schemas/apiContracts.mjs.map +1 -0
- package/dist/src/schemas/runWorktreeTool.cjs +152 -0
- package/dist/src/schemas/runWorktreeTool.cjs.map +1 -0
- package/dist/src/schemas/runWorktreeTool.mjs +150 -0
- package/dist/src/schemas/runWorktreeTool.mjs.map +1 -0
- package/dist/src/services/atomicJson/index.cjs +42 -0
- package/dist/src/services/atomicJson/index.cjs.map +1 -0
- package/dist/src/services/atomicJson/index.mjs +38 -0
- package/dist/src/services/atomicJson/index.mjs.map +1 -0
- package/dist/src/services/compositionMirror/index.cjs +119 -0
- package/dist/src/services/compositionMirror/index.cjs.map +1 -0
- package/dist/src/services/compositionMirror/index.mjs +116 -0
- package/dist/src/services/compositionMirror/index.mjs.map +1 -0
- package/dist/src/services/errors/index.cjs +29 -0
- package/dist/src/services/errors/index.cjs.map +1 -0
- package/dist/src/services/errors/index.mjs +27 -0
- package/dist/src/services/errors/index.mjs.map +1 -0
- package/dist/src/services/extensionService/index.cjs +13 -0
- package/dist/src/services/extensionService/index.cjs.map +1 -0
- package/dist/src/services/extensionService/index.d.cts +7 -0
- package/dist/src/services/extensionService/index.d.cts.map +1 -0
- package/dist/src/services/extensionService/index.d.mts +7 -0
- package/dist/src/services/extensionService/index.d.mts.map +1 -0
- package/dist/src/services/extensionService/index.mjs +13 -0
- package/dist/src/services/extensionService/index.mjs.map +1 -0
- package/dist/src/services/gitCli/index.cjs +144 -0
- package/dist/src/services/gitCli/index.cjs.map +1 -0
- package/dist/src/services/gitCli/index.mjs +142 -0
- package/dist/src/services/gitCli/index.mjs.map +1 -0
- package/dist/src/services/gitDependencies/index.cjs +14 -0
- package/dist/src/services/gitDependencies/index.cjs.map +1 -0
- package/dist/src/services/gitDependencies/index.d.cts +5 -0
- package/dist/src/services/gitDependencies/index.d.cts.map +1 -0
- package/dist/src/services/gitDependencies/index.d.mts +5 -0
- package/dist/src/services/gitDependencies/index.d.mts.map +1 -0
- package/dist/src/services/gitDependencies/index.mjs +14 -0
- package/dist/src/services/gitDependencies/index.mjs.map +1 -0
- package/dist/src/services/paths/index.cjs +66 -0
- package/dist/src/services/paths/index.cjs.map +1 -0
- package/dist/src/services/paths/index.mjs +60 -0
- package/dist/src/services/paths/index.mjs.map +1 -0
- package/dist/src/services/repositoryIdentity/index.cjs +71 -0
- package/dist/src/services/repositoryIdentity/index.cjs.map +1 -0
- package/dist/src/services/repositoryIdentity/index.mjs +65 -0
- package/dist/src/services/repositoryIdentity/index.mjs.map +1 -0
- package/dist/src/services/runWorktree/index.cjs +146 -0
- package/dist/src/services/runWorktree/index.cjs.map +1 -0
- package/dist/src/services/runWorktree/index.mjs +143 -0
- package/dist/src/services/runWorktree/index.mjs.map +1 -0
- package/dist/src/services/worktreeEvents/index.cjs +64 -0
- package/dist/src/services/worktreeEvents/index.cjs.map +1 -0
- package/dist/src/services/worktreeEvents/index.d.cts +14 -0
- package/dist/src/services/worktreeEvents/index.d.cts.map +1 -0
- package/dist/src/services/worktreeEvents/index.d.mts +14 -0
- package/dist/src/services/worktreeEvents/index.d.mts.map +1 -0
- package/dist/src/services/worktreeEvents/index.mjs +60 -0
- package/dist/src/services/worktreeEvents/index.mjs.map +1 -0
- package/dist/src/services/worktreeNaming/index.cjs +99 -0
- package/dist/src/services/worktreeNaming/index.cjs.map +1 -0
- package/dist/src/services/worktreeNaming/index.d.cts +33 -0
- package/dist/src/services/worktreeNaming/index.d.cts.map +1 -0
- package/dist/src/services/worktreeNaming/index.d.mts +33 -0
- package/dist/src/services/worktreeNaming/index.d.mts.map +1 -0
- package/dist/src/services/worktreeNaming/index.mjs +94 -0
- package/dist/src/services/worktreeNaming/index.mjs.map +1 -0
- package/dist/src/services/worktreeOperations/index.cjs +324 -0
- package/dist/src/services/worktreeOperations/index.cjs.map +1 -0
- package/dist/src/services/worktreeOperations/index.d.cts +39 -0
- package/dist/src/services/worktreeOperations/index.d.cts.map +1 -0
- package/dist/src/services/worktreeOperations/index.d.mts +39 -0
- package/dist/src/services/worktreeOperations/index.d.mts.map +1 -0
- package/dist/src/services/worktreeOperations/index.mjs +322 -0
- package/dist/src/services/worktreeOperations/index.mjs.map +1 -0
- package/dist/src/services/worktreeRegistry/index.cjs +38 -0
- package/dist/src/services/worktreeRegistry/index.cjs.map +1 -0
- package/dist/src/services/worktreeRegistry/index.mjs +38 -0
- package/dist/src/services/worktreeRegistry/index.mjs.map +1 -0
- package/dist/src/types/extension.d.cts +16 -0
- package/dist/src/types/extension.d.cts.map +1 -0
- package/dist/src/types/extension.d.mts +16 -0
- package/dist/src/types/extension.d.mts.map +1 -0
- package/dist/src/types/webWorktrees.cjs +20 -0
- package/dist/src/types/webWorktrees.cjs.map +1 -0
- package/dist/src/types/webWorktrees.mjs +20 -0
- package/dist/src/types/webWorktrees.mjs.map +1 -0
- package/dist/src/types/worktreeRegistry.d.cts +40 -0
- package/dist/src/types/worktreeRegistry.d.cts.map +1 -0
- package/dist/src/types/worktreeRegistry.d.mts +40 -0
- package/dist/src/types/worktreeRegistry.d.mts.map +1 -0
- package/package.json +6 -5
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//#region src/services/worktreeNaming/index.ts
|
|
2
|
+
const MAX_SLUG_LENGTH = 64;
|
|
3
|
+
/**
|
|
4
|
+
* A branch name reduced to one safe path segment.
|
|
5
|
+
*
|
|
6
|
+
* Slashes are the point: `wt/fix-auth` is a perfectly good branch and a
|
|
7
|
+
* terrible directory name, because the nested directory it implies is not the
|
|
8
|
+
* one the worktree lives in.
|
|
9
|
+
*/
|
|
10
|
+
function branchSlug(branch) {
|
|
11
|
+
const slug = branch.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, MAX_SLUG_LENGTH).replace(/-+$/gu, "");
|
|
12
|
+
return slug === "" ? "worktree" : slug;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The directory for one worktree.
|
|
16
|
+
*
|
|
17
|
+
* The short id disambiguates two branches whose slugs collide, such as
|
|
18
|
+
* `feature/x` and `feature_x`, which would otherwise want the same directory.
|
|
19
|
+
*/
|
|
20
|
+
function worktreeDirectory(input) {
|
|
21
|
+
const repository = `${input.repositoryLabel}--${input.repositoryId}`;
|
|
22
|
+
return `${input.worktreesRoot}/${repository}/${branchSlug(input.branch)}--${input.shortId}`;
|
|
23
|
+
}
|
|
24
|
+
/** Why a worktree cannot be created, or undefined when it can. */
|
|
25
|
+
function refuseSpawn(input) {
|
|
26
|
+
if (input.branch.trim() === "") return "A branch name is required.";
|
|
27
|
+
const live = input.existing.find((record) => record.branch === input.branch && record.status !== "orphaned");
|
|
28
|
+
if (live) return `Branch ${input.branch} already has a worktree (${live.id}).`;
|
|
29
|
+
}
|
|
30
|
+
/** Why a worktree cannot be closed, or undefined when it can. */
|
|
31
|
+
function refuseClose(input) {
|
|
32
|
+
if (!input.record) return "No worktree with that id. Call list to see the current ids.";
|
|
33
|
+
if (input.dirtyFiles.length === 0 || input.force) return void 0;
|
|
34
|
+
const names = input.dirtyFiles.slice(0, 10).join(", ");
|
|
35
|
+
const more = input.dirtyFiles.length > 10 ? `, and ${String(input.dirtyFiles.length - 10)} more` : "";
|
|
36
|
+
return `${String(input.dirtyFiles.length)} uncommitted file(s): ${names}${more}. Pass force: true to discard them.`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Marks records whose process or directory is gone as orphaned.
|
|
40
|
+
*
|
|
41
|
+
* Nothing is deleted here. A doompi-git orphan owns a directory that may hold
|
|
42
|
+
* uncommitted work, so losing the record would lose the only pointer to it.
|
|
43
|
+
* Deletion is always a separate, named decision.
|
|
44
|
+
*/
|
|
45
|
+
function reconcile(records, probe) {
|
|
46
|
+
const changed = [];
|
|
47
|
+
return {
|
|
48
|
+
records: records.map((record) => {
|
|
49
|
+
if (record.status === "orphaned") return record;
|
|
50
|
+
if (!(!probe.exists(record.path) || !probe.alive(record))) return record;
|
|
51
|
+
changed.push(record.id);
|
|
52
|
+
return {
|
|
53
|
+
...record,
|
|
54
|
+
status: "orphaned"
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
changed
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What a prune would do, decided before anything is destroyed.
|
|
62
|
+
*
|
|
63
|
+
* Separating the plan from the execution is what makes dryRun honest and lets
|
|
64
|
+
* the destructive half be reviewed on its own.
|
|
65
|
+
*/
|
|
66
|
+
function planPrune(input) {
|
|
67
|
+
const plan = {
|
|
68
|
+
remove: [],
|
|
69
|
+
forget: [],
|
|
70
|
+
untracked: [],
|
|
71
|
+
keptDirty: [],
|
|
72
|
+
keptBranches: []
|
|
73
|
+
};
|
|
74
|
+
const known = new Set(input.records.map((record) => record.path));
|
|
75
|
+
const listed = new Set(input.gitPaths);
|
|
76
|
+
for (const record of input.records) {
|
|
77
|
+
if (record.status !== "orphaned") continue;
|
|
78
|
+
if (!listed.has(record.path)) {
|
|
79
|
+
plan.forget.push(record);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if ((input.dirtyByPath.get(record.path) ?? []).length > 0) {
|
|
83
|
+
plan.keptDirty.push(record);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
plan.remove.push(record);
|
|
87
|
+
}
|
|
88
|
+
for (const path of input.gitPaths) if (path.startsWith(`${input.worktreesRoot}/`) && !known.has(path)) plan.untracked.push(path);
|
|
89
|
+
return plan;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
exports.branchSlug = branchSlug;
|
|
93
|
+
exports.planPrune = planPrune;
|
|
94
|
+
exports.reconcile = reconcile;
|
|
95
|
+
exports.refuseClose = refuseClose;
|
|
96
|
+
exports.refuseSpawn = refuseSpawn;
|
|
97
|
+
exports.worktreeDirectory = worktreeDirectory;
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../../src/services/worktreeNaming/index.ts"],"sourcesContent":["/**\n * Naming and reconciliation rules for worktrees, as pure functions.\n *\n * Everything here is a function of its arguments so the interesting decisions,\n * which are the ones about destroying work, can be tested without a repository\n * or a filesystem.\n */\n\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\n\nconst MAX_SLUG_LENGTH = 64;\n\n/**\n * A branch name reduced to one safe path segment.\n *\n * Slashes are the point: `wt/fix-auth` is a perfectly good branch and a\n * terrible directory name, because the nested directory it implies is not the\n * one the worktree lives in.\n */\nexport function branchSlug(branch: string): string {\n const slug = branch\n .toLowerCase()\n .replace(/[^a-z0-9]+/gu, '-')\n .replace(/^-+|-+$/gu, '')\n .slice(0, MAX_SLUG_LENGTH)\n .replace(/-+$/gu, '');\n return slug === '' ? 'worktree' : slug;\n}\n\n/**\n * The directory for one worktree.\n *\n * The short id disambiguates two branches whose slugs collide, such as\n * `feature/x` and `feature_x`, which would otherwise want the same directory.\n */\nexport function worktreeDirectory(input: {\n worktreesRoot: string;\n repositoryLabel: string;\n repositoryId: string;\n branch: string;\n shortId: string;\n}): string {\n const repository = `${input.repositoryLabel}--${input.repositoryId}`;\n return `${input.worktreesRoot}/${repository}/${branchSlug(input.branch)}--${input.shortId}`;\n}\n\n/** Why a worktree cannot be created, or undefined when it can. */\nexport function refuseSpawn(input: { branch: string; existing: readonly WorktreeRecord[] }): string | undefined {\n if (input.branch.trim() === '') return 'A branch name is required.';\n // Git refuses a second worktree on one branch, but its message names a path\n // the caller never chose, so this says it in the caller's own terms first.\n const live = input.existing.find((record) => record.branch === input.branch && record.status !== 'orphaned');\n if (live) return `Branch ${input.branch} already has a worktree (${live.id}).`;\n return undefined;\n}\n\n/** Why a worktree cannot be closed, or undefined when it can. */\nexport function refuseClose(input: {\n record: WorktreeRecord | undefined;\n dirtyFiles: readonly string[];\n force: boolean;\n}): string | undefined {\n if (!input.record) return 'No worktree with that id. Call list to see the current ids.';\n if (input.dirtyFiles.length === 0 || input.force) return undefined;\n // Naming the files is the point: \"the tree is dirty\" sends the reader to go\n // look, and the answer is usually a file they forgot rather than one they\n // meant to keep.\n const names = input.dirtyFiles.slice(0, 10).join(', ');\n const more = input.dirtyFiles.length > 10 ? `, and ${String(input.dirtyFiles.length - 10)} more` : '';\n return `${String(input.dirtyFiles.length)} uncommitted file(s): ${names}${more}. Pass force: true to discard them.`;\n}\n\nexport interface Reconciliation {\n records: WorktreeRecord[];\n /** Ids whose status changed, so a caller can report what it noticed. */\n changed: string[];\n}\n\n/**\n * Marks records whose process or directory is gone as orphaned.\n *\n * Nothing is deleted here. A doompi-git orphan owns a directory that may hold\n * uncommitted work, so losing the record would lose the only pointer to it.\n * Deletion is always a separate, named decision.\n */\nexport function reconcile(\n records: readonly WorktreeRecord[],\n probe: { alive: (record: WorktreeRecord) => boolean; exists: (path: string) => boolean },\n): Reconciliation {\n const changed: string[] = [];\n const next = records.map((record) => {\n if (record.status === 'orphaned') return record;\n const gone = !probe.exists(record.path) || !probe.alive(record);\n if (!gone) return record;\n changed.push(record.id);\n return { ...record, status: 'orphaned' as const };\n });\n return { records: next, changed };\n}\n\nexport interface PrunePlan {\n /** Orphans whose directory git still lists, safe to remove. */\n remove: WorktreeRecord[];\n /** Orphans whose directory is already gone; drop the record only. */\n forget: WorktreeRecord[];\n /** Paths git lists under our root that we have no record of; report, never touch. */\n untracked: string[];\n /** Orphans left alone because they still hold uncommitted work. */\n keptDirty: WorktreeRecord[];\n /**\n * Branches git refused to delete because they hold commits.\n *\n * Filled by the prune itself, not by the plan: a dry run destroys nothing, so\n * it has nothing to report here and leaves it empty.\n */\n keptBranches: string[];\n}\n\n/**\n * What a prune would do, decided before anything is destroyed.\n *\n * Separating the plan from the execution is what makes dryRun honest and lets\n * the destructive half be reviewed on its own.\n */\nexport function planPrune(input: {\n records: readonly WorktreeRecord[];\n gitPaths: readonly string[];\n worktreesRoot: string;\n dirtyByPath: ReadonlyMap<string, readonly string[]>;\n}): PrunePlan {\n const plan: PrunePlan = { remove: [], forget: [], untracked: [], keptDirty: [], keptBranches: [] };\n const known = new Set(input.records.map((record) => record.path));\n const listed = new Set(input.gitPaths);\n for (const record of input.records) {\n if (record.status !== 'orphaned') continue;\n if (!listed.has(record.path)) {\n plan.forget.push(record);\n continue;\n }\n if ((input.dirtyByPath.get(record.path) ?? []).length > 0) {\n plan.keptDirty.push(record);\n continue;\n }\n plan.remove.push(record);\n }\n for (const path of input.gitPaths) {\n // Only ever inside our own root: a worktree the user made by hand is none\n // of this package's business, even when it looks abandoned.\n if (path.startsWith(`${input.worktreesRoot}/`) && !known.has(path)) plan.untracked.push(path);\n }\n return plan;\n}\n"],"mappings":";AAUA,MAAM,kBAAkB;;;;;;;;AASxB,SAAgB,WAAW,QAAwB;CACjD,MAAM,OAAO,OACV,YAAY,CAAC,CACb,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,aAAa,EAAE,CAAC,CACxB,MAAM,GAAG,eAAe,CAAC,CACzB,QAAQ,SAAS,EAAE;CACtB,OAAO,SAAS,KAAK,aAAa;AACpC;;;;;;;AAQA,SAAgB,kBAAkB,OAMvB;CACT,MAAM,aAAa,GAAG,MAAM,gBAAgB,IAAI,MAAM;CACtD,OAAO,GAAG,MAAM,cAAc,GAAG,WAAW,GAAG,WAAW,MAAM,MAAM,EAAE,IAAI,MAAM;AACpF;;AAGA,SAAgB,YAAY,OAAoF;CAC9G,IAAI,MAAM,OAAO,KAAK,MAAM,IAAI,OAAO;CAGvC,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW,OAAO,WAAW,MAAM,UAAU,OAAO,WAAW,UAAU;CAC3G,IAAI,MAAM,OAAO,UAAU,MAAM,OAAO,2BAA2B,KAAK,GAAG;AAE7E;;AAGA,SAAgB,YAAY,OAIL;CACrB,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,IAAI,MAAM,WAAW,WAAW,KAAK,MAAM,OAAO,OAAO,KAAA;CAIzD,MAAM,QAAQ,MAAM,WAAW,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,IAAI;CACrD,MAAM,OAAO,MAAM,WAAW,SAAS,KAAK,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,EAAE,SAAS;CACnG,OAAO,GAAG,OAAO,MAAM,WAAW,MAAM,EAAE,wBAAwB,QAAQ,KAAK;AACjF;;;;;;;;AAeA,SAAgB,UACd,SACA,OACgB;CAChB,MAAM,UAAoB,CAAC;CAQ3B,OAAO;EAAE,SAPI,QAAQ,KAAK,WAAW;GACnC,IAAI,OAAO,WAAW,YAAY,OAAO;GAEzC,IAAI,EADS,CAAC,MAAM,OAAO,OAAO,IAAI,KAAK,CAAC,MAAM,MAAM,MAAM,IACnD,OAAO;GAClB,QAAQ,KAAK,OAAO,EAAE;GACtB,OAAO;IAAE,GAAG;IAAQ,QAAQ;GAAoB;EAClD,CACqB;EAAG;CAAQ;AAClC;;;;;;;AA0BA,SAAgB,UAAU,OAKZ;CACZ,MAAM,OAAkB;EAAE,QAAQ,CAAC;EAAG,QAAQ,CAAC;EAAG,WAAW,CAAC;EAAG,WAAW,CAAC;EAAG,cAAc,CAAC;CAAE;CACjG,MAAM,QAAQ,IAAI,IAAI,MAAM,QAAQ,KAAK,WAAW,OAAO,IAAI,CAAC;CAChE,MAAM,SAAS,IAAI,IAAI,MAAM,QAAQ;CACrC,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,OAAO,WAAW,YAAY;EAClC,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,GAAG;GAC5B,KAAK,OAAO,KAAK,MAAM;GACvB;EACF;EACA,KAAK,MAAM,YAAY,IAAI,OAAO,IAAI,KAAK,CAAC,EAAA,CAAG,SAAS,GAAG;GACzD,KAAK,UAAU,KAAK,MAAM;GAC1B;EACF;EACA,KAAK,OAAO,KAAK,MAAM;CACzB;CACA,KAAK,MAAM,QAAQ,MAAM,UAGvB,IAAI,KAAK,WAAW,GAAG,MAAM,cAAc,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI,GAAG,KAAK,UAAU,KAAK,IAAI;CAE9F,OAAO;AACT"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WorktreeRecord } from "../../types/worktreeRegistry.cjs";
|
|
2
|
+
//#region src/services/worktreeNaming/index.d.ts
|
|
3
|
+
export interface PrunePlan {
|
|
4
|
+
/** Orphans whose directory git still lists, safe to remove. */
|
|
5
|
+
remove: WorktreeRecord[];
|
|
6
|
+
/** Orphans whose directory is already gone; drop the record only. */
|
|
7
|
+
forget: WorktreeRecord[];
|
|
8
|
+
/** Paths git lists under our root that we have no record of; report, never touch. */
|
|
9
|
+
untracked: string[];
|
|
10
|
+
/** Orphans left alone because they still hold uncommitted work. */
|
|
11
|
+
keptDirty: WorktreeRecord[];
|
|
12
|
+
/**
|
|
13
|
+
* Branches git refused to delete because they hold commits.
|
|
14
|
+
*
|
|
15
|
+
* Filled by the prune itself, not by the plan: a dry run destroys nothing, so
|
|
16
|
+
* it has nothing to report here and leaves it empty.
|
|
17
|
+
*/
|
|
18
|
+
keptBranches: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a prune would do, decided before anything is destroyed.
|
|
22
|
+
*
|
|
23
|
+
* Separating the plan from the execution is what makes dryRun honest and lets
|
|
24
|
+
* the destructive half be reviewed on its own.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planPrune(input: {
|
|
27
|
+
records: readonly WorktreeRecord[];
|
|
28
|
+
gitPaths: readonly string[];
|
|
29
|
+
worktreesRoot: string;
|
|
30
|
+
dirtyByPath: ReadonlyMap<string, readonly string[]>;
|
|
31
|
+
}): PrunePlan;
|
|
32
|
+
//#endregion
|
|
33
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../../src/services/worktreeNaming/index.ts"],"mappings":";;iBAoGiB;;EAEf,QAAQ;;EAER,QAAQ;;EAER;;EAEA,WAAW;;;;;;;EAOX;;;;;;;;wBASc,UAAU;EACxB,kBAAkB;EAClB;EACA;EACA,aAAa;IACX"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WorktreeRecord } from "../../types/worktreeRegistry.mjs";
|
|
2
|
+
//#region src/services/worktreeNaming/index.d.ts
|
|
3
|
+
export interface PrunePlan {
|
|
4
|
+
/** Orphans whose directory git still lists, safe to remove. */
|
|
5
|
+
remove: WorktreeRecord[];
|
|
6
|
+
/** Orphans whose directory is already gone; drop the record only. */
|
|
7
|
+
forget: WorktreeRecord[];
|
|
8
|
+
/** Paths git lists under our root that we have no record of; report, never touch. */
|
|
9
|
+
untracked: string[];
|
|
10
|
+
/** Orphans left alone because they still hold uncommitted work. */
|
|
11
|
+
keptDirty: WorktreeRecord[];
|
|
12
|
+
/**
|
|
13
|
+
* Branches git refused to delete because they hold commits.
|
|
14
|
+
*
|
|
15
|
+
* Filled by the prune itself, not by the plan: a dry run destroys nothing, so
|
|
16
|
+
* it has nothing to report here and leaves it empty.
|
|
17
|
+
*/
|
|
18
|
+
keptBranches: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a prune would do, decided before anything is destroyed.
|
|
22
|
+
*
|
|
23
|
+
* Separating the plan from the execution is what makes dryRun honest and lets
|
|
24
|
+
* the destructive half be reviewed on its own.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planPrune(input: {
|
|
27
|
+
records: readonly WorktreeRecord[];
|
|
28
|
+
gitPaths: readonly string[];
|
|
29
|
+
worktreesRoot: string;
|
|
30
|
+
dirtyByPath: ReadonlyMap<string, readonly string[]>;
|
|
31
|
+
}): PrunePlan;
|
|
32
|
+
//#endregion
|
|
33
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/worktreeNaming/index.ts"],"mappings":";;iBAoGiB;;EAEf,QAAQ;;EAER,QAAQ;;EAER;;EAEA,WAAW;;;;;;;EAOX;;;;;;;;wBASc,UAAU;EACxB,kBAAkB;EAClB;EACA;EACA,aAAa;IACX"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/services/worktreeNaming/index.ts
|
|
2
|
+
const MAX_SLUG_LENGTH = 64;
|
|
3
|
+
/**
|
|
4
|
+
* A branch name reduced to one safe path segment.
|
|
5
|
+
*
|
|
6
|
+
* Slashes are the point: `wt/fix-auth` is a perfectly good branch and a
|
|
7
|
+
* terrible directory name, because the nested directory it implies is not the
|
|
8
|
+
* one the worktree lives in.
|
|
9
|
+
*/
|
|
10
|
+
function branchSlug(branch) {
|
|
11
|
+
const slug = branch.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, MAX_SLUG_LENGTH).replace(/-+$/gu, "");
|
|
12
|
+
return slug === "" ? "worktree" : slug;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The directory for one worktree.
|
|
16
|
+
*
|
|
17
|
+
* The short id disambiguates two branches whose slugs collide, such as
|
|
18
|
+
* `feature/x` and `feature_x`, which would otherwise want the same directory.
|
|
19
|
+
*/
|
|
20
|
+
function worktreeDirectory(input) {
|
|
21
|
+
const repository = `${input.repositoryLabel}--${input.repositoryId}`;
|
|
22
|
+
return `${input.worktreesRoot}/${repository}/${branchSlug(input.branch)}--${input.shortId}`;
|
|
23
|
+
}
|
|
24
|
+
/** Why a worktree cannot be created, or undefined when it can. */
|
|
25
|
+
function refuseSpawn(input) {
|
|
26
|
+
if (input.branch.trim() === "") return "A branch name is required.";
|
|
27
|
+
const live = input.existing.find((record) => record.branch === input.branch && record.status !== "orphaned");
|
|
28
|
+
if (live) return `Branch ${input.branch} already has a worktree (${live.id}).`;
|
|
29
|
+
}
|
|
30
|
+
/** Why a worktree cannot be closed, or undefined when it can. */
|
|
31
|
+
function refuseClose(input) {
|
|
32
|
+
if (!input.record) return "No worktree with that id. Call list to see the current ids.";
|
|
33
|
+
if (input.dirtyFiles.length === 0 || input.force) return void 0;
|
|
34
|
+
const names = input.dirtyFiles.slice(0, 10).join(", ");
|
|
35
|
+
const more = input.dirtyFiles.length > 10 ? `, and ${String(input.dirtyFiles.length - 10)} more` : "";
|
|
36
|
+
return `${String(input.dirtyFiles.length)} uncommitted file(s): ${names}${more}. Pass force: true to discard them.`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Marks records whose process or directory is gone as orphaned.
|
|
40
|
+
*
|
|
41
|
+
* Nothing is deleted here. A doompi-git orphan owns a directory that may hold
|
|
42
|
+
* uncommitted work, so losing the record would lose the only pointer to it.
|
|
43
|
+
* Deletion is always a separate, named decision.
|
|
44
|
+
*/
|
|
45
|
+
function reconcile(records, probe) {
|
|
46
|
+
const changed = [];
|
|
47
|
+
return {
|
|
48
|
+
records: records.map((record) => {
|
|
49
|
+
if (record.status === "orphaned") return record;
|
|
50
|
+
if (!(!probe.exists(record.path) || !probe.alive(record))) return record;
|
|
51
|
+
changed.push(record.id);
|
|
52
|
+
return {
|
|
53
|
+
...record,
|
|
54
|
+
status: "orphaned"
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
changed
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What a prune would do, decided before anything is destroyed.
|
|
62
|
+
*
|
|
63
|
+
* Separating the plan from the execution is what makes dryRun honest and lets
|
|
64
|
+
* the destructive half be reviewed on its own.
|
|
65
|
+
*/
|
|
66
|
+
function planPrune(input) {
|
|
67
|
+
const plan = {
|
|
68
|
+
remove: [],
|
|
69
|
+
forget: [],
|
|
70
|
+
untracked: [],
|
|
71
|
+
keptDirty: [],
|
|
72
|
+
keptBranches: []
|
|
73
|
+
};
|
|
74
|
+
const known = new Set(input.records.map((record) => record.path));
|
|
75
|
+
const listed = new Set(input.gitPaths);
|
|
76
|
+
for (const record of input.records) {
|
|
77
|
+
if (record.status !== "orphaned") continue;
|
|
78
|
+
if (!listed.has(record.path)) {
|
|
79
|
+
plan.forget.push(record);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if ((input.dirtyByPath.get(record.path) ?? []).length > 0) {
|
|
83
|
+
plan.keptDirty.push(record);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
plan.remove.push(record);
|
|
87
|
+
}
|
|
88
|
+
for (const path of input.gitPaths) if (path.startsWith(`${input.worktreesRoot}/`) && !known.has(path)) plan.untracked.push(path);
|
|
89
|
+
return plan;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { branchSlug, planPrune, reconcile, refuseClose, refuseSpawn, worktreeDirectory };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/worktreeNaming/index.ts"],"sourcesContent":["/**\n * Naming and reconciliation rules for worktrees, as pure functions.\n *\n * Everything here is a function of its arguments so the interesting decisions,\n * which are the ones about destroying work, can be tested without a repository\n * or a filesystem.\n */\n\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\n\nconst MAX_SLUG_LENGTH = 64;\n\n/**\n * A branch name reduced to one safe path segment.\n *\n * Slashes are the point: `wt/fix-auth` is a perfectly good branch and a\n * terrible directory name, because the nested directory it implies is not the\n * one the worktree lives in.\n */\nexport function branchSlug(branch: string): string {\n const slug = branch\n .toLowerCase()\n .replace(/[^a-z0-9]+/gu, '-')\n .replace(/^-+|-+$/gu, '')\n .slice(0, MAX_SLUG_LENGTH)\n .replace(/-+$/gu, '');\n return slug === '' ? 'worktree' : slug;\n}\n\n/**\n * The directory for one worktree.\n *\n * The short id disambiguates two branches whose slugs collide, such as\n * `feature/x` and `feature_x`, which would otherwise want the same directory.\n */\nexport function worktreeDirectory(input: {\n worktreesRoot: string;\n repositoryLabel: string;\n repositoryId: string;\n branch: string;\n shortId: string;\n}): string {\n const repository = `${input.repositoryLabel}--${input.repositoryId}`;\n return `${input.worktreesRoot}/${repository}/${branchSlug(input.branch)}--${input.shortId}`;\n}\n\n/** Why a worktree cannot be created, or undefined when it can. */\nexport function refuseSpawn(input: { branch: string; existing: readonly WorktreeRecord[] }): string | undefined {\n if (input.branch.trim() === '') return 'A branch name is required.';\n // Git refuses a second worktree on one branch, but its message names a path\n // the caller never chose, so this says it in the caller's own terms first.\n const live = input.existing.find((record) => record.branch === input.branch && record.status !== 'orphaned');\n if (live) return `Branch ${input.branch} already has a worktree (${live.id}).`;\n return undefined;\n}\n\n/** Why a worktree cannot be closed, or undefined when it can. */\nexport function refuseClose(input: {\n record: WorktreeRecord | undefined;\n dirtyFiles: readonly string[];\n force: boolean;\n}): string | undefined {\n if (!input.record) return 'No worktree with that id. Call list to see the current ids.';\n if (input.dirtyFiles.length === 0 || input.force) return undefined;\n // Naming the files is the point: \"the tree is dirty\" sends the reader to go\n // look, and the answer is usually a file they forgot rather than one they\n // meant to keep.\n const names = input.dirtyFiles.slice(0, 10).join(', ');\n const more = input.dirtyFiles.length > 10 ? `, and ${String(input.dirtyFiles.length - 10)} more` : '';\n return `${String(input.dirtyFiles.length)} uncommitted file(s): ${names}${more}. Pass force: true to discard them.`;\n}\n\nexport interface Reconciliation {\n records: WorktreeRecord[];\n /** Ids whose status changed, so a caller can report what it noticed. */\n changed: string[];\n}\n\n/**\n * Marks records whose process or directory is gone as orphaned.\n *\n * Nothing is deleted here. A doompi-git orphan owns a directory that may hold\n * uncommitted work, so losing the record would lose the only pointer to it.\n * Deletion is always a separate, named decision.\n */\nexport function reconcile(\n records: readonly WorktreeRecord[],\n probe: { alive: (record: WorktreeRecord) => boolean; exists: (path: string) => boolean },\n): Reconciliation {\n const changed: string[] = [];\n const next = records.map((record) => {\n if (record.status === 'orphaned') return record;\n const gone = !probe.exists(record.path) || !probe.alive(record);\n if (!gone) return record;\n changed.push(record.id);\n return { ...record, status: 'orphaned' as const };\n });\n return { records: next, changed };\n}\n\nexport interface PrunePlan {\n /** Orphans whose directory git still lists, safe to remove. */\n remove: WorktreeRecord[];\n /** Orphans whose directory is already gone; drop the record only. */\n forget: WorktreeRecord[];\n /** Paths git lists under our root that we have no record of; report, never touch. */\n untracked: string[];\n /** Orphans left alone because they still hold uncommitted work. */\n keptDirty: WorktreeRecord[];\n /**\n * Branches git refused to delete because they hold commits.\n *\n * Filled by the prune itself, not by the plan: a dry run destroys nothing, so\n * it has nothing to report here and leaves it empty.\n */\n keptBranches: string[];\n}\n\n/**\n * What a prune would do, decided before anything is destroyed.\n *\n * Separating the plan from the execution is what makes dryRun honest and lets\n * the destructive half be reviewed on its own.\n */\nexport function planPrune(input: {\n records: readonly WorktreeRecord[];\n gitPaths: readonly string[];\n worktreesRoot: string;\n dirtyByPath: ReadonlyMap<string, readonly string[]>;\n}): PrunePlan {\n const plan: PrunePlan = { remove: [], forget: [], untracked: [], keptDirty: [], keptBranches: [] };\n const known = new Set(input.records.map((record) => record.path));\n const listed = new Set(input.gitPaths);\n for (const record of input.records) {\n if (record.status !== 'orphaned') continue;\n if (!listed.has(record.path)) {\n plan.forget.push(record);\n continue;\n }\n if ((input.dirtyByPath.get(record.path) ?? []).length > 0) {\n plan.keptDirty.push(record);\n continue;\n }\n plan.remove.push(record);\n }\n for (const path of input.gitPaths) {\n // Only ever inside our own root: a worktree the user made by hand is none\n // of this package's business, even when it looks abandoned.\n if (path.startsWith(`${input.worktreesRoot}/`) && !known.has(path)) plan.untracked.push(path);\n }\n return plan;\n}\n"],"mappings":";AAUA,MAAM,kBAAkB;;;;;;;;AASxB,SAAgB,WAAW,QAAwB;CACjD,MAAM,OAAO,OACV,YAAY,CAAC,CACb,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,aAAa,EAAE,CAAC,CACxB,MAAM,GAAG,eAAe,CAAC,CACzB,QAAQ,SAAS,EAAE;CACtB,OAAO,SAAS,KAAK,aAAa;AACpC;;;;;;;AAQA,SAAgB,kBAAkB,OAMvB;CACT,MAAM,aAAa,GAAG,MAAM,gBAAgB,IAAI,MAAM;CACtD,OAAO,GAAG,MAAM,cAAc,GAAG,WAAW,GAAG,WAAW,MAAM,MAAM,EAAE,IAAI,MAAM;AACpF;;AAGA,SAAgB,YAAY,OAAoF;CAC9G,IAAI,MAAM,OAAO,KAAK,MAAM,IAAI,OAAO;CAGvC,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW,OAAO,WAAW,MAAM,UAAU,OAAO,WAAW,UAAU;CAC3G,IAAI,MAAM,OAAO,UAAU,MAAM,OAAO,2BAA2B,KAAK,GAAG;AAE7E;;AAGA,SAAgB,YAAY,OAIL;CACrB,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,IAAI,MAAM,WAAW,WAAW,KAAK,MAAM,OAAO,OAAO,KAAA;CAIzD,MAAM,QAAQ,MAAM,WAAW,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,IAAI;CACrD,MAAM,OAAO,MAAM,WAAW,SAAS,KAAK,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,EAAE,SAAS;CACnG,OAAO,GAAG,OAAO,MAAM,WAAW,MAAM,EAAE,wBAAwB,QAAQ,KAAK;AACjF;;;;;;;;AAeA,SAAgB,UACd,SACA,OACgB;CAChB,MAAM,UAAoB,CAAC;CAQ3B,OAAO;EAAE,SAPI,QAAQ,KAAK,WAAW;GACnC,IAAI,OAAO,WAAW,YAAY,OAAO;GAEzC,IAAI,EADS,CAAC,MAAM,OAAO,OAAO,IAAI,KAAK,CAAC,MAAM,MAAM,MAAM,IACnD,OAAO;GAClB,QAAQ,KAAK,OAAO,EAAE;GACtB,OAAO;IAAE,GAAG;IAAQ,QAAQ;GAAoB;EAClD,CACqB;EAAG;CAAQ;AAClC;;;;;;;AA0BA,SAAgB,UAAU,OAKZ;CACZ,MAAM,OAAkB;EAAE,QAAQ,CAAC;EAAG,QAAQ,CAAC;EAAG,WAAW,CAAC;EAAG,WAAW,CAAC;EAAG,cAAc,CAAC;CAAE;CACjG,MAAM,QAAQ,IAAI,IAAI,MAAM,QAAQ,KAAK,WAAW,OAAO,IAAI,CAAC;CAChE,MAAM,SAAS,IAAI,IAAI,MAAM,QAAQ;CACrC,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,OAAO,WAAW,YAAY;EAClC,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,GAAG;GAC5B,KAAK,OAAO,KAAK,MAAM;GACvB;EACF;EACA,KAAK,MAAM,YAAY,IAAI,OAAO,IAAI,KAAK,CAAC,EAAA,CAAG,SAAS,GAAG;GACzD,KAAK,UAAU,KAAK,MAAM;GAC1B;EACF;EACA,KAAK,OAAO,KAAK,MAAM;CACzB;CACA,KAAK,MAAM,QAAQ,MAAM,UAGvB,IAAI,KAAK,WAAW,GAAG,MAAM,cAAc,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI,GAAG,KAAK,UAAU,KAAK,IAAI;CAE9F,OAAO;AACT"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_index = require("../compositionMirror/index.cjs");
|
|
3
|
+
const require_index$1 = require("../errors/index.cjs");
|
|
4
|
+
const require_index$2 = require("../repositoryIdentity/index.cjs");
|
|
5
|
+
const require_index$3 = require("../paths/index.cjs");
|
|
6
|
+
const require_index$4 = require("../worktreeEvents/index.cjs");
|
|
7
|
+
const require_index$5 = require("../worktreeNaming/index.cjs");
|
|
8
|
+
const require_index$6 = require("../worktreeRegistry/index.cjs");
|
|
9
|
+
let node_fs = require("node:fs");
|
|
10
|
+
node_fs = require_runtime.__toESM(node_fs, 1);
|
|
11
|
+
let node_crypto = require("node:crypto");
|
|
12
|
+
//#region src/services/worktreeOperations/index.ts
|
|
13
|
+
const GIT_WORKTREE_DELEGATION_KIND = "git.worktree.delegation";
|
|
14
|
+
const GIT_WORKTREE_MESSAGE_KIND = "git.worktree.message";
|
|
15
|
+
const GIT_WORKTREE_REPORT_KIND = "git.worktree.report";
|
|
16
|
+
function createWorktreeOperations(deps) {
|
|
17
|
+
const { git } = deps;
|
|
18
|
+
const sessionService = deps.sessionService;
|
|
19
|
+
const mirror = deps.mirror ?? require_index.mirrorComposition;
|
|
20
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
21
|
+
const requireSessionService = () => {
|
|
22
|
+
if (sessionService === void 0) throw new require_index$1.HubUnavailableError("The cockpit session service is unavailable. Start the cockpit and try again.");
|
|
23
|
+
return sessionService;
|
|
24
|
+
};
|
|
25
|
+
const requireMessageInbox = () => {
|
|
26
|
+
if (deps.messageInbox === void 0) throw new require_index$1.HubUnavailableError("The Git message service is unavailable. Start the cockpit and try again.");
|
|
27
|
+
return deps.messageInbox;
|
|
28
|
+
};
|
|
29
|
+
const readSessionDelivery = () => deps.sessionDelivery?.();
|
|
30
|
+
const requireSessionDelivery = () => {
|
|
31
|
+
const delivery = readSessionDelivery();
|
|
32
|
+
if (delivery === void 0) throw new require_index$1.HubUnavailableError("The Session delivery service is unavailable. Start the cockpit and try again.");
|
|
33
|
+
return delivery;
|
|
34
|
+
};
|
|
35
|
+
const probe = {
|
|
36
|
+
alive: (record) => requireSessionService().isLive(record.sessionId),
|
|
37
|
+
exists: (path) => node_fs.default.existsSync(path)
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Everything an action needs about the repository it is acting on.
|
|
41
|
+
*
|
|
42
|
+
* Resolved per call rather than cached: a session's cwd is stable, but the
|
|
43
|
+
* worktree registry is shared and another process may have written it since
|
|
44
|
+
* the last call.
|
|
45
|
+
*/
|
|
46
|
+
const resolve = async (context) => {
|
|
47
|
+
const root = await git.repositoryRoot(context.cwd);
|
|
48
|
+
if (root === void 0) throw new require_index$1.DoomGitExpectedError("not_a_repository", `${context.cwd} is not inside a git repository.`, false, "Run this from a repository checkout.");
|
|
49
|
+
const store = require_index$6.createWorktreeRegistry(require_index$3.registryFile(root, deps.homeDir));
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
store,
|
|
53
|
+
records: store.list()
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const require$1 = (records, id) => {
|
|
57
|
+
const record = records.find((entry) => entry.id === id);
|
|
58
|
+
if (record === void 0) throw new require_index$1.DoomGitExpectedError("worktree_not_found", `No worktree with id ${id}.`, false, "Call list to see the current ids.");
|
|
59
|
+
return record;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Refuses to destroy or merge a worktree another live session is using.
|
|
63
|
+
*
|
|
64
|
+
* `parentSessionId` records who asked for the worktree. Two sessions in one
|
|
65
|
+
* checkout share this registry, so without the check either could close the
|
|
66
|
+
* other's work in progress by id.
|
|
67
|
+
*
|
|
68
|
+
* Once that session is gone there is no owner left to protect, and refusing
|
|
69
|
+
* would strand the worktree: nothing else could ever close it.
|
|
70
|
+
*/
|
|
71
|
+
const requireOwner = (context, record, action) => {
|
|
72
|
+
if (record.parentSessionId === context.sessionId) return;
|
|
73
|
+
if (!requireSessionService().isLive(record.parentSessionId)) return;
|
|
74
|
+
throw new require_index$1.DoomGitExpectedError("worktree_not_owned", `Worktree ${record.id} belongs to another session.`, false, `Ask the session that created it to ${action} it, or close that session first.`);
|
|
75
|
+
};
|
|
76
|
+
const peerFor = (context, record) => {
|
|
77
|
+
if (context.sessionId === record.parentSessionId) return {
|
|
78
|
+
sessionId: record.sessionId,
|
|
79
|
+
party: "parent"
|
|
80
|
+
};
|
|
81
|
+
if (context.sessionId === record.sessionId) return {
|
|
82
|
+
sessionId: record.parentSessionId,
|
|
83
|
+
party: "child"
|
|
84
|
+
};
|
|
85
|
+
throw new require_index$1.DoomGitExpectedError("worktree_not_owned", `Worktree ${record.id} belongs to another session.`, false, "Use a worktree owned by this session.");
|
|
86
|
+
};
|
|
87
|
+
const requirePeerLive = (peerSessionId) => {
|
|
88
|
+
if (!requireSessionService().isLive(peerSessionId)) throw new require_index$1.DoomGitExpectedError("worktree_peer_unavailable", "The other worktree session is no longer live.", false, "Start a new worktree session before sending a message.");
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
async spawn(context, request, options) {
|
|
92
|
+
const { root, store, records } = await resolve(context);
|
|
93
|
+
const refusal = require_index$5.refuseSpawn({
|
|
94
|
+
branch: request.branch,
|
|
95
|
+
existing: records
|
|
96
|
+
});
|
|
97
|
+
if (refusal !== void 0) throw new require_index$1.DoomGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
|
|
98
|
+
const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
|
|
99
|
+
const baseRef = request.baseRef ?? await git.currentBranch(root) ?? "HEAD";
|
|
100
|
+
const id = require_index$2.shortId((0, node_crypto.randomUUID)());
|
|
101
|
+
const path = require_index$5.worktreeDirectory({
|
|
102
|
+
worktreesRoot: require_index$3.worktreesRoot(deps.homeDir),
|
|
103
|
+
repositoryLabel: require_index$2.repositoryLabel(root),
|
|
104
|
+
repositoryId: require_index$2.repositoryId(root),
|
|
105
|
+
branch: request.branch,
|
|
106
|
+
shortId: id
|
|
107
|
+
});
|
|
108
|
+
const cancelled = () => options?.signal?.aborted === true;
|
|
109
|
+
const rollback = async () => {
|
|
110
|
+
await git.removeWorktree({
|
|
111
|
+
repositoryRoot: root,
|
|
112
|
+
path,
|
|
113
|
+
force: true
|
|
114
|
+
}).catch(() => void 0);
|
|
115
|
+
return await git.deleteBranch({
|
|
116
|
+
repositoryRoot: root,
|
|
117
|
+
branch: request.branch
|
|
118
|
+
}).catch(() => false) ? "" : ` The branch ${request.branch} has work on it and was kept.`;
|
|
119
|
+
};
|
|
120
|
+
options?.onProgress?.(`creating branch ${request.branch}\u2026`);
|
|
121
|
+
await git.addWorktree({
|
|
122
|
+
repositoryRoot: root,
|
|
123
|
+
path,
|
|
124
|
+
branch: request.branch,
|
|
125
|
+
baseRef
|
|
126
|
+
});
|
|
127
|
+
if (cancelled()) {
|
|
128
|
+
const kept = await rollback();
|
|
129
|
+
throw new require_index$1.DoomGitExpectedError("spawn_cancelled", `Cancelled before the session started.${kept}`, false, "The worktree was removed; run it again when you are ready.");
|
|
130
|
+
}
|
|
131
|
+
options?.onProgress?.("mirroring build output…");
|
|
132
|
+
mirror(root, path);
|
|
133
|
+
options?.onProgress?.("starting session…");
|
|
134
|
+
let sessionId;
|
|
135
|
+
try {
|
|
136
|
+
sessionId = (await requireSessionService().create({
|
|
137
|
+
cwd: path,
|
|
138
|
+
name: request.name ?? request.branch,
|
|
139
|
+
parentSessionId: context.sessionId,
|
|
140
|
+
sessionProvenance: "worktree",
|
|
141
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
142
|
+
})).sessionId;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const kept = await rollback();
|
|
145
|
+
if (cancelled()) throw new require_index$1.DoomGitExpectedError("spawn_cancelled", `Cancelled while the session was starting.${kept}`, false, "The worktree was removed; run it again when you are ready.");
|
|
146
|
+
if (error instanceof require_index$1.HubUnavailableError) throw new require_index$1.DoomGitExpectedError("hub_unavailable", `${error.message}${kept}`, true, "Start the cockpit and try again.");
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const record = {
|
|
150
|
+
version: 1,
|
|
151
|
+
id,
|
|
152
|
+
branch: request.branch,
|
|
153
|
+
baseRef,
|
|
154
|
+
path,
|
|
155
|
+
repositoryRoot: root,
|
|
156
|
+
sessionId,
|
|
157
|
+
parentSessionId: context.sessionId,
|
|
158
|
+
status: "running",
|
|
159
|
+
createdAt: now().toISOString()
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
store.replace([...records, record]);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
await requireSessionService().close(sessionId).catch(() => void 0);
|
|
165
|
+
const kept = await rollback();
|
|
166
|
+
throw new require_index$1.DoomGitExpectedError("registry_write_failed", `The worktree was created but the registry could not be written (${error.message}).${kept}`, true, "Check the registry file for permissions or free space, then try again.");
|
|
167
|
+
}
|
|
168
|
+
if (request.task !== void 0 && taskDelivery !== void 0) try {
|
|
169
|
+
const receipt = await taskDelivery.deliver({
|
|
170
|
+
recipientKey: sessionId,
|
|
171
|
+
kind: GIT_WORKTREE_DELEGATION_KIND,
|
|
172
|
+
prompt: request.task,
|
|
173
|
+
metadata: {
|
|
174
|
+
worktreeId: record.id,
|
|
175
|
+
fromRole: "parent",
|
|
176
|
+
text: request.task,
|
|
177
|
+
sentAt: record.createdAt
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const admission = await taskDelivery.waitForAdmission(receipt.deliveryId);
|
|
181
|
+
if (admission !== "admitted") throw new Error(admission === "recovery_required" ? "The recipient could not admit the task." : "The recipient did not confirm task admission before the timeout.");
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new require_index$1.DoomGitExpectedError("task_delivery_failed", `Worktree ${record.id} was created, but its task admission was not confirmed (${error.message}).`, true, `Use send with worktree ${record.id} to deliver the task without recreating the worktree.`);
|
|
184
|
+
}
|
|
185
|
+
return record;
|
|
186
|
+
},
|
|
187
|
+
async close(context, id, force) {
|
|
188
|
+
const { root, store, records } = await resolve(context);
|
|
189
|
+
const record = require$1(records, id);
|
|
190
|
+
requireOwner(context, record, "close");
|
|
191
|
+
const dirtyFiles = node_fs.default.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];
|
|
192
|
+
const refusal = require_index$5.refuseClose({
|
|
193
|
+
record,
|
|
194
|
+
dirtyFiles,
|
|
195
|
+
force
|
|
196
|
+
});
|
|
197
|
+
if (refusal !== void 0) throw new require_index$1.DoomGitExpectedError("worktree_dirty", refusal, false, "Commit the work, or pass force: true.");
|
|
198
|
+
await requireSessionService().close(record.sessionId);
|
|
199
|
+
await git.removeWorktree({
|
|
200
|
+
repositoryRoot: root,
|
|
201
|
+
path: record.path,
|
|
202
|
+
force
|
|
203
|
+
});
|
|
204
|
+
store.replace(records.filter((entry) => entry.id !== id));
|
|
205
|
+
return record;
|
|
206
|
+
},
|
|
207
|
+
async list(context) {
|
|
208
|
+
const { store, records } = await resolve(context);
|
|
209
|
+
const outcome = require_index$5.reconcile(records, probe);
|
|
210
|
+
if (outcome.changed.length > 0) store.replace(outcome.records);
|
|
211
|
+
return outcome.records;
|
|
212
|
+
},
|
|
213
|
+
async status(context, id) {
|
|
214
|
+
const { records } = await resolve(context);
|
|
215
|
+
const record = require$1(records, id);
|
|
216
|
+
return {
|
|
217
|
+
record,
|
|
218
|
+
dirtyFiles: node_fs.default.existsSync(record.path) ? await git.dirtyFiles(record.path) : []
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
async merge(context, id, message) {
|
|
222
|
+
const { root, records } = await resolve(context);
|
|
223
|
+
const record = require$1(records, id);
|
|
224
|
+
requireOwner(context, record, "merge");
|
|
225
|
+
const parentDirty = await git.dirtyFiles(root);
|
|
226
|
+
if (parentDirty.length > 0) throw new require_index$1.DoomGitExpectedError("worktree_dirty", `The parent checkout has ${String(parentDirty.length)} uncommitted file(s): ${parentDirty.slice(0, 10).join(", ")}.`, false, "Commit or stash the parent checkout first.");
|
|
227
|
+
await git.mergeBranch({
|
|
228
|
+
repositoryRoot: root,
|
|
229
|
+
branch: record.branch,
|
|
230
|
+
...message === void 0 ? {} : { message }
|
|
231
|
+
});
|
|
232
|
+
return record;
|
|
233
|
+
},
|
|
234
|
+
/** Sends only between the parent session and its own worktree session. */
|
|
235
|
+
async send(context, id, message) {
|
|
236
|
+
if (Buffer.byteLength(message, "utf8") > 65536) throw new require_index$1.DoomGitExpectedError("message_too_large", `A message may not exceed ${String(require_index$4.MAX_WORKTREE_MESSAGE_BYTES)} bytes.`, false, "Send a shorter message.");
|
|
237
|
+
const { records } = await resolve(context);
|
|
238
|
+
const record = require$1(records, id);
|
|
239
|
+
const peer = peerFor(context, record);
|
|
240
|
+
requirePeerLive(peer.sessionId);
|
|
241
|
+
const sentAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
242
|
+
const delivery = readSessionDelivery();
|
|
243
|
+
if (delivery !== void 0) {
|
|
244
|
+
const kind = peer.party === "parent" ? GIT_WORKTREE_MESSAGE_KIND : GIT_WORKTREE_REPORT_KIND;
|
|
245
|
+
const receipt = await delivery.deliver({
|
|
246
|
+
recipientKey: peer.sessionId,
|
|
247
|
+
kind,
|
|
248
|
+
prompt: peer.party === "parent" ? `Parent message for worktree ${record.id}:\n\n${message}` : `Report from worktree ${record.id}:\n\n${message}`,
|
|
249
|
+
metadata: {
|
|
250
|
+
worktreeId: record.id,
|
|
251
|
+
fromRole: peer.party,
|
|
252
|
+
text: message,
|
|
253
|
+
sentAt
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const admission = await delivery.waitForAdmission(receipt.deliveryId);
|
|
257
|
+
if (admission !== "admitted") throw new require_index$1.DoomGitExpectedError("message_delivery_failed", `Worktree ${record.id} persisted the message, but its session did not confirm admission.`, true, admission === "recovery_required" ? "Call messages from the recipient session to recover the persisted text." : "The message remains durable; retry after the recipient is ready.");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
requireMessageInbox().send(peer.sessionId, {
|
|
261
|
+
version: 1,
|
|
262
|
+
worktreeId: record.id,
|
|
263
|
+
fromSessionId: context.sessionId,
|
|
264
|
+
from: peer.party,
|
|
265
|
+
text: message,
|
|
266
|
+
sentAt
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
async messages(context, id) {
|
|
270
|
+
const { records } = await resolve(context);
|
|
271
|
+
const record = require$1(records, id);
|
|
272
|
+
const peer = peerFor(context, record);
|
|
273
|
+
const delivery = readSessionDelivery();
|
|
274
|
+
if (delivery === void 0) return requireMessageInbox().receive(record.id);
|
|
275
|
+
return delivery.inbox({
|
|
276
|
+
consumed: false,
|
|
277
|
+
metadata: { worktreeId: record.id }
|
|
278
|
+
}).filter((entry) => entry.senderKey === peer.sessionId && (entry.state === "admitted" || entry.state === "recovery_required") && (peer.party === "parent" ? entry.kind === GIT_WORKTREE_REPORT_KIND : entry.kind === GIT_WORKTREE_MESSAGE_KIND || entry.kind === GIT_WORKTREE_DELEGATION_KIND)).map((entry) => {
|
|
279
|
+
delivery.consume(entry.deliveryId);
|
|
280
|
+
return {
|
|
281
|
+
version: 1,
|
|
282
|
+
worktreeId: record.id,
|
|
283
|
+
fromSessionId: entry.senderKey,
|
|
284
|
+
from: peer.party === "parent" ? "child" : "parent",
|
|
285
|
+
text: entry.state === "recovery_required" ? `[Recovery required: prompt admission was not confirmed] ${entry.metadata.text ?? entry.prompt}` : entry.metadata.text ?? entry.prompt,
|
|
286
|
+
sentAt: entry.metadata.sentAt ?? ""
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
async prune(context, dryRun) {
|
|
291
|
+
const { root, store, records } = await resolve(context);
|
|
292
|
+
const reconciled = require_index$5.reconcile(records, probe);
|
|
293
|
+
const gitPaths = await git.listWorktreePaths(root);
|
|
294
|
+
const dirtyByPath = /* @__PURE__ */ new Map();
|
|
295
|
+
for (const record of reconciled.records) if (record.status === "orphaned" && node_fs.default.existsSync(record.path)) dirtyByPath.set(record.path, await git.dirtyFiles(record.path));
|
|
296
|
+
const plan = require_index$5.planPrune({
|
|
297
|
+
records: reconciled.records,
|
|
298
|
+
gitPaths,
|
|
299
|
+
worktreesRoot: require_index$3.worktreesRoot(deps.homeDir),
|
|
300
|
+
dirtyByPath
|
|
301
|
+
});
|
|
302
|
+
if (dryRun) {
|
|
303
|
+
store.replace(reconciled.records);
|
|
304
|
+
return plan;
|
|
305
|
+
}
|
|
306
|
+
for (const record of plan.remove) await git.removeWorktree({
|
|
307
|
+
repositoryRoot: root,
|
|
308
|
+
path: record.path,
|
|
309
|
+
force: false
|
|
310
|
+
});
|
|
311
|
+
for (const record of [...plan.remove, ...plan.forget]) if (!await git.deleteBranch({
|
|
312
|
+
repositoryRoot: root,
|
|
313
|
+
branch: record.branch
|
|
314
|
+
}).catch(() => false)) plan.keptBranches.push(record.branch);
|
|
315
|
+
const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));
|
|
316
|
+
store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
|
|
317
|
+
return plan;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
exports.createWorktreeOperations = createWorktreeOperations;
|
|
323
|
+
|
|
324
|
+
//# sourceMappingURL=index.cjs.map
|