@biffo/cli 0.309.0 → 0.309.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/index.js +74 -8
- 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,
|
|
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(
|
|
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(
|
|
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
|
);
|