@autosk/worktree 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/package.json +1 -1
- package/src/index.ts +57 -3
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { createHash } from "node:crypto";
|
|
|
22
22
|
import { mkdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
23
23
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
24
24
|
|
|
25
|
-
import type { IsolationHandle, IsolationProvider } from "@autosk/sdk";
|
|
25
|
+
import type { IsolationHandle, IsolationProvider, IsolationReapResult } from "@autosk/sdk";
|
|
26
26
|
|
|
27
27
|
/** Options for {@link worktreeIsolation}. */
|
|
28
28
|
export interface WorktreeIsolationOptions {
|
|
@@ -95,7 +95,7 @@ export function worktreeIsolation(opts: WorktreeIsolationOptions = {}): Isolatio
|
|
|
95
95
|
return { cwd: path, meta };
|
|
96
96
|
},
|
|
97
97
|
|
|
98
|
-
async release(handle, { terminal }): Promise<void> {
|
|
98
|
+
async release(handle, { terminal, force }): Promise<void> {
|
|
99
99
|
// Non-terminal (sibling step / human-park): keep the dir so the next step
|
|
100
100
|
// re-uses the same checkout. Nothing to do.
|
|
101
101
|
if (!terminal) return;
|
|
@@ -103,7 +103,20 @@ export function worktreeIsolation(opts: WorktreeIsolationOptions = {}): Isolatio
|
|
|
103
103
|
const path = handle.cwd;
|
|
104
104
|
const canon = meta.projectRoot ? canonRoot(meta.projectRoot) : "";
|
|
105
105
|
await ensureGitAvailable(gitBin);
|
|
106
|
-
await
|
|
106
|
+
const r = await cleanupTerminal(gitBin, canon, path, force);
|
|
107
|
+
// The engine drives a terminal `release` with `force:true`, so this never
|
|
108
|
+
// fires there; an explicit `force:false` caller gets a descriptive throw it
|
|
109
|
+
// can surface (the engine would wrap it as `isolation_release_failed:`).
|
|
110
|
+
if (r.dirty && !r.removed) {
|
|
111
|
+
throw new Error(`worktree_dirty: ${path}${r.detail ? ` (${r.detail})` : ""}`);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async reap({ projectRoot, taskId }, { force }): Promise<IsolationReapResult> {
|
|
116
|
+
const canon = canonRoot(projectRoot);
|
|
117
|
+
const path = pathFor(canon, taskId, opts.home);
|
|
118
|
+
await ensureGitAvailable(gitBin);
|
|
119
|
+
return cleanupTerminal(gitBin, canon, path, force);
|
|
107
120
|
},
|
|
108
121
|
};
|
|
109
122
|
}
|
|
@@ -172,6 +185,47 @@ async function verifyHealthy(gitBin: string, canon: string, path: string): Promi
|
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The shared terminal-cleanup core for both {@link IsolationProvider.release}
|
|
190
|
+
* (live handle) and {@link IsolationProvider.reap} (session-free identity).
|
|
191
|
+
*
|
|
192
|
+
* Removes the worktree dir while PRESERVING its branch, gated on `force`:
|
|
193
|
+
* `force:false` leaves a dirty checkout in place and reports `{dirty:true}` so
|
|
194
|
+
* the caller can warn; `force:true` removes it regardless. A vanished/absent dir
|
|
195
|
+
* is a no-op (`{removed:false}`); a stranded dir that is not a healthy checkout
|
|
196
|
+
* cannot be "dirty" in any recoverable sense and is reaped.
|
|
197
|
+
*/
|
|
198
|
+
async function cleanupTerminal(
|
|
199
|
+
gitBin: string,
|
|
200
|
+
canon: string,
|
|
201
|
+
path: string,
|
|
202
|
+
force: boolean,
|
|
203
|
+
): Promise<IsolationReapResult> {
|
|
204
|
+
if (!existsPath(path)) {
|
|
205
|
+
// Nothing on disk; best-effort clear of any stale git registration.
|
|
206
|
+
if (canon !== "" && (await isGitRepo(gitBin, canon))) await pruneWorktrees(gitBin, canon);
|
|
207
|
+
return { removed: false, dirty: false };
|
|
208
|
+
}
|
|
209
|
+
const { dirty, detail } = await worktreeDirty(gitBin, path);
|
|
210
|
+
if (dirty && !force) return { removed: false, dirty: true, detail };
|
|
211
|
+
await onTerminal(gitBin, canon, path);
|
|
212
|
+
return { removed: true, dirty, detail: dirty ? detail : undefined };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Reports whether the checkout at `path` has uncommitted changes (modified,
|
|
217
|
+
* staged, OR untracked — anything `git status --porcelain` surfaces). A path
|
|
218
|
+
* that is not a healthy worktree (status errors) reads as NOT dirty: there is no
|
|
219
|
+
* recoverable working state to protect, so the caller may reap the stranded dir.
|
|
220
|
+
*/
|
|
221
|
+
async function worktreeDirty(gitBin: string, path: string): Promise<{ dirty: boolean; detail: string }> {
|
|
222
|
+
const r = await runGit(gitBin, path, ["status", "--porcelain", "--untracked-files=all"]);
|
|
223
|
+
if (r.code !== 0) return { dirty: false, detail: "" };
|
|
224
|
+
const lines = r.stdout.split("\n").filter((l) => l.trim() !== "");
|
|
225
|
+
if (lines.length === 0) return { dirty: false, detail: "" };
|
|
226
|
+
return { dirty: true, detail: `${lines.length} uncommitted file(s)` };
|
|
227
|
+
}
|
|
228
|
+
|
|
175
229
|
/** Removes the worktree dir on a terminal transition while PRESERVING its branch. */
|
|
176
230
|
async function onTerminal(gitBin: string, canon: string, path: string): Promise<void> {
|
|
177
231
|
// git itself broken / no project root → still try to reap the on-disk dir.
|