arcane-cli 0.16.2 → 0.16.3

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.
@@ -101,7 +101,8 @@ Rules:
101
101
 
102
102
  9. **Stage and commit session-close docs.**
103
103
 
104
- **Remote-capability check:** classify the state created by `spell-open-session` before branching, staging, push, or PR operations.
104
+ **Remote-capability check:** classify the state created by `spell-open-session` before branching, staging, push, or PR operations. Check for **not a repository at all** first — every other classification assumes a `.git` directory exists.
105
+ - **Not a Git repository (EF-05):** `git rev-parse --is-inside-work-tree` fails. Do not attempt any git operation (branch, add, commit, push). Fail closed with: `No Git repository detected. Run \`git init -b main\` first (explicit -b avoids landing on "master" on systems where that's the init.defaultBranch default), then re-run spell-close-session.` This mirrors `spell init`'s own next-steps guidance for the same state.
105
106
  - **Usable supported remote/merge path:** requires an authenticated supported provider (`github.com`, `dev.azure.com`, or `visualstudio.com`). Keep the current compliant session/PR/worktree branch, resolve the actual remote name and integration branch, and use the provider-neutral PR path below.
106
107
  - **No usable remote/merge path:** remain on the current trunk. Do not create a dead session branch, push, pull, or open a PR. Apply the EF-28 interactive commit approval gate, commit locally on trunk after approval, report `Local-only close: committed on <trunk>; no remote PR/pull performed`, and skip steps 9a, 9c, and 10's remote operations.
107
108
  - **Read-only session:** if no repository mutation occurred, do not create a branch or commit.
package/dist/index.js CHANGED
@@ -1751,13 +1751,50 @@ async function inspectGitRepository(cwd) {
1751
1751
  uncommittedChanges: lines.length === 0 ? 0 : lines.split("\n").length
1752
1752
  };
1753
1753
  }
1754
- async function countUncommittedChanges(cwd) {
1754
+ async function correctUnbornMasterDefault(cwd) {
1755
+ let current;
1755
1756
  try {
1756
- const { stdout } = await runGit(cwd, ["status", "--porcelain", "-uall"]);
1757
- const lines = stdout.trim();
1758
- return lines.length === 0 ? 0 : lines.split("\n").length;
1757
+ const { stdout } = await runGit(cwd, ["symbolic-ref", "--short", "HEAD"]);
1758
+ current = stdout.trim();
1759
1759
  } catch {
1760
- return 0;
1760
+ return { corrected: false, to: "main" };
1761
+ }
1762
+ if (current !== "master") {
1763
+ return { corrected: false, to: "main" };
1764
+ }
1765
+ try {
1766
+ await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
1767
+ return { corrected: false, to: "main" };
1768
+ } catch {
1769
+ }
1770
+ try {
1771
+ await runGit(cwd, ["rev-parse", "--verify", "refs/heads/main"]);
1772
+ return { corrected: false, to: "main" };
1773
+ } catch {
1774
+ }
1775
+ await runGit(cwd, ["symbolic-ref", "HEAD", "refs/heads/main"]);
1776
+ return { corrected: true, from: current, to: "main" };
1777
+ }
1778
+ async function ensureLocalPullRebase(cwd) {
1779
+ let rawLocalValue;
1780
+ try {
1781
+ const { stdout } = await runGit(cwd, ["config", "--local", "--get", "pull.rebase"]);
1782
+ rawLocalValue = stdout.trim();
1783
+ } catch {
1784
+ rawLocalValue = void 0;
1785
+ }
1786
+ if (rawLocalValue === void 0) {
1787
+ await runGit(cwd, ["config", "--local", "pull.rebase", "true"]);
1788
+ return { action: "set" };
1789
+ }
1790
+ try {
1791
+ const { stdout } = await runGit(cwd, ["config", "--local", "--type=bool", "--get", "pull.rebase"]);
1792
+ if (stdout.trim() === "false") {
1793
+ return { action: "explicit-false-preserved" };
1794
+ }
1795
+ return { action: "already-set" };
1796
+ } catch {
1797
+ return { action: "already-set" };
1761
1798
  }
1762
1799
  }
1763
1800
 
@@ -1837,21 +1874,42 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
1837
1874
  console.log(chalk2.hex("#a855f7")(`
1838
1875
  \u2726 Arcane v${packageVersion}
1839
1876
  `));
1877
+ let repoNotFound = false;
1840
1878
  if (!options.dryRun) {
1841
- const dirtyCount = await countUncommittedChanges(targetDir);
1842
- if (dirtyCount > 0) {
1843
- printWarning(`This repo has ${dirtyCount} uncommitted change${dirtyCount === 1 ? "" : "s"}.`);
1844
- printInfo("Running spell init will add new files to your working tree.");
1845
- console.log();
1846
- const continueAnyway = await confirm({
1847
- message: "Continue anyway?",
1848
- default: true
1849
- });
1850
- if (!continueAnyway) {
1851
- console.log("\nCancelled. Commit or stash your changes first.");
1852
- return;
1879
+ const gitState = await inspectGitRepository(targetDir);
1880
+ if (gitState.status === "not-repository") {
1881
+ repoNotFound = true;
1882
+ } else {
1883
+ if (gitState.status === "no-commits") {
1884
+ const correction = await correctUnbornMasterDefault(targetDir);
1885
+ if (correction.corrected) {
1886
+ printInfo(
1887
+ `Repointed the unborn branch from "${correction.from}" to "${correction.to}" (avoids the Git for Windows default-branch trap).`
1888
+ );
1889
+ }
1890
+ }
1891
+ if (gitState.status === "ready" && gitState.uncommittedChanges > 0) {
1892
+ printWarning(
1893
+ `This repo has ${gitState.uncommittedChanges} uncommitted change${gitState.uncommittedChanges === 1 ? "" : "s"}.`
1894
+ );
1895
+ printInfo("Running spell init will add new files to your working tree.");
1896
+ console.log();
1897
+ const continueAnyway = await confirm({
1898
+ message: "Continue anyway?",
1899
+ default: true
1900
+ });
1901
+ if (!continueAnyway) {
1902
+ console.log("\nCancelled. Commit or stash your changes first.");
1903
+ return;
1904
+ }
1905
+ console.log();
1906
+ }
1907
+ const rebaseResult = await ensureLocalPullRebase(targetDir);
1908
+ if (rebaseResult.action === "explicit-false-preserved") {
1909
+ printWarning(
1910
+ "This repo's pull.rebase is explicitly set to false, but Arcane's governance mandates rebase-and-fast-forward (see .arcane/governance/git-conventions.md). Left as-is since you set it explicitly."
1911
+ );
1853
1912
  }
1854
- console.log();
1855
1913
  }
1856
1914
  }
1857
1915
  let profile = options.profile;
@@ -2000,6 +2058,12 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
2000
2058
  "Review .arcane/governance/ docs and customize for your project"
2001
2059
  );
2002
2060
  }
2061
+ if (repoNotFound) {
2062
+ printNextStep(
2063
+ step++,
2064
+ 'Initialize Git for this project: `git init -b main` (avoids defaulting to "master" on systems where that\'s the system default)'
2065
+ );
2066
+ }
2003
2067
  printNextStep(
2004
2068
  step++,
2005
2069
  "Commit the new files: git add .arcane .github .claude AGENTS.md CLAUDE.md .arcane.json"
@@ -2680,6 +2744,29 @@ async function checkArcaneManifest(targetDir) {
2680
2744
  };
2681
2745
  }
2682
2746
  }
