@biffo/cli 0.312.5 → 0.313.0

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 +151 -31
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16299,7 +16299,7 @@ function rawArgsAfter(subcommand) {
16299
16299
 
16300
16300
  // src/commands/doctor.ts
16301
16301
  import { existsSync as existsSync56, readFileSync as readFileSync47 } from "fs";
16302
- import { join as join68, resolve as resolve20 } from "path";
16302
+ import { join as join69, resolve as resolve20 } from "path";
16303
16303
  import chalk21 from "chalk";
16304
16304
  import { Command as Command25 } from "commander";
16305
16305
 
@@ -16638,33 +16638,110 @@ async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps
16638
16638
  return outcomes;
16639
16639
  }
16640
16640
 
16641
- // src/commands/doctor.ts
16641
+ // src/lib/scratch-clone-scan.ts
16642
+ import { readdirSync as readdirSync29, statSync as statSync18 } from "fs";
16643
+ import { join as join68 } from "path";
16642
16644
  var INTEGRATION_BRANCH = "dev";
16645
+ function isPlainCloneDir(path) {
16646
+ try {
16647
+ return statSync18(join68(path, ".git")).isDirectory();
16648
+ } catch {
16649
+ return false;
16650
+ }
16651
+ }
16652
+ function repoNameFromRemoteUrl(url) {
16653
+ const trimmed = url.trim();
16654
+ if (trimmed === "") return null;
16655
+ const match = /\/([^/]+?)(\.git)?\/?$/.exec(trimmed);
16656
+ return match?.[1] && match[1] !== "" ? match[1] : null;
16657
+ }
16658
+ async function findScratchCloneCandidates(estateRoot, deps) {
16659
+ const names = readdirSync29(estateRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
16660
+ const candidates = [];
16661
+ for (const name of names) {
16662
+ const path = join68(estateRoot, name);
16663
+ if (!isPlainCloneDir(path)) continue;
16664
+ let branch;
16665
+ try {
16666
+ branch = await deps.git.currentBranch(path);
16667
+ } catch {
16668
+ candidates.push({ path, branch: "", invalidRepo: true });
16669
+ continue;
16670
+ }
16671
+ const remoteUrl = await deps.git.getRemoteUrl(path).catch(() => "");
16672
+ const repoName = remoteUrl === "" ? null : repoNameFromRemoteUrl(remoteUrl);
16673
+ const looksLikeKnownRepoCheckout = repoName !== null && repoName.toLowerCase() === name.toLowerCase() && branch === INTEGRATION_BRANCH;
16674
+ if (looksLikeKnownRepoCheckout) continue;
16675
+ candidates.push({ path, branch });
16676
+ }
16677
+ return candidates;
16678
+ }
16679
+ async function classifyScratchClones(candidates, deps) {
16680
+ const reports = [];
16681
+ for (const candidate of candidates) {
16682
+ if (candidate.invalidRepo === true) {
16683
+ reports.push({ candidate, verdict: { action: "keep", reason: "not-a-git-repository" } });
16684
+ continue;
16685
+ }
16686
+ const isDetached = candidate.branch === "HEAD" || candidate.branch === "";
16687
+ const isDirty = await deps.git.hasUncommittedChanges(candidate.path);
16688
+ const prVerdict = isDetached || isDirty ? "unknown" : await deps.github.prVerdictForBranch(candidate.path, candidate.branch);
16689
+ let mergeContainsHead = true;
16690
+ if (prVerdict === "merged") {
16691
+ const [headSha, mergedHeadSha] = await Promise.all([
16692
+ deps.git.headSha(candidate.path),
16693
+ deps.github.mergedHeadSha(candidate.path, candidate.branch)
16694
+ ]);
16695
+ mergeContainsHead = headSha === null || mergedHeadSha === null ? null : await deps.git.isAncestor(candidate.path, headSha, mergedHeadSha);
16696
+ }
16697
+ const verdict = classifyReapCandidate({
16698
+ isDetached,
16699
+ isDirty,
16700
+ hasFleetClaim: false,
16701
+ prVerdict,
16702
+ mergeContainsHead
16703
+ });
16704
+ reports.push({ candidate, verdict });
16705
+ }
16706
+ return reports;
16707
+ }
16708
+
16709
+ // src/commands/doctor.ts
16710
+ var INTEGRATION_BRANCH2 = "dev";
16643
16711
  var doctorCommand = new Command25("doctor").description(
16644
16712
  "Report repo-state conditions that make everything read from this checkout unreliable"
16645
16713
  ).option("--cwd <path>", "Repo root to inspect (defaults to the current directory)").option("--no-fetch", "Skip the fetch; report against refs as they already are locally").option(
16646
16714
  "--fix",
16647
16715
  "Also remove any worktree, and delete any bare local branch, PROVEN safe: one whose branch's PR merged, verified via GitHub (never local commit reachability \u2014 a squash merge rewrites every SHA), AND whose current tip is confirmed contained in what that PR actually shipped \u2014 not merely on a branch of the same name (#1810). Everything else (no PR, PR open, PR closed unmerged, detached HEAD, uncommitted changes, commits ahead of what merged, or that containment could not be confirmed) is reported, never touched."
16648
- ).action(async (options) => {
16649
- const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
16650
- const git = new GitAdapter();
16651
- try {
16652
- const facts = await gatherRepoFacts({ cwd, fetch: options.fetch !== false }, { git });
16653
- const findings = runDoctorChecks(facts);
16654
- printFindings(findings);
16655
- if (options.fix === true) {
16656
- const github = new GithubCliAdapter();
16657
- const outcomes = await runDoctorFix(cwd, facts, { git, github });
16658
- printReapOutcomes(outcomes);
16659
- const branchOutcomes = await runDoctorFixBranches(cwd, facts, { git, github });
16660
- printBranchReapOutcomes(branchOutcomes);
16661
- }
16662
- if (findings.some((f) => f.severity === "error")) process.exit(1);
16663
- } catch (err) {
16664
- log.error(err.message);
16665
- process.exit(1);
16716
+ ).option(
16717
+ "--scratch-clones <estateRoot>",
16718
+ "Instead of checking one repo, scan <estateRoot> for top-level plain `git clone` directories (#1949) \u2014 invisible to every check above, which all start from `git worktree list`. Each is classified the same way `--fix` classifies a worktree (GitHub's own PR verdict), but nothing is ever removed by this mode: a wrongly-excluded directory costs a report line, an `rm -rf` on a wrongly-included one would not be recoverable the way `git worktree remove` is."
16719
+ ).action(
16720
+ async (options) => {
16721
+ if (options.scratchClones !== void 0) {
16722
+ await runScratchCloneScan(resolve20(options.scratchClones));
16723
+ return;
16724
+ }
16725
+ const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
16726
+ const git = new GitAdapter();
16727
+ try {
16728
+ const facts = await gatherRepoFacts({ cwd, fetch: options.fetch !== false }, { git });
16729
+ const findings = runDoctorChecks(facts);
16730
+ printFindings(findings);
16731
+ if (options.fix === true) {
16732
+ const github = new GithubCliAdapter();
16733
+ const outcomes = await runDoctorFix(cwd, facts, { git, github });
16734
+ printReapOutcomes(outcomes);
16735
+ const branchOutcomes = await runDoctorFixBranches(cwd, facts, { git, github });
16736
+ printBranchReapOutcomes(branchOutcomes);
16737
+ }
16738
+ if (findings.some((f) => f.severity === "error")) process.exit(1);
16739
+ } catch (err) {
16740
+ log.error(err.message);
16741
+ process.exit(1);
16742
+ }
16666
16743
  }
16667
- });
16744
+ );
16668
16745
  async function gatherRepoFacts(options, deps) {
16669
16746
  const { git } = deps;
16670
16747
  if (!await git.isGitRepo(options.cwd)) {
@@ -16680,13 +16757,13 @@ async function gatherRepoFacts(options, deps) {
16680
16757
  const worktrees = await Promise.all(
16681
16758
  worktreePaths.map(async (w) => ({
16682
16759
  ...w,
16683
- behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH}`)
16760
+ behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH2}`)
16684
16761
  }))
16685
16762
  );
16686
16763
  return {
16687
16764
  currentBranch,
16688
16765
  isPrimary,
16689
- integrationBranch: INTEGRATION_BRANCH,
16766
+ integrationBranch: INTEGRATION_BRANCH2,
16690
16767
  ahead,
16691
16768
  behind,
16692
16769
  hasUpstream,
@@ -16697,7 +16774,7 @@ async function gatherRepoFacts(options, deps) {
16697
16774
  // a fault in it.
16698
16775
  localCoreVersion: readLocalCoreVersion(options.cwd),
16699
16776
  remoteCoreVersion: parseCoreRecord(
16700
- await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH}`, INSTANCE_CORE_FILE)
16777
+ await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH2}`, INSTANCE_CORE_FILE)
16701
16778
  ),
16702
16779
  fossilCoreVersion: readFossil(options.cwd),
16703
16780
  branches,
@@ -16719,8 +16796,50 @@ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(),
16719
16796
  remoteBranchNames
16720
16797
  );
16721
16798
  }
