@biffo/cli 0.312.5 → 0.313.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.
Files changed (2) hide show
  1. package/dist/index.js +200 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -719,7 +719,8 @@ function checkCoreOwnership({
719
719
  branch = "",
720
720
  commitMessage = "",
721
721
  warnOnly = [],
722
- templateShippedPaths = null
722
+ templateShippedPaths = null,
723
+ upgradeCommitFiles = null
723
724
  }) {
724
725
  const empty = {
725
726
  blocked: [],
@@ -729,8 +730,13 @@ function checkCoreOwnership({
729
730
  knownOrphans: []
730
731
  };
731
732
  if (!isInstance) return { skipped: "template", ...empty };
732
- if (branch.startsWith(UPGRADE_BRANCH_PREFIX)) return { skipped: "upgrade-branch", ...empty };
733
- const manifestTemplateOwned = changedFiles.filter((f) => isTemplateOwned(f, manifest));
733
+ let effectiveChangedFiles = changedFiles;
734
+ if (branch.startsWith(UPGRADE_BRANCH_PREFIX)) {
735
+ const exempt = new Set(upgradeCommitFiles ?? []);
736
+ effectiveChangedFiles = changedFiles.filter((f) => !exempt.has(f));
737
+ if (effectiveChangedFiles.length === 0) return { skipped: "upgrade-branch", ...empty };
738
+ }
739
+ const manifestTemplateOwned = effectiveChangedFiles.filter((f) => isTemplateOwned(f, manifest));
734
740
  const knownOrphans = [];
735
741
  const templateOwned = [];
736
742
  for (const path of manifestTemplateOwned) {
@@ -12722,7 +12728,37 @@ async function fetchTemplateShippedPaths(repoRoot, runner = defaultRunner) {
12722
12728
  return paths.length > 0 ? new Set(paths) : null;
12723
12729
  }
12724
12730
 
12731
+ // src/lib/upgrade-commit-files.ts
12732
+ var defaultRunner2 = async (args, cwd) => {
12733
+ const result = await execa("git", args, { cwd, reject: false, timeout: 15e3 });
12734
+ return { stdout: String(result.stdout ?? ""), exitCode: result.exitCode ?? null };
12735
+ };
12736
+ var splitLines = (stdout) => stdout.split("\n").map((line) => line.trim()).filter(Boolean);
12737
+ async function resolveUpgradeCommitFiles(repoRoot, base, opts = {}, runner = defaultRunner2) {
12738
+ let rev;
12739
+ try {
12740
+ rev = await runner(["rev-list", "--reverse", `${base}..HEAD`], repoRoot);
12741
+ } catch {
12742
+ return null;
12743
+ }
12744
+ if (rev.exitCode !== 0) return null;
12745
+ const commits = splitLines(rev.stdout);
12746
+ if (commits.length === 0) {
12747
+ return opts.stagedFallbackFiles ?? null;
12748
+ }
12749
+ const first = commits[0];
12750
+ let diff;
12751
+ try {
12752
+ diff = await runner(["diff-tree", "--no-commit-id", "--name-only", "-r", first], repoRoot);
12753
+ } catch {
12754
+ return null;
12755
+ }
12756
+ if (diff.exitCode !== 0) return null;
12757
+ return splitLines(diff.stdout);
12758
+ }
12759
+
12725
12760
  // src/scripts/check-core-ownership.ts
12761
+ var LOCAL_UPGRADE_BASE = "origin/dev";
12726
12762
  var BOLD = "\x1B[1m";
12727
12763
  var DIM = "\x1B[2m";
12728
12764
  var RED = "\x1B[31m";
@@ -12748,6 +12784,7 @@ async function runOwnershipCheck(argv) {
12748
12784
  let changedFiles;
12749
12785
  let deletedFiles = [];
12750
12786
  let commitMessage = "";
12787
+ let ciBase = null;
12751
12788
  if (staged) {
12752
12789
  const { stdout } = await execa("git", ["diff", "--cached", "--name-status"], { cwd: root });
12753
12790
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
@@ -12762,11 +12799,12 @@ async function runOwnershipCheck(argv) {
12762
12799
  process.exit(2);
12763
12800
  }
12764
12801
  await execa("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12765
- const { stdout } = await execa("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
12802
+ ciBase = `origin/${base}`;
12803
+ const { stdout } = await execa("git", ["diff", "--name-status", `${ciBase}...HEAD`], {
12766
12804
  cwd: root
12767
12805
  });
12768
12806
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
12769
- const { stdout: log2 } = await execa("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
12807
+ const { stdout: log2 } = await execa("git", ["log", "--format=%B", `${ciBase}..HEAD`], {
12770
12808
  cwd: root,
12771
12809
  reject: false
12772
12810
  });
@@ -12777,6 +12815,11 @@ async function runOwnershipCheck(argv) {
12777
12815
  reject: false
12778
12816
  });
12779
12817
  const branch = resolveBranch(process.env, gitBranch);
12818
+ const upgradeCommitFiles = branch.startsWith(UPGRADE_BRANCH_PREFIX) ? await resolveUpgradeCommitFiles(
12819
+ root,
12820
+ ciBase ?? LOCAL_UPGRADE_BASE,
12821
+ staged ? { stagedFallbackFiles: changedFiles } : {}
12822
+ ) : null;
12780
12823
  const templateShippedPaths = staged ? null : await fetchTemplateShippedPaths(root);
12781
12824
  if (!staged && templateShippedPaths === null) {
12782
12825
  console.error(
@@ -12790,6 +12833,7 @@ async function runOwnershipCheck(argv) {
12790
12833
  branch,
12791
12834
  commitMessage,
12792
12835
  warnOnly: readDivergenceConfig(root).warnOnly,
12836
+ upgradeCommitFiles,
12793
12837
  templateShippedPaths
12794
12838
  });
12795
12839
  if (result.knownOrphans.length > 0) {
@@ -16299,7 +16343,7 @@ function rawArgsAfter(subcommand) {
16299
16343
 
16300
16344
  // src/commands/doctor.ts
16301
16345
  import { existsSync as existsSync56, readFileSync as readFileSync47 } from "fs";
16302
- import { join as join68, resolve as resolve20 } from "path";
16346
+ import { join as join69, resolve as resolve20 } from "path";
16303
16347
  import chalk21 from "chalk";
16304
16348
  import { Command as Command25 } from "commander";
16305
16349
 
@@ -16638,33 +16682,110 @@ async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps
16638
16682
  return outcomes;
16639
16683
  }
16640
16684
 
16641
- // src/commands/doctor.ts
16685
+ // src/lib/scratch-clone-scan.ts
16686
+ import { readdirSync as readdirSync29, statSync as statSync18 } from "fs";
16687
+ import { join as join68 } from "path";
16642
16688
  var INTEGRATION_BRANCH = "dev";
16689
+ function isPlainCloneDir(path) {
16690
+ try {
16691
+ return statSync18(join68(path, ".git")).isDirectory();
16692
+ } catch {
16693
+ return false;
16694
+ }
16695
+ }
16696
+ function repoNameFromRemoteUrl(url) {
16697
+ const trimmed = url.trim();
16698
+ if (trimmed === "") return null;
16699
+ const match = /\/([^/]+?)(\.git)?\/?$/.exec(trimmed);
16700
+ return match?.[1] && match[1] !== "" ? match[1] : null;
16701
+ }
16702
+ async function findScratchCloneCandidates(estateRoot, deps) {
16703
+ const names = readdirSync29(estateRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
16704
+ const candidates = [];
16705
+ for (const name of names) {
16706
+ const path = join68(estateRoot, name);
16707
+ if (!isPlainCloneDir(path)) continue;
16708
+ let branch;
16709
+ try {
16710
+ branch = await deps.git.currentBranch(path);
16711
+ } catch {
16712
+ candidates.push({ path, branch: "", invalidRepo: true });
16713
+ continue;
16714
+ }
16715
+ const remoteUrl = await deps.git.getRemoteUrl(path).catch(() => "");
16716
+ const repoName = remoteUrl === "" ? null : repoNameFromRemoteUrl(remoteUrl);
16717
+ const looksLikeKnownRepoCheckout = repoName !== null && repoName.toLowerCase() === name.toLowerCase() && branch === INTEGRATION_BRANCH;
16718
+ if (looksLikeKnownRepoCheckout) continue;
16719
+ candidates.push({ path, branch });
16720
+ }
16721
+ return candidates;
16722
+ }
16723
+ async function classifyScratchClones(candidates, deps) {
16724
+ const reports = [];
16725
+ for (const candidate of candidates) {
16726
+ if (candidate.invalidRepo === true) {
16727
+ reports.push({ candidate, verdict: { action: "keep", reason: "not-a-git-repository" } });
16728
+ continue;
16729
+ }
16730
+ const isDetached = candidate.branch === "HEAD" || candidate.branch === "";
16731
+ const isDirty = await deps.git.hasUncommittedChanges(candidate.path);
16732
+ const prVerdict = isDetached || isDirty ? "unknown" : await deps.github.prVerdictForBranch(candidate.path, candidate.branch);
16733
+ let mergeContainsHead = true;
16734
+ if (prVerdict === "merged") {
16735
+ const [headSha, mergedHeadSha] = await Promise.all([
16736
+ deps.git.headSha(candidate.path),
16737
+ deps.github.mergedHeadSha(candidate.path, candidate.branch)
16738
+ ]);
16739
+ mergeContainsHead = headSha === null || mergedHeadSha === null ? null : await deps.git.isAncestor(candidate.path, headSha, mergedHeadSha);
16740
+ }
16741
+ const verdict = classifyReapCandidate({
16742
+ isDetached,
16743
+ isDirty,
16744
+ hasFleetClaim: false,
16745
+ prVerdict,
16746
+ mergeContainsHead
16747
+ });
16748
+ reports.push({ candidate, verdict });
16749
+ }
16750
+ return reports;
16751
+ }
16752
+
16753
+ // src/commands/doctor.ts
16754
+ var INTEGRATION_BRANCH2 = "dev";
16643
16755
  var doctorCommand = new Command25("doctor").description(
16644
16756
  "Report repo-state conditions that make everything read from this checkout unreliable"
16645
16757
  ).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
16758
  "--fix",
16647
16759
  "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);
16760
+ ).option(
16761
+ "--scratch-clones <estateRoot>",
16762
+ "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."
16763
+ ).action(
16764
+ async (options) => {
16765
+ if (options.scratchClones !== void 0) {
16766
+ await runScratchCloneScan(resolve20(options.scratchClones));
16767
+ return;
16768
+ }
16769
+ const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
16770
+ const git = new GitAdapter();
16771
+ try {
16772
+ const facts = await gatherRepoFacts({ cwd, fetch: options.fetch !== false }, { git });
16773
+ const findings = runDoctorChecks(facts);
16774
+ printFindings(findings);
16775
+ if (options.fix === true) {
16776
+ const github = new GithubCliAdapter();
16777
+ const outcomes = await runDoctorFix(cwd, facts, { git, github });
16778
+ printReapOutcomes(outcomes);
16779
+ const branchOutcomes = await runDoctorFixBranches(cwd, facts, { git, github });
16780
+ printBranchReapOutcomes(branchOutcomes);
16781
+ }
16782
+ if (findings.some((f) => f.severity === "error")) process.exit(1);
16783
+ } catch (err) {
16784
+ log.error(err.message);
16785
+ process.exit(1);
16786
+ }
16666
16787
  }
16667
- });
16788
+ );
16668
16789
  async function gatherRepoFacts(options, deps) {
16669
16790
  const { git } = deps;
16670
16791
  if (!await git.isGitRepo(options.cwd)) {
@@ -16680,13 +16801,13 @@ async function gatherRepoFacts(options, deps) {
16680
16801
  const worktrees = await Promise.all(
16681
16802
  worktreePaths.map(async (w) => ({
16682
16803
  ...w,
16683
- behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH}`)
16804
+ behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH2}`)
16684
16805
  }))
16685
16806
  );
16686
16807
  return {
16687
16808
  currentBranch,
16688
16809
  isPrimary,
16689
- integrationBranch: INTEGRATION_BRANCH,
16810
+ integrationBranch: INTEGRATION_BRANCH2,
16690
16811
  ahead,
16691
16812
  behind,
16692
16813
  hasUpstream,
@@ -16697,7 +16818,7 @@ async function gatherRepoFacts(options, deps) {
16697
16818
  // a fault in it.
16698
16819
  localCoreVersion: readLocalCoreVersion(options.cwd),
16699
16820
  remoteCoreVersion: parseCoreRecord(
16700
- await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH}`, INSTANCE_CORE_FILE)
16821
+ await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH2}`, INSTANCE_CORE_FILE)
16701
16822
  ),
16702
16823
  fossilCoreVersion: readFossil(options.cwd),
16703
16824
  branches,
@@ -16719,8 +16840,50 @@ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(),
16719
16840
  remoteBranchNames
16720
16841
  );
16721
16842
  }
16843
+ async function runScratchCloneScan(estateRoot) {
16844
+ const git = new GitAdapter();
16845
+ const github = new GithubCliAdapter();
16846
+ try {
16847
+ const candidates = await findScratchCloneCandidates(estateRoot, { git });
16848
+ const reports = await classifyScratchClones(candidates, { git, github });
16849
+ printScratchCloneReports(estateRoot, reports);
16850
+ } catch (err) {
16851
+ log.error(err.message);
16852
+ process.exit(1);
16853
+ }
16854
+ }
16855
+ function printScratchCloneReports(estateRoot, reports) {
16856
+ if (reports.length === 0) {
16857
+ console.log(
16858
+ chalk21.dim(` scratch-clones: no plain git-clone directory found under ${estateRoot}.
16859
+ `)
16860
+ );
16861
+ return;
16862
+ }
16863
+ const reapable = reports.filter((r) => r.verdict.action === "reap");
16864
+ const kept = reports.filter((r) => r.verdict.action === "keep");
16865
+ console.log("");
16866
+ for (const r of reapable) {
16867
+ console.log(
16868
+ chalk21.yellow(
16869
+ ` 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}'`
16870
+ )
16871
+ );
16872
+ }
16873
+ for (const r of kept) {
16874
+ const reason = r.verdict.reason === void 0 ? "unknown" : KEEP_REASON_TEXT[r.verdict.reason];
16875
+ console.log(chalk21.dim(` kept ${r.candidate.path} (${r.candidate.branch}) \u2014 ${reason}`));
16876
+ }
16877
+ console.log(
16878
+ chalk21.dim(
16879
+ `
16880
+ 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}.
16881
+ `
16882
+ )
16883
+ );
16884
+ }
16722
16885
  function readLocalCoreVersion(cwd) {
16723
- const path = join68(cwd, INSTANCE_CORE_FILE);
16886
+ const path = join69(cwd, INSTANCE_CORE_FILE);
16724
16887
  if (!existsSync56(path)) return null;
16725
16888
  try {
16726
16889
  return extractVersionField(readFileSync47(path, "utf8"));
@@ -16743,7 +16906,7 @@ function extractVersionField(contents) {
16743
16906
  return match?.[1] ?? null;
16744
16907
  }
16745
16908
  function readFossil(cwd) {
16746
- const path = join68(cwd, CORE_VERSION_FILE);
16909
+ const path = join69(cwd, CORE_VERSION_FILE);
16747
16910
  if (!existsSync56(path)) return null;
16748
16911
  try {
16749
16912
  const value = readFileSync47(path, "utf8").trim();
@@ -16783,7 +16946,8 @@ var KEEP_REASON_TEXT = {
16783
16946
  "no-pr": "no PR was ever opened from this branch",
16784
16947
  "unknown-pr-verdict": "could not read this branch's PR state from GitHub",
16785
16948
  "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"
16949
+ "unknown-merge-head": "could not confirm worktree HEAD is contained in what merged",
16950
+ "not-a-git-repository": "the .git directory exists but is not a resolvable, initialised git repository"
16787
16951
  };
16788
16952
  function printReapOutcomes(outcomes) {
16789
16953
  if (outcomes.length === 0) {
@@ -17277,18 +17441,18 @@ function resolveGithubToken4() {
17277
17441
 
17278
17442
  // src/lib/packaged-script-command.ts
17279
17443
  import { spawnSync } from "child_process";
17280
- import { chmodSync as chmodSync2, statSync as statSync18 } from "fs";
17444
+ import { chmodSync as chmodSync2, statSync as statSync19 } from "fs";
17281
17445
  import { dirname as dirname12 } from "path";
17282
17446
  import { fileURLToPath as fileURLToPath6 } from "url";
17283
17447
  import { Command as Command27 } from "commander";
17284
17448
 
17285
17449
  // src/lib/packaged-scripts.ts
17286
17450
  import { existsSync as existsSync57 } from "fs";
17287
- import { dirname as dirname11, join as join69 } from "path";
17451
+ import { dirname as dirname11, join as join70 } from "path";
17288
17452
  function findPackagedScript(startDir, relativePath) {
17289
17453
  let dir = startDir;
17290
17454
  for (; ; ) {
17291
- const candidate = join69(dir, relativePath);
17455
+ const candidate = join70(dir, relativePath);
17292
17456
  if (existsSync57(candidate)) return candidate;
17293
17457
  const parent = dirname11(dir);
17294
17458
  if (parent === dir) return null;
@@ -17303,7 +17467,7 @@ It ships with this package via cli/scripts/packaged-root-assets.mjs; if you are
17303
17467
  // src/lib/packaged-script-command.ts
17304
17468
  function ensureExecutable(script) {
17305
17469
  try {
17306
- const { mode } = statSync18(script);
17470
+ const { mode } = statSync19(script);
17307
17471
  if ((mode & 73) === 0) chmodSync2(script, mode | 493);
17308
17472
  } catch {
17309
17473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.312.5",
3
+ "version": "0.313.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",