@biffo/cli 0.309.0 → 0.309.2

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.
Files changed (2) hide show
  1. package/dist/index.js +74 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -630,6 +630,7 @@ import { Command as Command3 } from "commander";
630
630
  // src/adapters/git/index.ts
631
631
  import { randomUUID } from "crypto";
632
632
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
633
+ import { readFile as readFile2, rm } from "fs/promises";
633
634
  import { tmpdir as tmpdir2 } from "os";
634
635
  import { join as join6 } from "path";
635
636
 
@@ -1237,6 +1238,51 @@ var GitAdapter = class {
1237
1238
  async hasFleetWorktreeClaim(worktreePath) {
1238
1239
  return existsSync6(join6(worktreePath, ".fleet-worktree-claim"));
1239
1240
  }
1241
+ /**
1242
+ * Milliseconds since `worktreePath`'s `.fleet-worktree-claim` lock was
1243
+ * written (biffo-template#1948), or `null` when there is no lock, or its
1244
+ * `holder` file cannot be read and parsed.
1245
+ *
1246
+ * `bin/fleet.sh worktree-claim` (biffo-fleet) writes the holder token on
1247
+ * line 1 and a UTC ISO-8601 timestamp (`now()`, i.e. `date -u
1248
+ * +%Y-%m-%dT%H:%M:%SZ`) on line 2 — this reads that second line and diffs
1249
+ * it against the current time. `null` is the fail-closed default for
1250
+ * "cannot tell": `doctor-reaper.ts` treats it exactly like a live claim,
1251
+ * never like a stale one, the same posture `hasFleetWorktreeClaim` already
1252
+ * takes and fleet.sh's own comment on an unwritten holder file takes ("An
1253
+ * unwritten holder is still a HELD lock").
1254
+ */
1255
+ async fleetWorktreeClaimAgeMs(worktreePath) {
1256
+ try {
1257
+ const raw = await readFile2(join6(worktreePath, ".fleet-worktree-claim", "holder"), "utf8");
1258
+ const claimedAt = raw.split("\n")[1]?.trim();
1259
+ if (!claimedAt) return null;
1260
+ const claimedMs = Date.parse(claimedAt);
1261
+ return Number.isNaN(claimedMs) ? null : Date.now() - claimedMs;
1262
+ } catch {
1263
+ return null;
1264
+ }
1265
+ }
1266
+ /**
1267
+ * Removes a `.fleet-worktree-claim` lock directory outright (biffo-template#1948).
1268
+ *
1269
+ * Used only once `doctor --fix` has independently judged the lock stale —
1270
+ * see `FLEET_CLAIM_STALE_AFTER_MS` in `lib/doctor-reaper.ts`. Needed because
1271
+ * the lock directory is untracked: leaving it in place after deciding to
1272
+ * disregard it would trip `hasUncommittedChanges`'s `git status
1273
+ * --porcelain` and keep the worktree anyway, under the less specific
1274
+ * `uncommitted-changes` reason, defeating the point of ignoring it.
1275
+ *
1276
+ * Best-effort — swallows any error, so a permissions problem or a
1277
+ * concurrent release falls back to the existing `isDirty` keep path rather
1278
+ * than crashing the whole `--fix` sweep.
1279
+ */
1280
+ async clearStaleFleetWorktreeClaim(worktreePath) {
1281
+ try {
1282
+ await rm(join6(worktreePath, ".fleet-worktree-claim"), { recursive: true, force: true });
1283
+ } catch {
1284
+ }
1285
+ }
1240
1286
  /** Best-effort fetch of the tracking remote, so the ahead/behind check below
1241
1287
  * compares against reality rather than a stale local ref (#394). Never throws:
1242
1288
  * offline or a missing remote must not block an upgrade on its own — the
@@ -16232,6 +16278,7 @@ function runDoctorChecks(facts) {
16232
16278
  }
16233
16279
 
16234
16280
  // src/lib/doctor-reaper.ts
16281
+ var FLEET_CLAIM_STALE_AFTER_MS = 4 * 60 * 60 * 1e3;
16235
16282
  function classifyReapCandidate(facts) {
16236
16283
  if (facts.hasFleetClaim) return { action: "keep", reason: "fleet-worktree-claimed" };
16237
16284
  if (facts.isDetached) return { action: "keep", reason: "detached-head" };
@@ -16259,12 +16306,23 @@ function findReapCandidates(branches, worktrees) {
16259
16306
  }
16260
16307
  async function reapCandidate(cwd, candidate, deps) {
16261
16308
  const { git, github } = deps;
16262
- const [current, isDirty, hasFleetClaim] = await Promise.all([
16309
+ const [current, claimExists] = await Promise.all([
16263
16310
  git.currentBranch(candidate.worktreePath),
16264
- git.hasUncommittedChanges(candidate.worktreePath),
16265
16311
  git.hasFleetWorktreeClaim(candidate.worktreePath)
16266
16312
  ]);
16267
16313
  const isDetached = current === "HEAD" || current === "";
16314
+ let hasFleetClaim = false;
16315
+ let staleClaimCleared = false;
16316
+ if (claimExists) {
16317
+ const ageMs = await git.fleetWorktreeClaimAgeMs(candidate.worktreePath);
16318
+ if (ageMs === null || ageMs < FLEET_CLAIM_STALE_AFTER_MS) {
16319
+ hasFleetClaim = true;
16320
+ } else {
16321
+ await git.clearStaleFleetWorktreeClaim(candidate.worktreePath);
16322
+ staleClaimCleared = true;
16323
+ }
16324
+ }
16325
+ const isDirty = await git.hasUncommittedChanges(candidate.worktreePath);
16268
16326
  const prVerdict = isDetached || isDirty || hasFleetClaim ? "unknown" : await github.prVerdictForBranch(cwd, candidate.branch);
16269
16327
  let mergeContainsHead = true;
16270
16328
  if (prVerdict === "merged") {
@@ -16282,10 +16340,10 @@ async function reapCandidate(cwd, candidate, deps) {
16282
16340
  mergeContainsHead
16283
16341
  });
16284
16342
  if (verdict.action === "keep") {
16285
- return { candidate, verdict, worktreeRemoved: null };
16343
+ return { candidate, verdict, worktreeRemoved: null, staleClaimCleared };
16286
16344
  }
16287
16345
  const worktreeRemoved = await git.removeWorktree(cwd, candidate.worktreePath);
16288
- return { candidate, verdict, worktreeRemoved };
16346
+ return { candidate, verdict, worktreeRemoved, staleClaimCleared };
16289
16347
  }
16290
16348
  async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
16291
16349
  const candidates = findReapCandidates(branches, worktrees).filter(
@@ -16486,28 +16544,36 @@ function printReapOutcomes(outcomes) {
16486
16544
  const kept = outcomes.filter((o) => o.verdict.action === "keep");
16487
16545
  const removed = attempted.filter((o) => o.worktreeRemoved === true);
16488
16546
  const failed = attempted.filter((o) => o.worktreeRemoved !== true);
16547
+ const staleClaims = outcomes.filter((o) => o.staleClaimCleared);
16489
16548
  console.log("");
16490
16549
  for (const o of attempted) {
16550
+ const staleNote = o.staleClaimCleared ? " (after clearing a stale fleet-worktree-claim lock)" : "";
16491
16551
  if (o.worktreeRemoved === true) {
16492
- console.log(chalk21.green(` removed ${o.candidate.worktreePath} (${o.candidate.branch})`));
16552
+ console.log(
16553
+ chalk21.green(` removed ${o.candidate.worktreePath} (${o.candidate.branch})${staleNote}`)
16554
+ );
16493
16555
  } else {
16494
16556
  console.log(
16495
16557
  chalk21.red(
16496
- ` FAILED ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 judged safe to remove but 'git worktree remove' did not succeed; left as-is`
16558
+ ` FAILED ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 judged safe to remove but 'git worktree remove' did not succeed; left as-is${staleNote}`
16497
16559
  )
16498
16560
  );
16499
16561
  }
16500
16562
  }
16501
16563
  for (const o of kept) {
16502
16564
  const reason = o.verdict.reason === void 0 ? "unknown" : KEEP_REASON_TEXT[o.verdict.reason];
16565
+ const staleNote = o.staleClaimCleared ? " (its stale fleet-worktree-claim lock was cleared first)" : "";
16503
16566
  console.log(
16504
- chalk21.dim(` kept ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 ${reason}`)
16567
+ chalk21.dim(
16568
+ ` kept ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 ${reason}${staleNote}`
16569
+ )
16505
16570
  );
16506
16571
  }
16572
+ const staleClaimNote = staleClaims.length > 0 ? ` (${String(staleClaims.length)} had a stale fleet-worktree-claim lock cleared)` : "";
16507
16573
  console.log(
16508
16574
  chalk21.dim(
16509
16575
  `
16510
- --fix: ${String(removed.length)} removed, ${String(failed.length)} failed, ${String(kept.length)} kept, of ${String(outcomes.length)} worktree(s) considered.
16576
+ --fix: ${String(removed.length)} removed, ${String(failed.length)} failed, ${String(kept.length)} kept, of ${String(outcomes.length)} worktree(s) considered${staleClaimNote}.
16511
16577
  `
16512
16578
  )
16513
16579
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.309.0",
3
+ "version": "0.309.2",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",