16799
+ async function runScratchCloneScan(estateRoot) {
16800
+ const git = new GitAdapter();
16801
+ const github = new GithubCliAdapter();
16802
+ try {
16803
+ const candidates = await findScratchCloneCandidates(estateRoot, { git });
16804
+ const reports = await classifyScratchClones(candidates, { git, github });
16805
+ printScratchCloneReports(estateRoot, reports);
16806
+ } catch (err) {
16807
+ log.error(err.message);
16808
+ process.exit(1);
16809
+ }
16810
+ }
16811
+ function printScratchCloneReports(estateRoot, reports) {
16812
+ if (reports.length === 0) {
16813
+ console.log(
16814
+ chalk21.dim(` scratch-clones: no plain git-clone directory found under ${estateRoot}.
16815
+ `)
16816
+ );
16817
+ return;
16818
+ }
16819
+ const reapable = reports.filter((r) => r.verdict.action === "reap");
16820
+ const kept = reports.filter((r) => r.verdict.action === "keep");
16821
+ console.log("");
16822
+ for (const r of reapable) {
16823
+ console.log(
16824
+ chalk21.yellow(
16825
+ ` reapable ${r.candidate.path} (${r.candidate.branch}) \u2014 branch's PR merged and this clone's tip is contained in what shipped; plain git clone, not a worktree, so \`git worktree remove\` does not apply \u2014 remove by hand once confirmed: rm -rf '${r.candidate.path}'`
16826
+ )
16827
+ );
16828
+ }
16829
+ for (const r of kept) {
16830
+ const reason = r.verdict.reason === void 0 ? "unknown" : KEEP_REASON_TEXT[r.verdict.reason];
16831
+ console.log(chalk21.dim(` kept ${r.candidate.path} (${r.candidate.branch}) \u2014 ${reason}`));
16832
+ }
16833
+ console.log(
16834
+ chalk21.dim(
16835
+ `
16836
+ scratch-clones: ${String(reapable.length)} reapable, ${String(kept.length)} kept, of ${String(reports.length)} plain git-clone director${reports.length === 1 ? "y" : "ies"} found under ${estateRoot}.
16837
+ `
16838
+ )
16839
+ );
16840
+ }
16722
16841
  function readLocalCoreVersion(cwd) {
16723
- const path = join68(cwd, INSTANCE_CORE_FILE);
16842
+ const path = join69(cwd, INSTANCE_CORE_FILE);
16724
16843
  if (!existsSync56(path)) return null;
16725
16844
  try {
16726
16845
  return extractVersionField(readFileSync47(path, "utf8"));
@@ -16743,7 +16862,7 @@ function extractVersionField(contents) {
16743
16862
  return match?.[1] ?? null;
16744
16863
  }
16745
16864
  function readFossil(cwd) {
16746
- const path = join68(cwd, CORE_VERSION_FILE);
16865
+ const path = join69(cwd, CORE_VERSION_FILE);
16747
16866
  if (!existsSync56(path)) return null;
16748
16867
  try {
16749
16868
  const value = readFileSync47(path, "utf8").trim();
@@ -16783,7 +16902,8 @@ var KEEP_REASON_TEXT = {
16783
16902
  "no-pr": "no PR was ever opened from this branch",
16784
16903
  "unknown-pr-verdict": "could not read this branch's PR state from GitHub",
16785
16904
  "commits-not-in-merge": "worktree HEAD includes commits the merged PR never shipped",
16786
- "unknown-merge-head": "could not confirm worktree HEAD is contained in what merged"
16905
+ "unknown-merge-head": "could not confirm worktree HEAD is contained in what merged",
16906
+ "not-a-git-repository": "the .git directory exists but is not a resolvable, initialised git repository"
16787
16907
  };
16788
16908
  function printReapOutcomes(outcomes) {
16789
16909
  if (outcomes.length === 0) {
@@ -17277,18 +17397,18 @@ function resolveGithubToken4() {
17277
17397
 
17278
17398
  // src/lib/packaged-script-command.ts
17279
17399
  import { spawnSync } from "child_process";
17280
- import { chmodSync as chmodSync2, statSync as statSync18 } from "fs";
17400
+ import { chmodSync as chmodSync2, statSync as statSync19 } from "fs";
17281
17401
  import { dirname as dirname12 } from "path";
17282
17402
  import { fileURLToPath as fileURLToPath6 } from "url";
17283
17403
  import { Command as Command27 } from "commander";
17284
17404
 
17285
17405
  // src/lib/packaged-scripts.ts
17286
17406
  import { existsSync as existsSync57 } from "fs";
17287
- import { dirname as dirname11, join as join69 } from "path";
17407
+ import { dirname as dirname11, join as join70 } from "path";
17288
17408
  function findPackagedScript(startDir, relativePath) {
17289
17409
  let dir = startDir;
17290
17410
  for (; ; ) {
17291
- const candidate = join69(dir, relativePath);
17411
+ const candidate = join70(dir, relativePath);
17292
17412
  if (existsSync57(candidate)) return candidate;
17293
17413
  const parent = dirname11(dir);
17294
17414
  if (parent === dir) return null;
@@ -17303,7 +17423,7 @@ It ships with this package via cli/scripts/packaged-root-assets.mjs; if you are
17303
17423
  // src/lib/packaged-script-command.ts
17304
17424
  function ensureExecutable(script) {
17305
17425
  try {
17306
- const { mode } = statSync18(script);
17426
+ const { mode } = statSync19(script);
17307
17427
  if ((mode & 73) === 0) chmodSync2(script, mode | 493);
17308
17428
  } catch {
17309
17429
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.312.5",
3
+ "version": "0.313.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",