@thanaen/worktree-cleanup 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cleanup-BQ4ci49S.mjs +424 -0
- package/dist/cleanup-BQ4ci49S.mjs.map +1 -0
- package/dist/index.d.mts +100 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/main.d.mts +2 -0
- package/dist/main.mjs +44 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +14 -12
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Console, Context, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
|
|
3
|
+
import { Prompt } from "effect/unstable/cli";
|
|
4
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
5
|
+
//#region src/domain.ts
|
|
6
|
+
const skipReasons = [
|
|
7
|
+
"not-registered",
|
|
8
|
+
"symlink",
|
|
9
|
+
"main-worktree",
|
|
10
|
+
"current-worktree",
|
|
11
|
+
"locked",
|
|
12
|
+
"dirty",
|
|
13
|
+
"base-ref-unknown",
|
|
14
|
+
"not-merged",
|
|
15
|
+
"git-error",
|
|
16
|
+
"outside-root"
|
|
17
|
+
];
|
|
18
|
+
const finishRecord = (records, current) => {
|
|
19
|
+
if (current.path === void 0) return;
|
|
20
|
+
records.push({
|
|
21
|
+
path: current.path,
|
|
22
|
+
...current.head === void 0 ? {} : { head: current.head },
|
|
23
|
+
...current.branch === void 0 ? {} : { branch: current.branch },
|
|
24
|
+
detached: current.detached,
|
|
25
|
+
...current.lockedReason === void 0 ? {} : { lockedReason: current.lockedReason },
|
|
26
|
+
...current.prunableReason === void 0 ? {} : { prunableReason: current.prunableReason }
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** Parse `git worktree list --porcelain -z` without interpreting path bytes as shell text. */
|
|
30
|
+
const parseWorktreePorcelain = (input) => {
|
|
31
|
+
const records = [];
|
|
32
|
+
let current = { detached: false };
|
|
33
|
+
for (const field of input.split("\0")) {
|
|
34
|
+
if (field.length === 0) {
|
|
35
|
+
finishRecord(records, current);
|
|
36
|
+
current = { detached: false };
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const separator = field.indexOf(" ");
|
|
40
|
+
const key = separator === -1 ? field : field.slice(0, separator);
|
|
41
|
+
const value = separator === -1 ? "" : field.slice(separator + 1);
|
|
42
|
+
if (key === "worktree") {
|
|
43
|
+
if (current.path !== void 0) {
|
|
44
|
+
finishRecord(records, current);
|
|
45
|
+
current = { detached: false };
|
|
46
|
+
}
|
|
47
|
+
current.path = value;
|
|
48
|
+
} else if (key === "HEAD") current.head = value;
|
|
49
|
+
else if (key === "branch") current.branch = value;
|
|
50
|
+
else if (key === "detached") current.detached = true;
|
|
51
|
+
else if (key === "locked") current.lockedReason = value.length === 0 ? "locked" : value;
|
|
52
|
+
else if (key === "prunable") current.prunableReason = value.length === 0 ? "prunable" : value;
|
|
53
|
+
}
|
|
54
|
+
finishRecord(records, current);
|
|
55
|
+
return records.map((record, index) => ({
|
|
56
|
+
...record,
|
|
57
|
+
isMain: index === 0
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
60
|
+
const skipReasonLabel = {
|
|
61
|
+
"not-registered": "not a registered Git worktree",
|
|
62
|
+
symlink: "symlinked directories are never removed",
|
|
63
|
+
"main-worktree": "repository main worktree",
|
|
64
|
+
"current-worktree": "worktree running this command",
|
|
65
|
+
locked: "worktree is locked",
|
|
66
|
+
dirty: "worktree has tracked or untracked changes",
|
|
67
|
+
"base-ref-unknown": "could not determine a trusted base branch",
|
|
68
|
+
"not-merged": "HEAD is not integrated into the base branch",
|
|
69
|
+
"git-error": "Git state could not be proven",
|
|
70
|
+
"outside-root": "canonical path is outside the selected root"
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/errors.ts
|
|
74
|
+
var GitExecutionError = class extends Schema.TaggedError()("GitExecutionError", {
|
|
75
|
+
operation: Schema.String,
|
|
76
|
+
message: Schema.String,
|
|
77
|
+
cause: Schema.Defect()
|
|
78
|
+
}) {};
|
|
79
|
+
var InputError = class extends Schema.TaggedError()("InputError", {
|
|
80
|
+
message: Schema.String,
|
|
81
|
+
exitCode: Schema.Int
|
|
82
|
+
}) {};
|
|
83
|
+
var DiscoveryError = class extends Schema.TaggedError()("DiscoveryError", {
|
|
84
|
+
path: Schema.String,
|
|
85
|
+
message: Schema.String,
|
|
86
|
+
cause: Schema.Defect()
|
|
87
|
+
}) {};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/git.ts
|
|
90
|
+
var Git = class Git extends Context.Service()("@thanaen/worktree-cleanup/Git") {
|
|
91
|
+
static layer = Layer.effect(Git, Effect.gen(function* () {
|
|
92
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
93
|
+
const run = Effect.fn("Git.run")(function* (cwd, args) {
|
|
94
|
+
const operation = `git ${args.join(" ")}`;
|
|
95
|
+
const handle = yield* spawner.spawn(ChildProcess.make("git", [...args], { cwd })).pipe(Effect.mapError((cause) => new GitExecutionError({
|
|
96
|
+
operation,
|
|
97
|
+
message: `Could not start Git in ${cwd}`,
|
|
98
|
+
cause
|
|
99
|
+
})));
|
|
100
|
+
const [stdout, stderr, exitCode] = yield* Effect.all([
|
|
101
|
+
Stream.mkString(Stream.decodeText(handle.stdout)),
|
|
102
|
+
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
103
|
+
handle.exitCode
|
|
104
|
+
], { concurrency: "unbounded" }).pipe(Effect.mapError((cause) => new GitExecutionError({
|
|
105
|
+
operation,
|
|
106
|
+
message: `Git execution failed in ${cwd}`,
|
|
107
|
+
cause
|
|
108
|
+
})));
|
|
109
|
+
return {
|
|
110
|
+
stdout,
|
|
111
|
+
stderr,
|
|
112
|
+
exitCode: Number(exitCode)
|
|
113
|
+
};
|
|
114
|
+
}, Effect.scoped);
|
|
115
|
+
return Git.of({ run });
|
|
116
|
+
}));
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/cleanup.ts
|
|
120
|
+
const smartRoots = [
|
|
121
|
+
{
|
|
122
|
+
relative: "worktrees",
|
|
123
|
+
source: "worktrees"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
relative: ".claude/worktrees",
|
|
127
|
+
source: "claude"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
relative: ".codex/worktrees",
|
|
131
|
+
source: "codex"
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
const trim = (value) => value.trim();
|
|
135
|
+
const pathKey = (pathService, value) => {
|
|
136
|
+
const normalized = pathService.normalize(value);
|
|
137
|
+
return pathService.sep === "\\" ? normalized.toLowerCase() : normalized;
|
|
138
|
+
};
|
|
139
|
+
const discoveryFailure = (path, message) => (cause) => new DiscoveryError({
|
|
140
|
+
path,
|
|
141
|
+
message,
|
|
142
|
+
cause
|
|
143
|
+
});
|
|
144
|
+
const discoverRoots = Effect.fn("discoverRoots")(function* (cwd, explicitDirectory) {
|
|
145
|
+
const fs = yield* FileSystem.FileSystem;
|
|
146
|
+
const path = yield* Path.Path;
|
|
147
|
+
if (Option.isSome(explicitDirectory)) {
|
|
148
|
+
const requested = path.resolve(cwd, explicitDirectory.value);
|
|
149
|
+
if (!(yield* fs.exists(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect --dir"))))) return yield* new InputError({
|
|
150
|
+
message: `--dir does not exist: ${requested}`,
|
|
151
|
+
exitCode: 2
|
|
152
|
+
});
|
|
153
|
+
if ((yield* fs.stat(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect --dir")))).type !== "Directory") return yield* new InputError({
|
|
154
|
+
message: `--dir is not a directory: ${requested}`,
|
|
155
|
+
exitCode: 2
|
|
156
|
+
});
|
|
157
|
+
return [{
|
|
158
|
+
path: yield* fs.realPath(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not resolve --dir"))),
|
|
159
|
+
source: "explicit"
|
|
160
|
+
}];
|
|
161
|
+
}
|
|
162
|
+
const roots = [];
|
|
163
|
+
for (const entry of smartRoots) {
|
|
164
|
+
const requested = path.resolve(cwd, entry.relative);
|
|
165
|
+
if (!(yield* fs.exists(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect smart default"))))) continue;
|
|
166
|
+
if ((yield* fs.stat(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect smart default")))).type !== "Directory") continue;
|
|
167
|
+
const canonical = yield* fs.realPath(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not resolve smart default")));
|
|
168
|
+
roots.push({
|
|
169
|
+
path: canonical,
|
|
170
|
+
source: entry.source
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return roots;
|
|
174
|
+
});
|
|
175
|
+
const enumerateCandidates = Effect.fn("enumerateCandidates")(function* (roots) {
|
|
176
|
+
const fs = yield* FileSystem.FileSystem;
|
|
177
|
+
const path = yield* Path.Path;
|
|
178
|
+
const candidates = [];
|
|
179
|
+
for (const root of roots) {
|
|
180
|
+
const names = yield* fs.readDirectory(root.path).pipe(Effect.mapError(discoveryFailure(root.path, "Could not list worktree root")));
|
|
181
|
+
for (const name of names.toSorted()) {
|
|
182
|
+
const requested = path.resolve(root.path, name);
|
|
183
|
+
if ((yield* fs.stat(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect candidate")))).type !== "Directory") continue;
|
|
184
|
+
const canonical = yield* fs.realPath(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not resolve candidate")));
|
|
185
|
+
const normalizedRequested = path.normalize(requested);
|
|
186
|
+
if (canonical !== normalizedRequested) {
|
|
187
|
+
candidates.push({
|
|
188
|
+
path: normalizedRequested,
|
|
189
|
+
root,
|
|
190
|
+
structuralSkip: {
|
|
191
|
+
reason: path.dirname(canonical) === root.path ? "symlink" : "outside-root",
|
|
192
|
+
detail: `resolves to ${canonical}`
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (path.dirname(canonical) !== root.path) {
|
|
198
|
+
candidates.push({
|
|
199
|
+
path: canonical,
|
|
200
|
+
root,
|
|
201
|
+
structuralSkip: {
|
|
202
|
+
reason: "outside-root",
|
|
203
|
+
detail: `canonical parent is ${path.dirname(canonical)}`
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
candidates.push({
|
|
209
|
+
path: canonical,
|
|
210
|
+
root
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const unique = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const candidate of candidates) unique.set(candidate.path, candidate);
|
|
216
|
+
return [...unique.values()];
|
|
217
|
+
});
|
|
218
|
+
const findCurrentWorktree = Effect.fn("findCurrentWorktree")(function* (cwd) {
|
|
219
|
+
const git = yield* Git;
|
|
220
|
+
const fs = yield* FileSystem.FileSystem;
|
|
221
|
+
const result = yield* git.run(cwd, ["rev-parse", "--show-toplevel"]);
|
|
222
|
+
if (result.exitCode !== 0) return void 0;
|
|
223
|
+
return yield* fs.realPath(trim(result.stdout)).pipe(Effect.orElseSucceed(() => trim(result.stdout)));
|
|
224
|
+
});
|
|
225
|
+
const detectBaseRef = Effect.fn("detectBaseRef")(function* (repositoryPath, mainWorktree) {
|
|
226
|
+
const git = yield* Git;
|
|
227
|
+
const originHead = yield* git.run(repositoryPath, [
|
|
228
|
+
"symbolic-ref",
|
|
229
|
+
"--quiet",
|
|
230
|
+
"refs/remotes/origin/HEAD"
|
|
231
|
+
]);
|
|
232
|
+
if (originHead.exitCode === 0 && trim(originHead.stdout).length > 0) return trim(originHead.stdout);
|
|
233
|
+
for (const ref of ["refs/heads/main", "refs/heads/master"]) if ((yield* git.run(repositoryPath, [
|
|
234
|
+
"show-ref",
|
|
235
|
+
"--verify",
|
|
236
|
+
"--quiet",
|
|
237
|
+
ref
|
|
238
|
+
])).exitCode === 0) return ref;
|
|
239
|
+
if (mainWorktree.branch !== void 0) {
|
|
240
|
+
if ((yield* git.run(repositoryPath, [
|
|
241
|
+
"show-ref",
|
|
242
|
+
"--verify",
|
|
243
|
+
"--quiet",
|
|
244
|
+
mainWorktree.branch
|
|
245
|
+
])).exitCode === 0) return mainWorktree.branch;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
const skipped = (candidate, reason, detail, context) => ({
|
|
249
|
+
candidate,
|
|
250
|
+
status: "skipped",
|
|
251
|
+
...reason === void 0 ? {} : { reason },
|
|
252
|
+
...detail === void 0 ? {} : { detail },
|
|
253
|
+
...context?.repositoryPath === void 0 ? {} : { repositoryPath: context.repositoryPath },
|
|
254
|
+
...context?.worktree === void 0 ? {} : { worktree: context.worktree },
|
|
255
|
+
...context?.baseRef === void 0 ? {} : { baseRef: context.baseRef }
|
|
256
|
+
});
|
|
257
|
+
const assessCandidate = Effect.fn("assessCandidate")(function* (candidate, currentWorktree) {
|
|
258
|
+
const git = yield* Git;
|
|
259
|
+
const fs = yield* FileSystem.FileSystem;
|
|
260
|
+
const path = yield* Path.Path;
|
|
261
|
+
if (candidate.structuralSkip !== void 0) return skipped(candidate, candidate.structuralSkip.reason, candidate.structuralSkip.detail);
|
|
262
|
+
const repository = yield* git.run(candidate.path, [
|
|
263
|
+
"rev-parse",
|
|
264
|
+
"--path-format=absolute",
|
|
265
|
+
"--git-common-dir"
|
|
266
|
+
]);
|
|
267
|
+
if (repository.exitCode !== 0) return skipped(candidate, "not-registered", trim(repository.stderr));
|
|
268
|
+
const listResult = yield* git.run(candidate.path, [
|
|
269
|
+
"worktree",
|
|
270
|
+
"list",
|
|
271
|
+
"--porcelain",
|
|
272
|
+
"-z"
|
|
273
|
+
]);
|
|
274
|
+
if (listResult.exitCode !== 0) return skipped(candidate, "git-error", trim(listResult.stderr));
|
|
275
|
+
const worktrees = parseWorktreePorcelain(listResult.stdout);
|
|
276
|
+
const mainWorktree = worktrees[0];
|
|
277
|
+
if (mainWorktree === void 0) return skipped(candidate, "git-error", "Git returned an empty worktree list");
|
|
278
|
+
const topLevel = yield* git.run(candidate.path, [
|
|
279
|
+
"rev-parse",
|
|
280
|
+
"--path-format=absolute",
|
|
281
|
+
"--show-toplevel"
|
|
282
|
+
]);
|
|
283
|
+
if (topLevel.exitCode !== 0) return skipped(candidate, "git-error", trim(topLevel.stderr));
|
|
284
|
+
const gitCandidatePath = pathKey(path, trim(topLevel.stdout));
|
|
285
|
+
const registered = worktrees.find((entry) => pathKey(path, entry.path) === gitCandidatePath);
|
|
286
|
+
if (registered === void 0) return skipped(candidate, "not-registered");
|
|
287
|
+
const worktree = {
|
|
288
|
+
...registered,
|
|
289
|
+
path: candidate.path
|
|
290
|
+
};
|
|
291
|
+
const canonicalMain = {
|
|
292
|
+
...mainWorktree,
|
|
293
|
+
path: yield* fs.realPath(mainWorktree.path).pipe(Effect.orElseSucceed(() => path.normalize(mainWorktree.path)))
|
|
294
|
+
};
|
|
295
|
+
const repositoryPath = canonicalMain.path;
|
|
296
|
+
const context = {
|
|
297
|
+
repositoryPath,
|
|
298
|
+
worktree
|
|
299
|
+
};
|
|
300
|
+
if (worktree.isMain) return skipped(candidate, "main-worktree", void 0, context);
|
|
301
|
+
if (currentWorktree === candidate.path) return skipped(candidate, "current-worktree", void 0, context);
|
|
302
|
+
if (worktree.lockedReason !== void 0) return skipped(candidate, "locked", worktree.lockedReason, context);
|
|
303
|
+
if (worktree.head === void 0) return skipped(candidate, "git-error", "Git did not report a HEAD", context);
|
|
304
|
+
const status = yield* git.run(candidate.path, [
|
|
305
|
+
"status",
|
|
306
|
+
"--porcelain",
|
|
307
|
+
"--untracked-files=all"
|
|
308
|
+
]);
|
|
309
|
+
if (status.exitCode !== 0) return skipped(candidate, "git-error", trim(status.stderr), context);
|
|
310
|
+
if (status.stdout.length > 0) return skipped(candidate, "dirty", void 0, context);
|
|
311
|
+
const baseRef = yield* detectBaseRef(repositoryPath, canonicalMain);
|
|
312
|
+
if (baseRef === void 0) return skipped(candidate, "base-ref-unknown", void 0, context);
|
|
313
|
+
const merged = yield* git.run(repositoryPath, [
|
|
314
|
+
"merge-base",
|
|
315
|
+
"--is-ancestor",
|
|
316
|
+
worktree.head,
|
|
317
|
+
baseRef
|
|
318
|
+
]);
|
|
319
|
+
if (merged.exitCode === 1) return skipped(candidate, "not-merged", `base: ${baseRef}`, {
|
|
320
|
+
...context,
|
|
321
|
+
baseRef
|
|
322
|
+
});
|
|
323
|
+
if (merged.exitCode !== 0) return skipped(candidate, "git-error", trim(merged.stderr), {
|
|
324
|
+
...context,
|
|
325
|
+
baseRef
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
candidate,
|
|
329
|
+
repositoryPath,
|
|
330
|
+
worktree,
|
|
331
|
+
baseRef,
|
|
332
|
+
status: "removable"
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
const renderPlan = Effect.fn("renderPlan")(function* (roots, assessments) {
|
|
336
|
+
yield* Console.log("Worktree cleanup plan");
|
|
337
|
+
yield* Console.log("Roots:");
|
|
338
|
+
for (const root of roots) yield* Console.log(` - ${root.path} (${root.source})`);
|
|
339
|
+
const removable = assessments.filter((assessment) => assessment.status === "removable");
|
|
340
|
+
const skippedItems = assessments.filter((assessment) => assessment.status === "skipped");
|
|
341
|
+
yield* Console.log(`Removable (${removable.length}):`);
|
|
342
|
+
if (removable.length === 0) yield* Console.log(" - none");
|
|
343
|
+
for (const assessment of removable) yield* Console.log(` REMOVE ${assessment.candidate.path} [${assessment.baseRef}]`);
|
|
344
|
+
yield* Console.log(`Skipped (${skippedItems.length}):`);
|
|
345
|
+
if (skippedItems.length === 0) yield* Console.log(" - none");
|
|
346
|
+
for (const assessment of skippedItems) {
|
|
347
|
+
const reason = assessment.reason === void 0 ? "unknown" : skipReasonLabel[assessment.reason];
|
|
348
|
+
const detail = assessment.detail === void 0 || assessment.detail.length === 0 ? "" : `: ${assessment.detail}`;
|
|
349
|
+
yield* Console.log(` skip ${assessment.candidate.path} — ${reason}${detail}`);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const runCleanup = Effect.fn("runCleanup")(function* (options) {
|
|
353
|
+
const git = yield* Git;
|
|
354
|
+
const roots = yield* discoverRoots(options.cwd, options.directory);
|
|
355
|
+
if (roots.length === 0) {
|
|
356
|
+
yield* Console.log("No worktree roots found (checked worktrees, .claude/worktrees, .codex/worktrees).");
|
|
357
|
+
return {
|
|
358
|
+
removed: [],
|
|
359
|
+
revalidationSkipped: [],
|
|
360
|
+
failures: []
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const candidates = yield* enumerateCandidates(roots);
|
|
364
|
+
const currentWorktree = yield* findCurrentWorktree(options.cwd);
|
|
365
|
+
const assessments = yield* Effect.forEach(candidates, (candidate) => assessCandidate(candidate, currentWorktree), { concurrency: 4 });
|
|
366
|
+
yield* renderPlan(roots, assessments);
|
|
367
|
+
const removable = assessments.filter((assessment) => assessment.status === "removable" && assessment.repositoryPath !== void 0);
|
|
368
|
+
if (removable.length === 0) {
|
|
369
|
+
yield* Console.log("Nothing to remove.");
|
|
370
|
+
return {
|
|
371
|
+
removed: [],
|
|
372
|
+
revalidationSkipped: [],
|
|
373
|
+
failures: []
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (!options.yes && !options.interactive) return yield* new InputError({
|
|
377
|
+
message: "Refusing to delete without an interactive terminal. Pass --yes to approve.",
|
|
378
|
+
exitCode: 2
|
|
379
|
+
});
|
|
380
|
+
if (!(options.yes ? true : yield* Prompt.run(Prompt.confirm({
|
|
381
|
+
message: `Remove ${removable.length} stale worktree${removable.length === 1 ? "" : "s"}?`,
|
|
382
|
+
initial: false
|
|
383
|
+
})).pipe(Effect.orElseSucceed(() => false)))) {
|
|
384
|
+
yield* Console.log("Cleanup cancelled; nothing was removed.");
|
|
385
|
+
return {
|
|
386
|
+
removed: [],
|
|
387
|
+
revalidationSkipped: [],
|
|
388
|
+
failures: []
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const removed = [];
|
|
392
|
+
const revalidationSkipped = [];
|
|
393
|
+
const failures = [];
|
|
394
|
+
for (const planned of removable) {
|
|
395
|
+
const revalidated = yield* assessCandidate(planned.candidate, currentWorktree);
|
|
396
|
+
if (revalidated.status !== "removable" || revalidated.repositoryPath === void 0) {
|
|
397
|
+
revalidationSkipped.push(revalidated);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const removal = yield* git.run(revalidated.repositoryPath, [
|
|
401
|
+
"worktree",
|
|
402
|
+
"remove",
|
|
403
|
+
"--",
|
|
404
|
+
revalidated.candidate.path
|
|
405
|
+
]);
|
|
406
|
+
if (removal.exitCode === 0) removed.push(revalidated.candidate.path);
|
|
407
|
+
else failures.push({
|
|
408
|
+
path: revalidated.candidate.path,
|
|
409
|
+
message: trim(removal.stderr) || `git exited with ${removal.exitCode}`
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
yield* Console.log(`Cleanup complete: ${removed.length} removed, ${revalidationSkipped.length} skipped after revalidation, ${failures.length} failed.`);
|
|
413
|
+
for (const failure of failures) yield* Console.error(`Failed ${failure.path}: ${failure.message}`);
|
|
414
|
+
if (failures.length > 0) process.exitCode = 1;
|
|
415
|
+
return {
|
|
416
|
+
removed,
|
|
417
|
+
revalidationSkipped,
|
|
418
|
+
failures
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
//#endregion
|
|
422
|
+
export { skipReasons as a, skipReasonLabel as i, Git as n, parseWorktreePorcelain as r, runCleanup as t };
|
|
423
|
+
|
|
424
|
+
//# sourceMappingURL=cleanup-BQ4ci49S.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup-BQ4ci49S.mjs","names":[],"sources":["../src/domain.ts","../src/errors.ts","../src/git.ts","../src/cleanup.ts"],"sourcesContent":["export const skipReasons = [\n \"not-registered\",\n \"symlink\",\n \"main-worktree\",\n \"current-worktree\",\n \"locked\",\n \"dirty\",\n \"base-ref-unknown\",\n \"not-merged\",\n \"git-error\",\n \"outside-root\"\n] as const\n\nexport type SkipReason = (typeof skipReasons)[number]\n\nexport interface TargetRoot {\n readonly path: string\n readonly source: \"explicit\" | \"worktrees\" | \"claude\" | \"codex\"\n}\n\nexport interface RegisteredWorktree {\n readonly path: string\n readonly head?: string\n readonly branch?: string\n readonly detached: boolean\n readonly lockedReason?: string\n readonly prunableReason?: string\n readonly isMain: boolean\n}\n\nexport interface Candidate {\n readonly path: string\n readonly root: TargetRoot\n readonly structuralSkip?: {\n readonly reason: \"symlink\" | \"outside-root\"\n readonly detail: string\n }\n}\n\nexport interface Assessment {\n readonly candidate: Candidate\n readonly repositoryPath?: string\n readonly worktree?: RegisteredWorktree\n readonly baseRef?: string\n readonly status: \"removable\" | \"skipped\"\n readonly reason?: SkipReason\n readonly detail?: string\n}\n\nexport interface CleanupResult {\n readonly removed: ReadonlyArray<string>\n readonly revalidationSkipped: ReadonlyArray<Assessment>\n readonly failures: ReadonlyArray<{\n readonly path: string\n readonly message: string\n }>\n}\n\ninterface MutableWorktree {\n path?: string\n head?: string\n branch?: string\n detached: boolean\n lockedReason?: string\n prunableReason?: string\n}\n\nconst finishRecord = (\n records: Array<Omit<RegisteredWorktree, \"isMain\">>,\n current: MutableWorktree\n): void => {\n if (current.path === undefined) return\n records.push({\n path: current.path,\n ...(current.head === undefined ? {} : { head: current.head }),\n ...(current.branch === undefined ? {} : { branch: current.branch }),\n detached: current.detached,\n ...(current.lockedReason === undefined ? {} : { lockedReason: current.lockedReason }),\n ...(current.prunableReason === undefined ? {} : { prunableReason: current.prunableReason })\n })\n}\n\n/** Parse `git worktree list --porcelain -z` without interpreting path bytes as shell text. */\nexport const parseWorktreePorcelain = (input: string): ReadonlyArray<RegisteredWorktree> => {\n const records: Array<Omit<RegisteredWorktree, \"isMain\">> = []\n let current: MutableWorktree = { detached: false }\n\n for (const field of input.split(\"\\0\")) {\n if (field.length === 0) {\n finishRecord(records, current)\n current = { detached: false }\n continue\n }\n\n const separator = field.indexOf(\" \")\n const key = separator === -1 ? field : field.slice(0, separator)\n const value = separator === -1 ? \"\" : field.slice(separator + 1)\n\n if (key === \"worktree\") {\n if (current.path !== undefined) {\n finishRecord(records, current)\n current = { detached: false }\n }\n current.path = value\n } else if (key === \"HEAD\") {\n current.head = value\n } else if (key === \"branch\") {\n current.branch = value\n } else if (key === \"detached\") {\n current.detached = true\n } else if (key === \"locked\") {\n current.lockedReason = value.length === 0 ? \"locked\" : value\n } else if (key === \"prunable\") {\n current.prunableReason = value.length === 0 ? \"prunable\" : value\n }\n }\n\n finishRecord(records, current)\n return records.map((record, index) => ({ ...record, isMain: index === 0 }))\n}\n\nexport const skipReasonLabel: Readonly<Record<SkipReason, string>> = {\n \"not-registered\": \"not a registered Git worktree\",\n symlink: \"symlinked directories are never removed\",\n \"main-worktree\": \"repository main worktree\",\n \"current-worktree\": \"worktree running this command\",\n locked: \"worktree is locked\",\n dirty: \"worktree has tracked or untracked changes\",\n \"base-ref-unknown\": \"could not determine a trusted base branch\",\n \"not-merged\": \"HEAD is not integrated into the base branch\",\n \"git-error\": \"Git state could not be proven\",\n \"outside-root\": \"canonical path is outside the selected root\"\n}\n","import { Schema } from \"effect\"\n\nexport class GitExecutionError extends Schema.TaggedError<GitExecutionError>()(\n \"GitExecutionError\",\n {\n operation: Schema.String,\n message: Schema.String,\n cause: Schema.Defect()\n }\n) {}\n\nexport class InputError extends Schema.TaggedError<InputError>()(\"InputError\", {\n message: Schema.String,\n exitCode: Schema.Int\n}) {}\n\nexport class DiscoveryError extends Schema.TaggedError<DiscoveryError>()(\"DiscoveryError\", {\n path: Schema.String,\n message: Schema.String,\n cause: Schema.Defect()\n}) {}\n\nexport type AppError = GitExecutionError | InputError | DiscoveryError\n","import { Context, Effect, Layer, Stream } from \"effect\"\nimport { ChildProcess, ChildProcessSpawner } from \"effect/unstable/process\"\n\nimport { GitExecutionError } from \"./errors.js\"\n\nexport interface GitResult {\n readonly exitCode: number\n readonly stdout: string\n readonly stderr: string\n}\n\nexport interface GitService {\n readonly run: (\n cwd: string,\n args: ReadonlyArray<string>\n ) => Effect.Effect<GitResult, GitExecutionError>\n}\n\nexport class Git extends Context.Service<Git, GitService>()(\"@thanaen/worktree-cleanup/Git\") {\n static readonly layer = Layer.effect(\n Git,\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n\n const run = Effect.fn(\"Git.run\")(function* (cwd: string, args: ReadonlyArray<string>) {\n const operation = `git ${args.join(\" \")}`\n const handle = yield* spawner.spawn(ChildProcess.make(\"git\", [...args], { cwd })).pipe(\n Effect.mapError(\n (cause) =>\n new GitExecutionError({\n operation,\n message: `Could not start Git in ${cwd}`,\n cause\n })\n )\n )\n\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n Stream.mkString(Stream.decodeText(handle.stdout)),\n Stream.mkString(Stream.decodeText(handle.stderr)),\n handle.exitCode\n ] as const,\n { concurrency: \"unbounded\" }\n ).pipe(\n Effect.mapError(\n (cause) =>\n new GitExecutionError({\n operation,\n message: `Git execution failed in ${cwd}`,\n cause\n })\n )\n )\n\n return {\n stdout,\n stderr,\n exitCode: Number(exitCode)\n }\n }, Effect.scoped)\n\n return Git.of({ run })\n })\n )\n}\n","import { Console, Effect, FileSystem, Option, Path } from \"effect\"\nimport { Prompt } from \"effect/unstable/cli\"\n\nimport type {\n Assessment,\n Candidate,\n CleanupResult,\n RegisteredWorktree,\n TargetRoot\n} from \"./domain.js\"\nimport { parseWorktreePorcelain, skipReasonLabel } from \"./domain.js\"\nimport { DiscoveryError, InputError } from \"./errors.js\"\nimport { Git } from \"./git.js\"\n\nconst smartRoots = [\n { relative: \"worktrees\", source: \"worktrees\" },\n { relative: \".claude/worktrees\", source: \"claude\" },\n { relative: \".codex/worktrees\", source: \"codex\" }\n] as const\n\nconst trim = (value: string): string => value.trim()\n\nconst pathKey = (pathService: Path.Path, value: string): string => {\n const normalized = pathService.normalize(value)\n return pathService.sep === \"\\\\\" ? normalized.toLowerCase() : normalized\n}\n\nconst discoveryFailure = (path: string, message: string) => (cause: unknown) =>\n new DiscoveryError({ path, message, cause })\n\nexport const discoverRoots = Effect.fn(\"discoverRoots\")(function* (\n cwd: string,\n explicitDirectory: Option.Option<string>\n) {\n const fs = yield* FileSystem.FileSystem\n const path = yield* Path.Path\n\n if (Option.isSome(explicitDirectory)) {\n const requested = path.resolve(cwd, explicitDirectory.value)\n const exists = yield* fs\n .exists(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect --dir\")))\n if (!exists) {\n return yield* new InputError({\n message: `--dir does not exist: ${requested}`,\n exitCode: 2\n })\n }\n const info = yield* fs\n .stat(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect --dir\")))\n if (info.type !== \"Directory\") {\n return yield* new InputError({\n message: `--dir is not a directory: ${requested}`,\n exitCode: 2\n })\n }\n const canonical = yield* fs\n .realPath(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not resolve --dir\")))\n return [{ path: canonical, source: \"explicit\" }] satisfies ReadonlyArray<TargetRoot>\n }\n\n const roots: Array<TargetRoot> = []\n for (const entry of smartRoots) {\n const requested = path.resolve(cwd, entry.relative)\n const exists = yield* fs\n .exists(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect smart default\")))\n if (!exists) continue\n const info = yield* fs\n .stat(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect smart default\")))\n if (info.type !== \"Directory\") continue\n const canonical = yield* fs\n .realPath(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not resolve smart default\")))\n roots.push({ path: canonical, source: entry.source })\n }\n\n return roots\n})\n\nexport const enumerateCandidates = Effect.fn(\"enumerateCandidates\")(function* (\n roots: ReadonlyArray<TargetRoot>\n) {\n const fs = yield* FileSystem.FileSystem\n const path = yield* Path.Path\n const candidates: Array<Candidate> = []\n\n for (const root of roots) {\n const names = yield* fs\n .readDirectory(root.path)\n .pipe(Effect.mapError(discoveryFailure(root.path, \"Could not list worktree root\")))\n\n for (const name of names.toSorted()) {\n const requested = path.resolve(root.path, name)\n const info = yield* fs\n .stat(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect candidate\")))\n if (info.type !== \"Directory\") continue\n\n const canonical = yield* fs\n .realPath(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not resolve candidate\")))\n const normalizedRequested = path.normalize(requested)\n\n if (canonical !== normalizedRequested) {\n candidates.push({\n path: normalizedRequested,\n root,\n structuralSkip: {\n reason: path.dirname(canonical) === root.path ? \"symlink\" : \"outside-root\",\n detail: `resolves to ${canonical}`\n }\n })\n continue\n }\n if (path.dirname(canonical) !== root.path) {\n candidates.push({\n path: canonical,\n root,\n structuralSkip: {\n reason: \"outside-root\",\n detail: `canonical parent is ${path.dirname(canonical)}`\n }\n })\n continue\n }\n candidates.push({ path: canonical, root })\n }\n }\n\n const unique = new Map<string, Candidate>()\n for (const candidate of candidates) unique.set(candidate.path, candidate)\n return [...unique.values()]\n})\n\nconst findCurrentWorktree = Effect.fn(\"findCurrentWorktree\")(function* (cwd: string) {\n const git = yield* Git\n const fs = yield* FileSystem.FileSystem\n const result = yield* git.run(cwd, [\"rev-parse\", \"--show-toplevel\"])\n if (result.exitCode !== 0) return undefined\n return yield* fs\n .realPath(trim(result.stdout))\n .pipe(Effect.orElseSucceed(() => trim(result.stdout)))\n})\n\nconst detectBaseRef = Effect.fn(\"detectBaseRef\")(function* (\n repositoryPath: string,\n mainWorktree: RegisteredWorktree\n) {\n const git = yield* Git\n const originHead = yield* git.run(repositoryPath, [\n \"symbolic-ref\",\n \"--quiet\",\n \"refs/remotes/origin/HEAD\"\n ])\n if (originHead.exitCode === 0 && trim(originHead.stdout).length > 0)\n return trim(originHead.stdout)\n\n for (const ref of [\"refs/heads/main\", \"refs/heads/master\"]) {\n const exists = yield* git.run(repositoryPath, [\"show-ref\", \"--verify\", \"--quiet\", ref])\n if (exists.exitCode === 0) return ref\n }\n\n if (mainWorktree.branch !== undefined) {\n const exists = yield* git.run(repositoryPath, [\n \"show-ref\",\n \"--verify\",\n \"--quiet\",\n mainWorktree.branch\n ])\n if (exists.exitCode === 0) return mainWorktree.branch\n }\n return undefined\n})\n\nconst skipped = (\n candidate: Candidate,\n reason: Assessment[\"reason\"],\n detail?: string,\n context?: {\n readonly repositoryPath?: string\n readonly worktree?: RegisteredWorktree\n readonly baseRef?: string\n }\n): Assessment => ({\n candidate,\n status: \"skipped\",\n ...(reason === undefined ? {} : { reason }),\n ...(detail === undefined ? {} : { detail }),\n ...(context?.repositoryPath === undefined ? {} : { repositoryPath: context.repositoryPath }),\n ...(context?.worktree === undefined ? {} : { worktree: context.worktree }),\n ...(context?.baseRef === undefined ? {} : { baseRef: context.baseRef })\n})\n\nexport const assessCandidate = Effect.fn(\"assessCandidate\")(function* (\n candidate: Candidate,\n currentWorktree: string | undefined\n) {\n const git = yield* Git\n const fs = yield* FileSystem.FileSystem\n const path = yield* Path.Path\n\n if (candidate.structuralSkip !== undefined) {\n return skipped(candidate, candidate.structuralSkip.reason, candidate.structuralSkip.detail)\n }\n\n const repository = yield* git.run(candidate.path, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--git-common-dir\"\n ])\n if (repository.exitCode !== 0) {\n return skipped(candidate, \"not-registered\", trim(repository.stderr))\n }\n\n const listResult = yield* git.run(candidate.path, [\"worktree\", \"list\", \"--porcelain\", \"-z\"])\n if (listResult.exitCode !== 0) {\n return skipped(candidate, \"git-error\", trim(listResult.stderr))\n }\n const worktrees = parseWorktreePorcelain(listResult.stdout)\n const mainWorktree = worktrees[0]\n if (mainWorktree === undefined) {\n return skipped(candidate, \"git-error\", \"Git returned an empty worktree list\")\n }\n\n const topLevel = yield* git.run(candidate.path, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--show-toplevel\"\n ])\n if (topLevel.exitCode !== 0) {\n return skipped(candidate, \"git-error\", trim(topLevel.stderr))\n }\n\n // Git for Windows can spell the same path using either its long form or an\n // 8.3 component (for example `runneradmin` versus `RUNNER~1`). Match the\n // path reported by Git from inside the candidate against Git's own list.\n const gitCandidatePath = pathKey(path, trim(topLevel.stdout))\n const registered = worktrees.find((entry) => pathKey(path, entry.path) === gitCandidatePath)\n if (registered === undefined) {\n return skipped(candidate, \"not-registered\")\n }\n const worktree = { ...registered, path: candidate.path }\n const canonicalMain = {\n ...mainWorktree,\n path: yield* fs\n .realPath(mainWorktree.path)\n .pipe(Effect.orElseSucceed(() => path.normalize(mainWorktree.path)))\n }\n const repositoryPath = canonicalMain.path\n const context = { repositoryPath, worktree }\n\n if (worktree.isMain) return skipped(candidate, \"main-worktree\", undefined, context)\n if (currentWorktree === candidate.path) {\n return skipped(candidate, \"current-worktree\", undefined, context)\n }\n if (worktree.lockedReason !== undefined) {\n return skipped(candidate, \"locked\", worktree.lockedReason, context)\n }\n if (worktree.head === undefined) {\n return skipped(candidate, \"git-error\", \"Git did not report a HEAD\", context)\n }\n\n const status = yield* git.run(candidate.path, [\"status\", \"--porcelain\", \"--untracked-files=all\"])\n if (status.exitCode !== 0) return skipped(candidate, \"git-error\", trim(status.stderr), context)\n if (status.stdout.length > 0) return skipped(candidate, \"dirty\", undefined, context)\n\n const baseRef = yield* detectBaseRef(repositoryPath, canonicalMain)\n if (baseRef === undefined) return skipped(candidate, \"base-ref-unknown\", undefined, context)\n\n const merged = yield* git.run(repositoryPath, [\n \"merge-base\",\n \"--is-ancestor\",\n worktree.head,\n baseRef\n ])\n if (merged.exitCode === 1) {\n return skipped(candidate, \"not-merged\", `base: ${baseRef}`, { ...context, baseRef })\n }\n if (merged.exitCode !== 0) {\n return skipped(candidate, \"git-error\", trim(merged.stderr), { ...context, baseRef })\n }\n\n return {\n candidate,\n repositoryPath,\n worktree,\n baseRef,\n status: \"removable\" as const\n }\n})\n\nconst renderPlan = Effect.fn(\"renderPlan\")(function* (\n roots: ReadonlyArray<TargetRoot>,\n assessments: ReadonlyArray<Assessment>\n) {\n yield* Console.log(\"Worktree cleanup plan\")\n yield* Console.log(\"Roots:\")\n for (const root of roots) yield* Console.log(` - ${root.path} (${root.source})`)\n\n const removable = assessments.filter((assessment) => assessment.status === \"removable\")\n const skippedItems = assessments.filter((assessment) => assessment.status === \"skipped\")\n\n yield* Console.log(`Removable (${removable.length}):`)\n if (removable.length === 0) yield* Console.log(\" - none\")\n for (const assessment of removable) {\n yield* Console.log(` REMOVE ${assessment.candidate.path} [${assessment.baseRef}]`)\n }\n\n yield* Console.log(`Skipped (${skippedItems.length}):`)\n if (skippedItems.length === 0) yield* Console.log(\" - none\")\n for (const assessment of skippedItems) {\n const reason = assessment.reason === undefined ? \"unknown\" : skipReasonLabel[assessment.reason]\n const detail =\n assessment.detail === undefined || assessment.detail.length === 0\n ? \"\"\n : `: ${assessment.detail}`\n yield* Console.log(` skip ${assessment.candidate.path} — ${reason}${detail}`)\n }\n})\n\nexport interface CleanupOptions {\n readonly cwd: string\n readonly directory: Option.Option<string>\n readonly yes: boolean\n readonly interactive: boolean\n}\n\nexport const runCleanup = Effect.fn(\"runCleanup\")(function* (options: CleanupOptions) {\n const git = yield* Git\n const roots = yield* discoverRoots(options.cwd, options.directory)\n if (roots.length === 0) {\n yield* Console.log(\n \"No worktree roots found (checked worktrees, .claude/worktrees, .codex/worktrees).\"\n )\n return {\n removed: [],\n revalidationSkipped: [],\n failures: []\n } satisfies CleanupResult\n }\n\n const candidates = yield* enumerateCandidates(roots)\n const currentWorktree = yield* findCurrentWorktree(options.cwd)\n const assessments = yield* Effect.forEach(\n candidates,\n (candidate) => assessCandidate(candidate, currentWorktree),\n { concurrency: 4 }\n )\n yield* renderPlan(roots, assessments)\n\n const removable = assessments.filter(\n (\n assessment\n ): assessment is Assessment & {\n readonly repositoryPath: string\n readonly status: \"removable\"\n } => assessment.status === \"removable\" && assessment.repositoryPath !== undefined\n )\n if (removable.length === 0) {\n yield* Console.log(\"Nothing to remove.\")\n return {\n removed: [],\n revalidationSkipped: [],\n failures: []\n } satisfies CleanupResult\n }\n\n if (!options.yes && !options.interactive) {\n return yield* new InputError({\n message: \"Refusing to delete without an interactive terminal. Pass --yes to approve.\",\n exitCode: 2\n })\n }\n\n const confirmed = options.yes\n ? true\n : yield* Prompt.run(\n Prompt.confirm({\n message: `Remove ${removable.length} stale worktree${removable.length === 1 ? \"\" : \"s\"}?`,\n initial: false\n })\n ).pipe(Effect.orElseSucceed(() => false))\n\n if (!confirmed) {\n yield* Console.log(\"Cleanup cancelled; nothing was removed.\")\n return {\n removed: [],\n revalidationSkipped: [],\n failures: []\n } satisfies CleanupResult\n }\n\n const removed: Array<string> = []\n const revalidationSkipped: Array<Assessment> = []\n const failures: Array<{ path: string; message: string }> = []\n\n for (const planned of removable) {\n const revalidated = yield* assessCandidate(planned.candidate, currentWorktree)\n if (revalidated.status !== \"removable\" || revalidated.repositoryPath === undefined) {\n revalidationSkipped.push(revalidated)\n continue\n }\n\n const removal = yield* git.run(revalidated.repositoryPath, [\n \"worktree\",\n \"remove\",\n \"--\",\n revalidated.candidate.path\n ])\n if (removal.exitCode === 0) {\n removed.push(revalidated.candidate.path)\n } else {\n failures.push({\n path: revalidated.candidate.path,\n message: trim(removal.stderr) || `git exited with ${removal.exitCode}`\n })\n }\n }\n\n yield* Console.log(\n `Cleanup complete: ${removed.length} removed, ${revalidationSkipped.length} skipped after revalidation, ${failures.length} failed.`\n )\n for (const failure of failures) yield* Console.error(`Failed ${failure.path}: ${failure.message}`)\n\n if (failures.length > 0) {\n process.exitCode = 1\n }\n return { removed, revalidationSkipped, failures } satisfies CleanupResult\n})\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
3
|
+
import { Prompt } from "effect/unstable/cli";
|
|
4
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
5
|
+
//#region src/errors.d.ts
|
|
6
|
+
declare const GitExecutionError_base: Schema.Class<GitExecutionError, Schema.TaggedStruct<"GitExecutionError", {
|
|
7
|
+
readonly operation: Schema.String;
|
|
8
|
+
readonly message: Schema.String;
|
|
9
|
+
readonly cause: Schema.Defect;
|
|
10
|
+
}>, import("effect/Cause").YieldableError>;
|
|
11
|
+
declare class GitExecutionError extends GitExecutionError_base {}
|
|
12
|
+
declare const InputError_base: Schema.Class<InputError, Schema.TaggedStruct<"InputError", {
|
|
13
|
+
readonly message: Schema.String;
|
|
14
|
+
readonly exitCode: Schema.Int;
|
|
15
|
+
}>, import("effect/Cause").YieldableError>;
|
|
16
|
+
declare class InputError extends InputError_base {}
|
|
17
|
+
declare const DiscoveryError_base: Schema.Class<DiscoveryError, Schema.TaggedStruct<"DiscoveryError", {
|
|
18
|
+
readonly path: Schema.String;
|
|
19
|
+
readonly message: Schema.String;
|
|
20
|
+
readonly cause: Schema.Defect;
|
|
21
|
+
}>, import("effect/Cause").YieldableError>;
|
|
22
|
+
declare class DiscoveryError extends DiscoveryError_base {}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/domain.d.ts
|
|
25
|
+
declare const skipReasons: readonly ["not-registered", "symlink", "main-worktree", "current-worktree", "locked", "dirty", "base-ref-unknown", "not-merged", "git-error", "outside-root"];
|
|
26
|
+
type SkipReason = (typeof skipReasons)[number];
|
|
27
|
+
interface TargetRoot {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly source: "explicit" | "worktrees" | "claude" | "codex";
|
|
30
|
+
}
|
|
31
|
+
interface RegisteredWorktree {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly head?: string;
|
|
34
|
+
readonly branch?: string;
|
|
35
|
+
readonly detached: boolean;
|
|
36
|
+
readonly lockedReason?: string;
|
|
37
|
+
readonly prunableReason?: string;
|
|
38
|
+
readonly isMain: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface Candidate {
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly root: TargetRoot;
|
|
43
|
+
readonly structuralSkip?: {
|
|
44
|
+
readonly reason: "symlink" | "outside-root";
|
|
45
|
+
readonly detail: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
interface Assessment {
|
|
49
|
+
readonly candidate: Candidate;
|
|
50
|
+
readonly repositoryPath?: string;
|
|
51
|
+
readonly worktree?: RegisteredWorktree;
|
|
52
|
+
readonly baseRef?: string;
|
|
53
|
+
readonly status: "removable" | "skipped";
|
|
54
|
+
readonly reason?: SkipReason;
|
|
55
|
+
readonly detail?: string;
|
|
56
|
+
}
|
|
57
|
+
interface CleanupResult {
|
|
58
|
+
readonly removed: ReadonlyArray<string>;
|
|
59
|
+
readonly revalidationSkipped: ReadonlyArray<Assessment>;
|
|
60
|
+
readonly failures: ReadonlyArray<{
|
|
61
|
+
readonly path: string;
|
|
62
|
+
readonly message: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/** Parse `git worktree list --porcelain -z` without interpreting path bytes as shell text. */
|
|
66
|
+
declare const parseWorktreePorcelain: (input: string) => ReadonlyArray<RegisteredWorktree>;
|
|
67
|
+
declare const skipReasonLabel: Readonly<Record<SkipReason, string>>;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/git.d.ts
|
|
70
|
+
interface GitResult {
|
|
71
|
+
readonly exitCode: number;
|
|
72
|
+
readonly stdout: string;
|
|
73
|
+
readonly stderr: string;
|
|
74
|
+
}
|
|
75
|
+
interface GitService {
|
|
76
|
+
readonly run: (cwd: string, args: ReadonlyArray<string>) => Effect.Effect<GitResult, GitExecutionError>;
|
|
77
|
+
}
|
|
78
|
+
declare const Git_base: Context.ServiceClass<Git, "@thanaen/worktree-cleanup/Git", GitService>;
|
|
79
|
+
declare class Git extends Git_base {
|
|
80
|
+
static readonly layer: Layer.Layer<Git, never, ChildProcessSpawner.ChildProcessSpawner>;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/cleanup.d.ts
|
|
84
|
+
interface CleanupOptions {
|
|
85
|
+
readonly cwd: string;
|
|
86
|
+
readonly directory: Option.Option<string>;
|
|
87
|
+
readonly yes: boolean;
|
|
88
|
+
readonly interactive: boolean;
|
|
89
|
+
}
|
|
90
|
+
declare const runCleanup: (options: CleanupOptions) => Effect.Effect<{
|
|
91
|
+
removed: string[];
|
|
92
|
+
revalidationSkipped: Assessment[];
|
|
93
|
+
failures: {
|
|
94
|
+
path: string;
|
|
95
|
+
message: string;
|
|
96
|
+
}[];
|
|
97
|
+
}, DiscoveryError | GitExecutionError | InputError, Git | Prompt.Environment>;
|
|
98
|
+
//#endregion
|
|
99
|
+
export { type Assessment, type Candidate, type CleanupOptions, type CleanupResult, Git, type GitResult, type GitService, type RegisteredWorktree, type SkipReason, type TargetRoot, parseWorktreePorcelain, runCleanup, skipReasonLabel, skipReasons };
|
|
100
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/domain.ts","../src/git.ts","../src/cleanup.ts"],"mappings":";;;;;;;;;;cAEa,0BAA0B;;;;;cAS1B,mBAAmB;;;;;;cAKnB,uBAAuB;;;cChBvB;KAaD,qBAAqB;UAEhB;WACN;WACA;;UAGM;WACN;WACA;WACA;WACA;WACA;WACA;WACA;;UAGM;WACN;WACA,MAAM;WACN;aACE;aACA;;;UAII;WACN,WAAW;WACX;WACA,WAAW;WACX;WACA;WACA,SAAS;WACT;;UAGM;WACN,SAAS;WACT,qBAAqB,cAAc;WACnC,UAAU;aACR;aACA;;;;cA6BA,yBAAsB,kBAAoB,cAAc;cAsCxD,iBAAiB,SAAS,OAAO;;;UCpH7B;WACN;WACA;WACA;;UAGM;WACN,MACP,aACA,MAAM,0BACH,OAAO,OAAO,WAAW;;;cAGnB,YAAY;kBACP,OAAK,MAAA,MAAA,YAAA,oBAAA;;;;UCiTN;WACN;WACA,WAAW,OAAO;WAClB;WACA;;cAGE,aAAU,SAAA,mBAAA,OAAA;;;;IAmES;IAAiB;;GAkC/C,iBAAA,oBAAA,YAAA,MAAA,OAAA"}
|
package/dist/index.mjs
ADDED
package/dist/main.d.mts
ADDED
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as Git, t as runCleanup } from "./cleanup-BQ4ci49S.mjs";
|
|
3
|
+
import { Console, Effect, Layer } from "effect";
|
|
4
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
5
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
6
|
+
//#region package.json
|
|
7
|
+
var version = "0.1.1";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/cli.ts
|
|
10
|
+
const directory = Flag.string("dir").pipe(Flag.withDescription("Inspect exactly this worktree root and ignore smart defaults"), Flag.optional);
|
|
11
|
+
const yes = Flag.boolean("yes").pipe(Flag.withAlias("y"), Flag.withDescription("Approve the displayed cleanup plan without prompting"));
|
|
12
|
+
const handleError = (error) => {
|
|
13
|
+
const candidate = error;
|
|
14
|
+
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
15
|
+
const exitCode = typeof candidate.exitCode === "number" ? candidate.exitCode : 1;
|
|
16
|
+
return Console.error(`Error: ${message}`).pipe(Effect.andThen(Effect.sync(() => {
|
|
17
|
+
process.exitCode = exitCode;
|
|
18
|
+
})));
|
|
19
|
+
};
|
|
20
|
+
const program = Command.make("worktree-cleanup", {
|
|
21
|
+
directory,
|
|
22
|
+
yes
|
|
23
|
+
}, Effect.fn("worktree-cleanup")(function* ({ directory, yes }) {
|
|
24
|
+
yield* runCleanup({
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
directory,
|
|
27
|
+
yes,
|
|
28
|
+
interactive: process.stdin.isTTY === true
|
|
29
|
+
}).pipe(Effect.catch(handleError));
|
|
30
|
+
})).pipe(Command.withAlias("worktree-clean"), Command.withDescription("Safely remove clean Git worktrees already integrated into the base branch"), Command.withExamples([{
|
|
31
|
+
command: "worktree-cleanup",
|
|
32
|
+
description: "Inspect smart-default worktree roots and ask before deletion"
|
|
33
|
+
}, {
|
|
34
|
+
command: "worktree-cleanup --dir ../worktrees --yes",
|
|
35
|
+
description: "Clean one explicit root non-interactively"
|
|
36
|
+
}])).pipe(Command.run({ version }));
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/main.ts
|
|
39
|
+
const MainLayer = Git.layer.pipe(Layer.provideMerge(NodeServices.layer));
|
|
40
|
+
program.pipe(Effect.provide(MainLayer), NodeRuntime.runMain);
|
|
41
|
+
//#endregion
|
|
42
|
+
export {};
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=main.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.mjs","names":["packageJson.version"],"sources":["../package.json","../src/cli.ts","../src/main.ts"],"sourcesContent":["","import { Console, Effect } from \"effect\"\nimport { Command, Flag } from \"effect/unstable/cli\"\n\nimport packageJson from \"../package.json\" with { type: \"json\" }\nimport { runCleanup } from \"./cleanup.js\"\n\nconst directory = Flag.string(\"dir\").pipe(\n Flag.withDescription(\"Inspect exactly this worktree root and ignore smart defaults\"),\n Flag.optional\n)\n\nconst yes = Flag.boolean(\"yes\").pipe(\n Flag.withAlias(\"y\"),\n Flag.withDescription(\"Approve the displayed cleanup plan without prompting\")\n)\n\nconst handleError = (error: unknown) => {\n const candidate = error as { readonly message?: unknown; readonly exitCode?: unknown }\n const message = typeof candidate.message === \"string\" ? candidate.message : String(error)\n const exitCode = typeof candidate.exitCode === \"number\" ? candidate.exitCode : 1\n return Console.error(`Error: ${message}`).pipe(\n Effect.andThen(\n Effect.sync(() => {\n process.exitCode = exitCode\n })\n )\n )\n}\n\nexport const command = Command.make(\n \"worktree-cleanup\",\n { directory, yes },\n Effect.fn(\"worktree-cleanup\")(function* ({ directory, yes }) {\n yield* runCleanup({\n cwd: process.cwd(),\n directory,\n yes,\n interactive: process.stdin.isTTY === true\n }).pipe(Effect.catch(handleError))\n })\n).pipe(\n Command.withAlias(\"worktree-clean\"),\n Command.withDescription(\n \"Safely remove clean Git worktrees already integrated into the base branch\"\n ),\n Command.withExamples([\n {\n command: \"worktree-cleanup\",\n description: \"Inspect smart-default worktree roots and ask before deletion\"\n },\n {\n command: \"worktree-cleanup --dir ../worktrees --yes\",\n description: \"Clean one explicit root non-interactively\"\n }\n ])\n)\n\nexport const program = command.pipe(Command.run({ version: packageJson.version }))\n","import { NodeRuntime, NodeServices } from \"@effect/platform-node\"\nimport { Effect, Layer } from \"effect\"\n\nimport { program } from \"./cli.js\"\nimport { Git } from \"./git.js\"\n\nconst MainLayer = Git.layer.pipe(Layer.provideMerge(NodeServices.layer))\n\nprogram.pipe(Effect.provide(MainLayer), NodeRuntime.runMain)\n"],"mappings":";;;;;;;;;ACMA,MAAM,YAAY,KAAK,OAAO,KAAK,CAAC,CAAC,KACnC,KAAK,gBAAgB,8DAA8D,GACnF,KAAK,QACP;AAEA,MAAM,MAAM,KAAK,QAAQ,KAAK,CAAC,CAAC,KAC9B,KAAK,UAAU,GAAG,GAClB,KAAK,gBAAgB,sDAAsD,CAC7E;AAEA,MAAM,eAAe,UAAmB;CACtC,MAAM,YAAY;CAClB,MAAM,UAAU,OAAO,UAAU,YAAY,WAAW,UAAU,UAAU,OAAO,KAAK;CACxF,MAAM,WAAW,OAAO,UAAU,aAAa,WAAW,UAAU,WAAW;CAC/E,OAAO,QAAQ,MAAM,UAAU,SAAS,CAAC,CAAC,KACxC,OAAO,QACL,OAAO,WAAW;EAChB,QAAQ,WAAW;CACrB,CAAC,CACH,CACF;AACF;AA8BA,MAAa,UA5BU,QAAQ,KAC7B,oBACA;CAAE;CAAW;AAAI,GACjB,OAAO,GAAG,kBAAkB,CAAC,CAAC,WAAW,EAAE,WAAW,OAAO;CAC3D,OAAO,WAAW;EAChB,KAAK,QAAQ,IAAI;EACjB;EACA;EACA,aAAa,QAAQ,MAAM,UAAU;CACvC,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,WAAW,CAAC;AACnC,CAAC,CACH,CAAC,CAAC,KACA,QAAQ,UAAU,gBAAgB,GAClC,QAAQ,gBACN,2EACF,GACA,QAAQ,aAAa,CACnB;CACE,SAAS;CACT,aAAa;AACf,GACA;CACE,SAAS;CACT,aAAa;AACf,CACF,CAAC,CAGoB,CAAA,CAAQ,KAAK,QAAQ,IAAI,EAAWA,QAAoB,CAAC,CAAC;;;ACnDjF,MAAM,YAAY,IAAI,MAAM,KAAK,MAAM,aAAa,aAAa,KAAK,CAAC;AAEvE,QAAQ,KAAK,OAAO,QAAQ,SAAS,GAAG,YAAY,OAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thanaen/worktree-cleanup",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Safely discover and remove stale Git worktrees.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cleanup",
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
"access": "public",
|
|
39
39
|
"provenance": true
|
|
40
40
|
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"check": "pnpm format:check && pnpm diagnostics && pnpm typecheck && pnpm test && pnpm build",
|
|
44
|
+
"diagnostics": "effect-tsgo diagnostics --project tsconfig.json",
|
|
45
|
+
"format": "oxfmt --ignore-path=.oxfmtignore .",
|
|
46
|
+
"format:check": "oxfmt --check --ignore-path=.oxfmtignore .",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
50
|
+
"prepare": "effect-tsgo patch --typescript --no-oxlint"
|
|
51
|
+
},
|
|
41
52
|
"dependencies": {
|
|
42
53
|
"@effect/platform-node": "4.0.0-rc.108",
|
|
43
54
|
"effect": "4.0.0-rc.108"
|
|
@@ -54,14 +65,5 @@
|
|
|
54
65
|
"engines": {
|
|
55
66
|
"node": ">=22"
|
|
56
67
|
},
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"check": "pnpm format:check && pnpm diagnostics && pnpm typecheck && pnpm test && pnpm build",
|
|
60
|
-
"diagnostics": "effect-tsgo diagnostics --project tsconfig.json",
|
|
61
|
-
"format": "oxfmt --ignore-path=.oxfmtignore .",
|
|
62
|
-
"format:check": "oxfmt --check --ignore-path=.oxfmtignore .",
|
|
63
|
-
"test": "vitest run",
|
|
64
|
-
"test:watch": "vitest",
|
|
65
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
+
"packageManager": "pnpm@10.17.1"
|
|
69
|
+
}
|