@biffo/cli 0.68.1 → 0.68.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 +48 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -516,6 +516,26 @@ var GitAdapter = class {
516
516
  const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
517
517
  return stdout.trim().length > 0;
518
518
  }
519
+ /** Best-effort fetch of the tracking remote, so the ahead/behind check below
520
+ * compares against reality rather than a stale local ref (#394). Never throws:
521
+ * offline or a missing remote must not block an upgrade on its own — the
522
+ * ahead/behind check that follows simply works from whatever is local. */
523
+ async fetch(cwd, remote = "origin") {
524
+ await execa("git", ["fetch", "--quiet", remote], { cwd, reject: false });
525
+ }
526
+ /** HEAD's position relative to its upstream. `hasUpstream` is false when the
527
+ * branch tracks nothing (or HEAD is detached), in which case currency cannot
528
+ * be established and ahead/behind are 0 (#394). */
529
+ async aheadBehind(cwd) {
530
+ const { stdout, exitCode } = await execa(
531
+ "git",
532
+ ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"],
533
+ { cwd, reject: false }
534
+ );
535
+ if (exitCode !== 0) return { ahead: 0, behind: 0, hasUpstream: false };
536
+ const [ahead, behind] = stdout.trim().split(/\s+/).map((n) => Number.parseInt(n, 10));
537
+ return { ahead: ahead ?? 0, behind: behind ?? 0, hasUpstream: true };
538
+ }
519
539
  /** The fetch URL of a remote (default "origin"). */
520
540
  async getRemoteUrl(cwd, remote = "origin") {
521
541
  const { stdout } = await execa("git", ["remote", "get-url", remote], { cwd });
@@ -2031,6 +2051,9 @@ var coreUpgradeCommand = new Command3("upgrade").description("Three-way-merge te
2031
2051
  ).option("--apply", "Apply the plan on a new branch and open a PR (default: dry run)").option("--allow-conflicts", "With --apply, open the PR even if some files conflict").option(
2032
2052
  "--acknowledge-breaking",
2033
2053
  "With --apply, proceed even though this upgrade crosses a documented breaking change"
2054
+ ).option(
2055
+ "--allow-dirty",
2056
+ "Compute the plan even with uncommitted changes in the instance tree (they become part of the merge)"
2034
2057
  ).option("--base <branch>", "Base branch for the PR (defaults to the repo\u2019s default branch)").option("--remote <name>", "Git remote to push to and open the PR on (default: origin)").action(
2035
2058
  async (options) => {
2036
2059
  const cwd = options.cwd ? resolve3(options.cwd) : process.cwd();
@@ -2038,7 +2061,8 @@ var coreUpgradeCommand = new Command3("upgrade").description("Three-way-merge te
2038
2061
  cwd,
2039
2062
  apply: options.apply ?? false,
2040
2063
  allowConflicts: options.allowConflicts ?? false,
2041
- acknowledgeBreaking: options.acknowledgeBreaking ?? false
2064
+ acknowledgeBreaking: options.acknowledgeBreaking ?? false,
2065
+ allowDirty: options.allowDirty ?? false
2042
2066
  };
2043
2067
  if (options.templateRepo) runOptions.templateRepo = resolve3(options.templateRepo);
2044
2068
  if (options.to) runOptions.toVersion = options.to;
@@ -2083,7 +2107,30 @@ async function runCoreUpgrade(options, deps = defaultDeps()) {
2083
2107
  for (const c of cleanups) c();
2084
2108
  }
2085
2109
  }
2110
+ async function checkInstanceCurrency(git, cwd, allowDirty) {
2111
+ if (!await git.isGitRepo(cwd)) return;
2112
+ const branch = await git.currentBranch(cwd);
2113
+ if (branch === "HEAD" || branch === "") {
2114
+ throw new Error(
2115
+ `${cwd} has a detached HEAD, so the tree the plan would merge as "ours" has no branch identity. Check out the integration branch first (#394).`
2116
+ );
2117
+ }
2118
+ await git.fetch(cwd);
2119
+ const { ahead, behind, hasUpstream } = await git.aheadBehind(cwd);
2120
+ if (hasUpstream && behind > 0) {
2121
+ const diverged = ahead > 0 ? ` and ${ahead} ahead (diverged)` : "";
2122
+ throw new Error(
2123
+ `${cwd} is ${behind} commit(s) behind its upstream${diverged}. The plan would merge onto a stale tree and propose reintroducing superseded content. Run \`git pull\` first, or fix the divergence, then re-run (#394).`
2124
+ );
2125
+ }
2126
+ if (!allowDirty && await git.hasUncommittedChanges(cwd)) {
2127
+ throw new Error(
2128
+ `${cwd} has uncommitted changes, which would silently become part of the merge's "ours" side. Commit or stash them, or pass --allow-dirty to accept them deliberately (#394).`
2129
+ );
2130
+ }
2131
+ }
2086
2132
  async function runCoreUpgradeResolved(options, deps, cleanups) {
2133
+ await checkInstanceCurrency(deps.git, options.cwd, options.allowDirty ?? false);
2087
2134
  const materialize = deps.materialize ?? materializeTemplateAtTag;
2088
2135
  const templateRepo = options.templateRepo ? resolve3(options.templateRepo) : resolveTemplateRoot({ guidance: MISSING_TEMPLATE_ROOT_GUIDANCE2 });
2089
2136
  const instanceVersion = readInstanceCoreVersion(options.cwd);
@@ -2181,9 +2228,6 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
2181
2228
  if (!await git.isGitRepo(options.cwd)) {
2182
2229
  throw new Error(`${options.cwd} is not a git repository.`);
2183
2230
  }
2184
- if (await git.hasUncommittedChanges(options.cwd)) {
2185
- throw new Error("The working tree has uncommitted changes. Commit or stash them first.");
2186
- }
2187
2231
  const branch = upgradeBranchName(fromVersion, toVersion);
2188
2232
  log.step(1, 4, `Creating branch ${branch}`);
2189
2233
  await git.createBranch(options.cwd, branch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.68.1",
3
+ "version": "0.68.2",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",