2747
+ async function checkPullRebase(targetDir) {
2748
+ const name = "Git pull.rebase (rebase-and-fast-forward mandate)";
2749
+ try {
2750
+ const { stdout } = await runGit(targetDir, ["config", "--type=bool", "--get", "pull.rebase"]);
2751
+ const value = stdout.trim();
2752
+ if (value === "true") {
2753
+ return { name, passed: true, message: "pull.rebase=true" };
2754
+ }
2755
+ return {
2756
+ name,
2757
+ passed: false,
2758
+ message: `pull.rebase resolves to "${value}", not "true" -- a bare \`git pull\` will create merge commits, contradicting git-conventions.md's rebase-and-fast-forward mandate. Run: git config --local pull.rebase true`,
2759
+ blocking: false
2760
+ };
2761
+ } catch {
2762
+ return {
2763
+ name,
2764
+ passed: false,
2765
+ message: "pull.rebase is unset, not a recognized boolean, or this isn't a Git repository -- effective behavior may not be rebase-and-fast-forward, and Git for Windows defaults this to false when unset. Run: git config --local pull.rebase true",
2766
+ blocking: false
2767
+ };
2768
+ }
2769
+ }
2683
2770
  function checkIncidentReleaseGate() {
2684
2771
  const result = evaluateIncidentGate(INCIDENT_QUEUE);
2685
2772
  if (result.blocked) {
@@ -2742,7 +2829,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
2742
2829
  checkNodeVersion(),
2743
2830
  checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)"),
2744
2831
  checkArcaneManifest(targetDir),
2745
- Promise.resolve(checkIncidentReleaseGate())
2832
+ Promise.resolve(checkIncidentReleaseGate()),
2833
+ checkPullRebase(targetDir)
2746
2834
  ]);
2747
2835
  const sessionResults = await checkSessionContinuity(targetDir);
2748
2836
  results.push(...sessionResults);