@clipboard-health/groundcrew 4.3.4 → 4.3.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AA2OD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AA6ExB,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAyBD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
1
+ {"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAKlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AA8PD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAyBD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
@@ -7,14 +7,15 @@
7
7
  * (workspace-close + worktree-remove paired) so callers don't reach into
8
8
  * git directly.
9
9
  */
10
- import { existsSync, readdirSync } from "node:fs";
10
+ import { existsSync, readdirSync, rmSync } from "node:fs";
11
11
  import { userInfo } from "node:os";
12
- import { resolve } from "node:path";
12
+ import { isAbsolute, relative, resolve } from "node:path";
13
13
  import { runCommandAsync } from "./commandRunner.js";
14
14
  import { resolveDefaultBranch } from "./defaultBranch.js";
15
15
  import { errorMessage, log } from "./util.js";
16
16
  import { workspaces } from "./workspaces.js";
17
17
  const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
18
+ const WORKTREE_LIST_PREFIX = "worktree ";
18
19
  export class WorktreeAlreadyExistsError extends Error {
19
20
  dir;
20
21
  constructor(dir) {
@@ -186,26 +187,46 @@ async function removeWorktree(config, entry, options) {
186
187
  // ourselves so the failure message names the condition — dirty
187
188
  // (modified/untracked files, fixable with `crew cleanup --force`) or
188
189
  // orphan (directory exists on disk but is not registered with the
189
- // parent repo, requires manual inspection + `rm -rf`).
190
- if (options.force || options.signal?.aborted === true) {
190
+ // parent repo, fixable with `crew cleanup --force` when the path still
191
+ // matches groundcrew's expected worktree location).
192
+ if (options.signal?.aborted === true) {
191
193
  throw error;
192
194
  }
193
- const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
194
- if (dirtiness.kind === "dirty") {
195
- throw new Error(describeDirtyWorktree({
196
- ticket: entry.ticket,
197
- dir: entry.dir,
198
- modified: dirtiness.modified,
199
- untracked: dirtiness.untracked,
200
- }), { cause: error });
195
+ if (options.force) {
196
+ const registration = await probeWorktreeRegistration({
197
+ repoDir,
198
+ worktreeDir: entry.dir,
199
+ ...signalProperty(options.signal),
200
+ });
201
+ if (registration !== "orphan") {
202
+ throw error;
203
+ }
204
+ removeOrphanWorktreeDirectory(config, entry);
201
205
  }
202
- if (dirtiness.kind === "unknown") {
203
- const registration = await probeWorktreeRegistration(entry.dir, options.signal);
204
- if (registration === "orphan") {
205
- throw new Error(describeOrphanWorktree({ dir: entry.dir }), { cause: error });
206
+ else {
207
+ const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
208
+ if (dirtiness.kind === "dirty") {
209
+ throw new Error(describeDirtyWorktree({
210
+ ticket: entry.ticket,
211
+ dir: entry.dir,
212
+ modified: dirtiness.modified,
213
+ untracked: dirtiness.untracked,
214
+ }), { cause: error });
206
215
  }
216
+ if (dirtiness.kind === "unknown") {
217
+ const registration = await probeWorktreeRegistration({
218
+ repoDir,
219
+ worktreeDir: entry.dir,
220
+ ...signalProperty(options.signal),
221
+ });
222
+ if (registration === "orphan") {
223
+ throw new Error(describeOrphanWorktree({ ticket: entry.ticket, dir: entry.dir }), {
224
+ cause: error,
225
+ });
226
+ }
227
+ }
228
+ throw error;
207
229
  }
208
- throw error;
209
230
  }
210
231
  }
211
232
  else {
@@ -258,19 +279,47 @@ function describeDirtyWorktree(arguments_) {
258
279
  const pronoun = modified + untracked === 1 ? "it" : "them";
259
280
  return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
260
281
  }
261
- async function probeWorktreeRegistration(worktreeDir, signal) {
282
+ async function probeWorktreeRegistration(arguments_) {
262
283
  let output;
263
284
  try {
264
- output = await runCommandAsync("git", ["-C", worktreeDir, "rev-parse", "--is-inside-work-tree"], signalProperty(signal));
285
+ output = await runCommandAsync("git", ["-C", arguments_.repoDir, "worktree", "list", "--porcelain"], signalProperty(arguments_.signal));
265
286
  }
266
287
  catch {
267
288
  return "unknown";
268
289
  }
269
- return output === "true" ? "registered" : "orphan";
290
+ const resolvedWorktreeDir = resolve(arguments_.worktreeDir);
291
+ for (const line of output.split("\n")) {
292
+ if (!line.startsWith(WORKTREE_LIST_PREFIX)) {
293
+ continue;
294
+ }
295
+ if (resolve(line.slice(WORKTREE_LIST_PREFIX.length)) === resolvedWorktreeDir) {
296
+ return "registered";
297
+ }
298
+ }
299
+ return "orphan";
270
300
  }
271
301
  function describeOrphanWorktree(arguments_) {
272
- const { dir } = arguments_;
273
- return `directory exists but is not a registered git worktree. If ${dir} has nothing of value, \`rm -rf\` ${dir} manually; otherwise inspect it before deleting.`;
302
+ const { ticket, dir } = arguments_;
303
+ return `directory exists but is not a registered git worktree. Run \`crew cleanup --force ${ticket}\` to remove ${dir}, or inspect it first if it may contain valuable files.`;
304
+ }
305
+ function expectedHostWorktreeDir(config, entry) {
306
+ return resolve(config.workspace.projectDir, `${entry.repository}-${entry.ticket}`);
307
+ }
308
+ function isInsideDirectory(parentDir, childDir) {
309
+ const childRelativePath = relative(parentDir, childDir);
310
+ return (childRelativePath.length > 0 &&
311
+ !childRelativePath.startsWith("..") &&
312
+ !isAbsolute(childRelativePath));
313
+ }
314
+ function removeOrphanWorktreeDirectory(config, entry) {
315
+ const projectDir = resolve(config.workspace.projectDir);
316
+ const expectedDir = expectedHostWorktreeDir(config, entry);
317
+ const targetDir = resolve(entry.dir);
318
+ if (targetDir !== expectedDir || !isInsideDirectory(projectDir, targetDir)) {
319
+ throw new Error(`Refusing to force-delete ${entry.dir}: expected groundcrew worktree path ${expectedDir}.`);
320
+ }
321
+ log(`Removing orphaned worktree directory ${entry.dir} (--force)...`);
322
+ rmSync(targetDir, { recursive: true, force: true });
274
323
  }
275
324
  function list(config) {
276
325
  return listWorktrees(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.3.4",
3
+ "version": "4.3.5",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",