contribute-now 0.2.0-dev.33be40f → 0.2.0-dev.69b11fd

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 +698 -320
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // src/index.ts
4
- import { defineCommand as defineCommand11, runMain } from "citty";
6
+ import { defineCommand as defineCommand12, runMain } from "citty";
5
7
 
6
8
  // src/commands/clean.ts
7
9
  import { defineCommand } from "citty";
@@ -30,6 +32,9 @@ var CONFIG_FILENAME = ".contributerc.json";
30
32
  function getConfigPath(cwd = process.cwd()) {
31
33
  return join(cwd, CONFIG_FILENAME);
32
34
  }
35
+ function configExists(cwd = process.cwd()) {
36
+ return existsSync(getConfigPath(cwd));
37
+ }
33
38
  function readConfig(cwd = process.cwd()) {
34
39
  const path = getConfigPath(cwd);
35
40
  if (!existsSync(path))
@@ -638,14 +643,60 @@ async function rebase(branch) {
638
643
  return run2(["rebase", branch]);
639
644
  }
640
645
  async function getUpstreamRef() {
641
- const { exitCode, stdout } = await run2(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
646
+ const { exitCode, stdout } = await run2([
647
+ "rev-parse",
648
+ "--abbrev-ref",
649
+ "--symbolic-full-name",
650
+ "@{u}"
651
+ ]);
642
652
  if (exitCode !== 0)
643
653
  return null;
644
654
  return stdout.trim() || null;
645
655
  }
656
+ async function unsetUpstream() {
657
+ return run2(["branch", "--unset-upstream"]);
658
+ }
646
659
  async function rebaseOnto(newBase, oldBase) {
647
660
  return run2(["rebase", "--onto", newBase, oldBase]);
648
661
  }
662
+ async function getMergeBase(ref1, ref2) {
663
+ const { exitCode, stdout } = await run2(["merge-base", ref1, ref2]);
664
+ if (exitCode !== 0)
665
+ return null;
666
+ return stdout.trim() || null;
667
+ }
668
+ async function getCommitHash(ref) {
669
+ const { exitCode, stdout } = await run2(["rev-parse", ref]);
670
+ if (exitCode !== 0)
671
+ return null;
672
+ return stdout.trim() || null;
673
+ }
674
+ async function determineRebaseStrategy(currentBranch, syncRef) {
675
+ const upstreamRef = await getUpstreamRef();
676
+ if (!upstreamRef) {
677
+ return { strategy: "plain" };
678
+ }
679
+ const upstreamHash = await getCommitHash(upstreamRef);
680
+ if (!upstreamHash) {
681
+ return { strategy: "plain" };
682
+ }
683
+ const slashIdx = upstreamRef.indexOf("/");
684
+ const upstreamBranchName = slashIdx !== -1 ? upstreamRef.slice(slashIdx + 1) : upstreamRef;
685
+ if (upstreamBranchName === currentBranch) {
686
+ return { strategy: "plain" };
687
+ }
688
+ const [forkFromUpstream, forkFromSync] = await Promise.all([
689
+ getMergeBase("HEAD", upstreamRef),
690
+ getMergeBase("HEAD", syncRef)
691
+ ]);
692
+ if (forkFromUpstream && forkFromSync && forkFromUpstream === forkFromSync) {
693
+ return { strategy: "plain" };
694
+ }
695
+ if (forkFromUpstream) {
696
+ return { strategy: "onto", ontoOldBase: forkFromUpstream };
697
+ }
698
+ return { strategy: "plain" };
699
+ }
649
700
  async function getStagedDiff() {
650
701
  const { stdout } = await run2(["diff", "--cached"]);
651
702
  return stdout;
@@ -1565,11 +1616,374 @@ ${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
1565
1616
  process.exit(0);
1566
1617
  }
1567
1618
 
1619
+ // src/commands/doctor.ts
1620
+ import { execFile as execFileCb3 } from "node:child_process";
1621
+ import { defineCommand as defineCommand3 } from "citty";
1622
+ import pc6 from "picocolors";
1623
+ // package.json
1624
+ var package_default = {
1625
+ name: "contribute-now",
1626
+ version: "0.2.0-dev.69b11fd",
1627
+ description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
1628
+ type: "module",
1629
+ bin: {
1630
+ contrib: "dist/index.js",
1631
+ contribute: "dist/index.js"
1632
+ },
1633
+ files: [
1634
+ "dist"
1635
+ ],
1636
+ scripts: {
1637
+ build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
1638
+ cli: "bun run src/index.ts --",
1639
+ dev: "bun src/index.ts",
1640
+ test: "bun test",
1641
+ lint: "biome check .",
1642
+ "lint:fix": "biome check --write .",
1643
+ format: "biome format --write .",
1644
+ "www:dev": "bun run --cwd www dev",
1645
+ "www:build": "bun run --cwd www build",
1646
+ "www:preview": "bun run --cwd www preview"
1647
+ },
1648
+ engines: {
1649
+ node: ">=18",
1650
+ bun: ">=1.0"
1651
+ },
1652
+ keywords: [
1653
+ "git",
1654
+ "workflow",
1655
+ "squash-merge",
1656
+ "sync",
1657
+ "cli",
1658
+ "contribute",
1659
+ "fork",
1660
+ "dev-branch",
1661
+ "clean-commit"
1662
+ ],
1663
+ author: "Waren Gonzaga",
1664
+ license: "GPL-3.0",
1665
+ repository: {
1666
+ type: "git",
1667
+ url: "git+https://github.com/warengonzaga/contribute-now.git"
1668
+ },
1669
+ dependencies: {
1670
+ "@clack/prompts": "^1.0.1",
1671
+ "@github/copilot-sdk": "^0.1.25",
1672
+ "@wgtechlabs/log-engine": "^2.3.1",
1673
+ citty: "^0.1.6",
1674
+ figlet: "^1.10.0",
1675
+ picocolors: "^1.1.1"
1676
+ },
1677
+ devDependencies: {
1678
+ "@biomejs/biome": "^2.4.4",
1679
+ "@types/bun": "latest",
1680
+ "@types/figlet": "^1.7.0",
1681
+ typescript: "^5.7.0"
1682
+ }
1683
+ };
1684
+
1685
+ // src/utils/remote.ts
1686
+ function parseRepoFromUrl(url) {
1687
+ const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
1688
+ if (httpsMatch) {
1689
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
1690
+ }
1691
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
1692
+ if (sshMatch) {
1693
+ return { owner: sshMatch[1], repo: sshMatch[2] };
1694
+ }
1695
+ return null;
1696
+ }
1697
+ async function detectForkSetup() {
1698
+ const remotes = await getRemotes();
1699
+ const hasOrigin = remotes.includes("origin");
1700
+ const hasUpstream = remotes.includes("upstream");
1701
+ return {
1702
+ isFork: hasUpstream,
1703
+ originRemote: hasOrigin ? "origin" : null,
1704
+ upstreamRemote: hasUpstream ? "upstream" : null
1705
+ };
1706
+ }
1707
+ async function getRepoInfoFromRemote(remote = "origin") {
1708
+ const url = await getRemoteUrl(remote);
1709
+ if (!url)
1710
+ return null;
1711
+ return parseRepoFromUrl(url);
1712
+ }
1713
+
1714
+ // src/commands/doctor.ts
1715
+ var PASS = ` ${pc6.green("✔")} `;
1716
+ var FAIL = ` ${pc6.red("✗")} `;
1717
+ var WARN = ` ${pc6.yellow("⚠")} `;
1718
+ function printReport(report) {
1719
+ for (const section of report.sections) {
1720
+ console.log(`
1721
+ ${pc6.bold(pc6.underline(section.title))}`);
1722
+ for (const check of section.checks) {
1723
+ const prefix = check.ok ? check.warning ? WARN : PASS : FAIL;
1724
+ const text2 = check.detail ? `${check.label} ${pc6.dim(`— ${check.detail}`)}` : check.label;
1725
+ console.log(`${prefix}${text2}`);
1726
+ }
1727
+ }
1728
+ console.log();
1729
+ }
1730
+ function toJson(report) {
1731
+ return JSON.stringify(report.sections.map((s) => ({
1732
+ section: s.title,
1733
+ checks: s.checks.map((c) => ({
1734
+ label: c.label,
1735
+ ok: c.ok,
1736
+ warning: c.warning ?? false,
1737
+ detail: c.detail ?? null
1738
+ }))
1739
+ })), null, 2);
1740
+ }
1741
+ function runCmd(cmd, args) {
1742
+ return new Promise((resolve) => {
1743
+ execFileCb3(cmd, args, (error2, stdout) => {
1744
+ resolve({
1745
+ ok: !error2,
1746
+ stdout: (stdout ?? "").trim()
1747
+ });
1748
+ });
1749
+ });
1750
+ }
1751
+ async function toolSection() {
1752
+ const checks = [];
1753
+ checks.push({
1754
+ label: `contrib v${package_default.version ?? "unknown"}`,
1755
+ ok: true
1756
+ });
1757
+ const runtime = typeof globalThis.Bun !== "undefined" ? `Bun ${globalThis.Bun.version ?? "?"}` : `Node ${process.version}`;
1758
+ checks.push({ label: runtime, ok: true, detail: `${process.platform}-${process.arch}` });
1759
+ return { title: "Tool", checks };
1760
+ }
1761
+ async function depsSection() {
1762
+ const checks = [];
1763
+ const git = await runCmd("git", ["--version"]);
1764
+ checks.push({
1765
+ label: git.ok ? git.stdout.replace("git version ", "git ") : "git not found",
1766
+ ok: git.ok
1767
+ });
1768
+ const ghInstalled = await checkGhInstalled();
1769
+ if (ghInstalled) {
1770
+ const ghVer = await runCmd("gh", ["--version"]);
1771
+ const ver = ghVer.stdout.split(`
1772
+ `)[0] ?? "gh";
1773
+ checks.push({ label: ver, ok: true });
1774
+ const ghAuth = await checkGhAuth();
1775
+ checks.push({
1776
+ label: ghAuth ? "gh authenticated" : "gh not authenticated",
1777
+ ok: ghAuth,
1778
+ warning: !ghAuth,
1779
+ detail: ghAuth ? undefined : "run `gh auth login`"
1780
+ });
1781
+ } else {
1782
+ checks.push({
1783
+ label: "gh CLI not installed",
1784
+ ok: false,
1785
+ detail: "install from https://cli.github.com"
1786
+ });
1787
+ }
1788
+ try {
1789
+ await import("@github/copilot-sdk");
1790
+ checks.push({ label: "Copilot SDK importable", ok: true });
1791
+ } catch {
1792
+ checks.push({
1793
+ label: "Copilot SDK not loadable",
1794
+ ok: false,
1795
+ warning: true,
1796
+ detail: "AI features will be unavailable"
1797
+ });
1798
+ }
1799
+ return { title: "Dependencies", checks };
1800
+ }
1801
+ async function configSection() {
1802
+ const checks = [];
1803
+ const exists = configExists();
1804
+ if (!exists) {
1805
+ checks.push({
1806
+ label: ".contributerc.json not found",
1807
+ ok: false,
1808
+ detail: "run `contrib setup` to create it"
1809
+ });
1810
+ return { title: "Config", checks };
1811
+ }
1812
+ const config = readConfig();
1813
+ if (!config) {
1814
+ checks.push({ label: ".contributerc.json found but invalid", ok: false });
1815
+ return { title: "Config", checks };
1816
+ }
1817
+ checks.push({ label: ".contributerc.json found and valid", ok: true });
1818
+ const desc = WORKFLOW_DESCRIPTIONS[config.workflow] ?? config.workflow;
1819
+ checks.push({
1820
+ label: `Workflow: ${config.workflow}`,
1821
+ ok: true,
1822
+ detail: desc
1823
+ });
1824
+ checks.push({ label: `Role: ${config.role}`, ok: true });
1825
+ checks.push({ label: `Commit convention: ${config.commitConvention}`, ok: true });
1826
+ if (hasDevBranch(config.workflow)) {
1827
+ checks.push({
1828
+ label: `Dev branch: ${config.devBranch ?? "(not set)"}`,
1829
+ ok: !!config.devBranch
1830
+ });
1831
+ }
1832
+ const ignored = isGitignored();
1833
+ checks.push({
1834
+ label: ignored ? ".contributerc.json in .gitignore" : ".contributerc.json NOT in .gitignore",
1835
+ ok: true,
1836
+ warning: !ignored,
1837
+ detail: ignored ? undefined : "consider adding it to .gitignore"
1838
+ });
1839
+ return { title: "Config", checks };
1840
+ }
1841
+ async function gitSection() {
1842
+ const checks = [];
1843
+ const inRepo = await isGitRepo();
1844
+ checks.push({
1845
+ label: inRepo ? "Inside a git repository" : "Not inside a git repository",
1846
+ ok: inRepo
1847
+ });
1848
+ if (!inRepo)
1849
+ return { title: "Git Environment", checks };
1850
+ const branch = await getCurrentBranch();
1851
+ const head = await runCmd("git", ["rev-parse", "--short", "HEAD"]);
1852
+ checks.push({
1853
+ label: `Branch: ${branch ?? "(detached)"}`,
1854
+ ok: !!branch,
1855
+ detail: head.ok ? `HEAD ${head.stdout}` : undefined
1856
+ });
1857
+ const remotes = await getRemotes();
1858
+ if (remotes.length === 0) {
1859
+ checks.push({ label: "No remotes configured", ok: false, warning: true });
1860
+ } else {
1861
+ for (const remote of remotes) {
1862
+ const url = await getRemoteUrl(remote);
1863
+ const repoInfo = url ? parseRepoFromUrl(url) : null;
1864
+ const detail = repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : url ?? "unknown URL";
1865
+ checks.push({ label: `Remote: ${remote}`, ok: true, detail });
1866
+ }
1867
+ }
1868
+ const dirty = await hasUncommittedChanges();
1869
+ checks.push({
1870
+ label: dirty ? "Uncommitted changes detected" : "Working tree clean",
1871
+ ok: true,
1872
+ warning: dirty
1873
+ });
1874
+ return { title: "Git Environment", checks };
1875
+ }
1876
+ async function forkSection() {
1877
+ const checks = [];
1878
+ const fork = await detectForkSetup();
1879
+ checks.push({
1880
+ label: fork.isFork ? "Fork detected (upstream remote exists)" : "Not a fork (no upstream remote)",
1881
+ ok: true
1882
+ });
1883
+ if (fork.originRemote) {
1884
+ checks.push({ label: `Origin remote: ${fork.originRemote}`, ok: true });
1885
+ }
1886
+ if (fork.upstreamRemote) {
1887
+ checks.push({ label: `Upstream remote: ${fork.upstreamRemote}`, ok: true });
1888
+ }
1889
+ return { title: "Fork Detection", checks };
1890
+ }
1891
+ async function workflowSection() {
1892
+ const checks = [];
1893
+ const config = readConfig();
1894
+ if (!config) {
1895
+ checks.push({
1896
+ label: "Cannot resolve workflow (no config)",
1897
+ ok: false,
1898
+ detail: "run `contrib setup` first"
1899
+ });
1900
+ return { title: "Workflow Resolution", checks };
1901
+ }
1902
+ const baseBranch = getBaseBranch(config);
1903
+ checks.push({ label: `Base branch: ${baseBranch}`, ok: true });
1904
+ const sync = getSyncSource(config);
1905
+ checks.push({
1906
+ label: `Sync source: ${sync.ref}`,
1907
+ ok: true,
1908
+ detail: `strategy: ${sync.strategy}`
1909
+ });
1910
+ checks.push({
1911
+ label: `Branch prefixes: ${config.branchPrefixes.join(", ")}`,
1912
+ ok: config.branchPrefixes.length > 0
1913
+ });
1914
+ return { title: "Workflow Resolution", checks };
1915
+ }
1916
+ function envSection() {
1917
+ const checks = [];
1918
+ const vars = ["GITHUB_TOKEN", "GH_TOKEN", "COPILOT_AGENT_TOKEN", "NO_COLOR", "FORCE_COLOR", "CI"];
1919
+ for (const name of vars) {
1920
+ const val = process.env[name];
1921
+ if (val !== undefined) {
1922
+ const isSecret = name.toLowerCase().includes("token");
1923
+ const display = isSecret ? `${val.slice(0, 4)}${"*".repeat(Math.min(val.length - 4, 12))}` : val;
1924
+ checks.push({ label: `${name} = ${display}`, ok: true });
1925
+ }
1926
+ }
1927
+ if (checks.length === 0) {
1928
+ checks.push({ label: "No relevant environment variables set", ok: true });
1929
+ }
1930
+ return { title: "Environment", checks };
1931
+ }
1932
+ var doctor_default = defineCommand3({
1933
+ meta: {
1934
+ name: "doctor",
1935
+ description: "Diagnose the contribute-now CLI environment and configuration"
1936
+ },
1937
+ args: {
1938
+ json: {
1939
+ type: "boolean",
1940
+ description: "Output report as JSON",
1941
+ default: false
1942
+ }
1943
+ },
1944
+ async run({ args }) {
1945
+ const isJson = args.json;
1946
+ const [tool, deps, config, git, fork, workflow] = await Promise.all([
1947
+ toolSection(),
1948
+ depsSection(),
1949
+ configSection(),
1950
+ gitSection(),
1951
+ forkSection(),
1952
+ workflowSection()
1953
+ ]);
1954
+ const env = envSection();
1955
+ const report = {
1956
+ sections: [tool, deps, config, git, fork, workflow, env]
1957
+ };
1958
+ if (isJson) {
1959
+ console.log(toJson(report));
1960
+ return;
1961
+ }
1962
+ heading("\uD83E\uDE7A contribute-now doctor");
1963
+ printReport(report);
1964
+ const total = report.sections.flatMap((s) => s.checks);
1965
+ const failures = total.filter((c) => !c.ok);
1966
+ const warnings = total.filter((c) => c.ok && c.warning);
1967
+ if (failures.length === 0 && warnings.length === 0) {
1968
+ console.log(` ${pc6.green("All checks passed!")} No issues detected.
1969
+ `);
1970
+ } else {
1971
+ if (failures.length > 0) {
1972
+ console.log(` ${pc6.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
1973
+ }
1974
+ if (warnings.length > 0) {
1975
+ console.log(` ${pc6.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
1976
+ }
1977
+ console.log();
1978
+ }
1979
+ }
1980
+ });
1981
+
1568
1982
  // src/commands/hook.ts
1569
1983
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1570
1984
  import { join as join3 } from "node:path";
1571
- import { defineCommand as defineCommand3 } from "citty";
1572
- import pc6 from "picocolors";
1985
+ import { defineCommand as defineCommand4 } from "citty";
1986
+ import pc7 from "picocolors";
1573
1987
  var HOOK_MARKER = "# managed by contribute-now";
1574
1988
  function getHooksDir(cwd = process.cwd()) {
1575
1989
  return join3(cwd, ".git", "hooks");
@@ -1607,7 +2021,7 @@ else
1607
2021
  fi
1608
2022
  `;
1609
2023
  }
1610
- var hook_default = defineCommand3({
2024
+ var hook_default = defineCommand4({
1611
2025
  meta: {
1612
2026
  name: "hook",
1613
2027
  description: "Install or uninstall the commit-msg git hook"
@@ -1665,8 +2079,8 @@ async function installHook() {
1665
2079
  }
1666
2080
  writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
1667
2081
  success(`commit-msg hook installed.`);
1668
- info(`Convention: ${pc6.bold(CONVENTION_LABELS[config.commitConvention])}`);
1669
- info(`Path: ${pc6.dim(hookPath)}`);
2082
+ info(`Convention: ${pc7.bold(CONVENTION_LABELS[config.commitConvention])}`);
2083
+ info(`Path: ${pc7.dim(hookPath)}`);
1670
2084
  }
1671
2085
  async function uninstallHook() {
1672
2086
  heading("\uD83E\uDE9D hook uninstall");
@@ -1685,30 +2099,9 @@ async function uninstallHook() {
1685
2099
  }
1686
2100
 
1687
2101
  // src/commands/setup.ts
1688
- import { defineCommand as defineCommand4 } from "citty";
1689
- import pc7 from "picocolors";
1690
-
1691
- // src/utils/remote.ts
1692
- function parseRepoFromUrl(url) {
1693
- const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
1694
- if (httpsMatch) {
1695
- return { owner: httpsMatch[1], repo: httpsMatch[2] };
1696
- }
1697
- const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
1698
- if (sshMatch) {
1699
- return { owner: sshMatch[1], repo: sshMatch[2] };
1700
- }
1701
- return null;
1702
- }
1703
- async function getRepoInfoFromRemote(remote = "origin") {
1704
- const url = await getRemoteUrl(remote);
1705
- if (!url)
1706
- return null;
1707
- return parseRepoFromUrl(url);
1708
- }
1709
-
1710
- // src/commands/setup.ts
1711
- var setup_default = defineCommand4({
2102
+ import { defineCommand as defineCommand5 } from "citty";
2103
+ import pc8 from "picocolors";
2104
+ var setup_default = defineCommand5({
1712
2105
  meta: {
1713
2106
  name: "setup",
1714
2107
  description: "Initialize contribute-now config for this repo (.contributerc.json)"
@@ -1729,7 +2122,7 @@ var setup_default = defineCommand4({
1729
2122
  workflow = "github-flow";
1730
2123
  else if (workflowChoice.startsWith("Git Flow"))
1731
2124
  workflow = "git-flow";
1732
- info(`Workflow: ${pc7.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
2125
+ info(`Workflow: ${pc8.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
1733
2126
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
1734
2127
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
1735
2128
  CONVENTION_DESCRIPTIONS.conventional,
@@ -1782,8 +2175,8 @@ var setup_default = defineCommand4({
1782
2175
  detectedRole = roleChoice;
1783
2176
  detectionSource = "user selection";
1784
2177
  } else {
1785
- info(`Detected role: ${pc7.bold(detectedRole)} (via ${detectionSource})`);
1786
- const confirmed = await confirmPrompt(`Role detected as ${pc7.bold(detectedRole)}. Is this correct?`);
2178
+ info(`Detected role: ${pc8.bold(detectedRole)} (via ${detectionSource})`);
2179
+ const confirmed = await confirmPrompt(`Role detected as ${pc8.bold(detectedRole)}. Is this correct?`);
1787
2180
  if (!confirmed) {
1788
2181
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
1789
2182
  detectedRole = roleChoice;
@@ -1828,22 +2221,22 @@ var setup_default = defineCommand4({
1828
2221
  warn(' echo ".contributerc.json" >> .gitignore');
1829
2222
  }
1830
2223
  console.log();
1831
- info(`Workflow: ${pc7.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
1832
- info(`Convention: ${pc7.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
1833
- info(`Role: ${pc7.bold(config.role)}`);
2224
+ info(`Workflow: ${pc8.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
2225
+ info(`Convention: ${pc8.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
2226
+ info(`Role: ${pc8.bold(config.role)}`);
1834
2227
  if (config.devBranch) {
1835
- info(`Main: ${pc7.bold(config.mainBranch)} | Dev: ${pc7.bold(config.devBranch)}`);
2228
+ info(`Main: ${pc8.bold(config.mainBranch)} | Dev: ${pc8.bold(config.devBranch)}`);
1836
2229
  } else {
1837
- info(`Main: ${pc7.bold(config.mainBranch)}`);
2230
+ info(`Main: ${pc8.bold(config.mainBranch)}`);
1838
2231
  }
1839
- info(`Origin: ${pc7.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc7.bold(config.upstream)}` : ""}`);
2232
+ info(`Origin: ${pc8.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc8.bold(config.upstream)}` : ""}`);
1840
2233
  }
1841
2234
  });
1842
2235
 
1843
2236
  // src/commands/start.ts
1844
- import { defineCommand as defineCommand5 } from "citty";
1845
- import pc8 from "picocolors";
1846
- var start_default = defineCommand5({
2237
+ import { defineCommand as defineCommand6 } from "citty";
2238
+ import pc9 from "picocolors";
2239
+ var start_default = defineCommand6({
1847
2240
  meta: {
1848
2241
  name: "start",
1849
2242
  description: "Create a new feature branch from the latest base branch"
@@ -1890,8 +2283,8 @@ var start_default = defineCommand5({
1890
2283
  if (suggested) {
1891
2284
  spinner.success("Branch name suggestion ready.");
1892
2285
  console.log(`
1893
- ${pc8.dim("AI suggestion:")} ${pc8.bold(pc8.cyan(suggested))}`);
1894
- const accepted = await confirmPrompt(`Use ${pc8.bold(suggested)} as your branch name?`);
2286
+ ${pc9.dim("AI suggestion:")} ${pc9.bold(pc9.cyan(suggested))}`);
2287
+ const accepted = await confirmPrompt(`Use ${pc9.bold(suggested)} as your branch name?`);
1895
2288
  if (accepted) {
1896
2289
  branchName = suggested;
1897
2290
  } else {
@@ -1902,14 +2295,14 @@ var start_default = defineCommand5({
1902
2295
  }
1903
2296
  }
1904
2297
  if (!hasPrefix(branchName, branchPrefixes)) {
1905
- const prefix = await selectPrompt(`Choose a branch type for ${pc8.bold(branchName)}:`, branchPrefixes);
2298
+ const prefix = await selectPrompt(`Choose a branch type for ${pc9.bold(branchName)}:`, branchPrefixes);
1906
2299
  branchName = formatBranchName(prefix, branchName);
1907
2300
  }
1908
2301
  if (!isValidBranchName(branchName)) {
1909
2302
  error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
1910
2303
  process.exit(1);
1911
2304
  }
1912
- info(`Creating branch: ${pc8.bold(branchName)}`);
2305
+ info(`Creating branch: ${pc9.bold(branchName)}`);
1913
2306
  await fetchRemote(syncSource.remote);
1914
2307
  const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
1915
2308
  if (updateResult.exitCode !== 0) {}
@@ -1918,14 +2311,14 @@ var start_default = defineCommand5({
1918
2311
  error(`Failed to create branch: ${result.stderr}`);
1919
2312
  process.exit(1);
1920
2313
  }
1921
- success(`✅ Created ${pc8.bold(branchName)} from latest ${pc8.bold(baseBranch)}`);
2314
+ success(`✅ Created ${pc9.bold(branchName)} from latest ${pc9.bold(baseBranch)}`);
1922
2315
  }
1923
2316
  });
1924
2317
 
1925
2318
  // src/commands/status.ts
1926
- import { defineCommand as defineCommand6 } from "citty";
1927
- import pc9 from "picocolors";
1928
- var status_default = defineCommand6({
2319
+ import { defineCommand as defineCommand7 } from "citty";
2320
+ import pc10 from "picocolors";
2321
+ var status_default = defineCommand7({
1929
2322
  meta: {
1930
2323
  name: "status",
1931
2324
  description: "Show sync status of branches"
@@ -1941,20 +2334,17 @@ var status_default = defineCommand6({
1941
2334
  process.exit(1);
1942
2335
  }
1943
2336
  heading("\uD83D\uDCCA contribute-now status");
1944
- console.log(` ${pc9.dim("Workflow:")} ${pc9.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
1945
- console.log(` ${pc9.dim("Role:")} ${pc9.bold(config.role)}`);
2337
+ console.log(` ${pc10.dim("Workflow:")} ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
2338
+ console.log(` ${pc10.dim("Role:")} ${pc10.bold(config.role)}`);
1946
2339
  console.log();
1947
2340
  await fetchAll();
1948
2341
  const currentBranch = await getCurrentBranch();
1949
2342
  const { mainBranch, origin, upstream, workflow } = config;
1950
2343
  const baseBranch = getBaseBranch(config);
1951
2344
  const isContributor = config.role === "contributor";
1952
- const [dirty, fileStatus] = await Promise.all([
1953
- hasUncommittedChanges(),
1954
- getFileStatus()
1955
- ]);
2345
+ const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
1956
2346
  if (dirty) {
1957
- console.log(` ${pc9.yellow("⚠")} ${pc9.yellow("Uncommitted changes in working tree")}`);
2347
+ console.log(` ${pc10.yellow("⚠")} ${pc10.yellow("Uncommitted changes in working tree")}`);
1958
2348
  console.log();
1959
2349
  }
1960
2350
  const mainRemote = `${origin}/${mainBranch}`;
@@ -1970,82 +2360,82 @@ var status_default = defineCommand6({
1970
2360
  if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
1971
2361
  const branchDiv = await getDivergence(currentBranch, baseBranch);
1972
2362
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
1973
- console.log(branchLine + pc9.dim(` (current ${pc9.green("*")})`));
2363
+ console.log(branchLine + pc10.dim(` (current ${pc10.green("*")})`));
1974
2364
  } else if (currentBranch) {
1975
- console.log(pc9.dim(` (on ${pc9.bold(currentBranch)} branch)`));
2365
+ console.log(pc10.dim(` (on ${pc10.bold(currentBranch)} branch)`));
1976
2366
  }
1977
2367
  const hasFiles = fileStatus.staged.length > 0 || fileStatus.modified.length > 0 || fileStatus.untracked.length > 0;
1978
2368
  if (hasFiles) {
1979
2369
  console.log();
1980
2370
  if (fileStatus.staged.length > 0) {
1981
- console.log(` ${pc9.green("Staged for commit:")}`);
2371
+ console.log(` ${pc10.green("Staged for commit:")}`);
1982
2372
  for (const { file, status } of fileStatus.staged) {
1983
- console.log(` ${pc9.green("+")} ${pc9.dim(`${status}:`)} ${file}`);
2373
+ console.log(` ${pc10.green("+")} ${pc10.dim(`${status}:`)} ${file}`);
1984
2374
  }
1985
2375
  }
1986
2376
  if (fileStatus.modified.length > 0) {
1987
- console.log(` ${pc9.yellow("Unstaged changes:")}`);
2377
+ console.log(` ${pc10.yellow("Unstaged changes:")}`);
1988
2378
  for (const { file, status } of fileStatus.modified) {
1989
- console.log(` ${pc9.yellow("~")} ${pc9.dim(`${status}:`)} ${file}`);
2379
+ console.log(` ${pc10.yellow("~")} ${pc10.dim(`${status}:`)} ${file}`);
1990
2380
  }
1991
2381
  }
1992
2382
  if (fileStatus.untracked.length > 0) {
1993
- console.log(` ${pc9.red("Untracked files:")}`);
2383
+ console.log(` ${pc10.red("Untracked files:")}`);
1994
2384
  for (const file of fileStatus.untracked) {
1995
- console.log(` ${pc9.red("?")} ${file}`);
2385
+ console.log(` ${pc10.red("?")} ${file}`);
1996
2386
  }
1997
2387
  }
1998
2388
  } else if (!dirty) {
1999
- console.log(` ${pc9.green("✓")} ${pc9.dim("Working tree clean")}`);
2389
+ console.log(` ${pc10.green("✓")} ${pc10.dim("Working tree clean")}`);
2000
2390
  }
2001
2391
  const tips = [];
2002
2392
  if (fileStatus.staged.length > 0) {
2003
- tips.push(`Run ${pc9.bold("contrib commit")} to commit staged changes`);
2393
+ tips.push(`Run ${pc10.bold("contrib commit")} to commit staged changes`);
2004
2394
  }
2005
2395
  if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
2006
- tips.push(`Run ${pc9.bold("contrib commit")} to stage and commit changes`);
2396
+ tips.push(`Run ${pc10.bold("contrib commit")} to stage and commit changes`);
2007
2397
  }
2008
2398
  if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0 && currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
2009
2399
  const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
2010
2400
  if (branchDiv.ahead > 0) {
2011
- tips.push(`Run ${pc9.bold("contrib submit")} to push and create/update your PR`);
2401
+ tips.push(`Run ${pc10.bold("contrib submit")} to push and create/update your PR`);
2012
2402
  }
2013
2403
  }
2014
2404
  if (tips.length > 0) {
2015
2405
  console.log();
2016
- console.log(` ${pc9.dim("\uD83D\uDCA1 Tip:")}`);
2406
+ console.log(` ${pc10.dim("\uD83D\uDCA1 Tip:")}`);
2017
2407
  for (const tip of tips) {
2018
- console.log(` ${pc9.dim(tip)}`);
2408
+ console.log(` ${pc10.dim(tip)}`);
2019
2409
  }
2020
2410
  }
2021
2411
  console.log();
2022
2412
  }
2023
2413
  });
2024
2414
  function formatStatus(branch, base, ahead, behind) {
2025
- const label = pc9.bold(branch.padEnd(20));
2415
+ const label = pc10.bold(branch.padEnd(20));
2026
2416
  if (ahead === 0 && behind === 0) {
2027
- return ` ${pc9.green("✓")} ${label} ${pc9.dim(`in sync with ${base}`)}`;
2417
+ return ` ${pc10.green("✓")} ${label} ${pc10.dim(`in sync with ${base}`)}`;
2028
2418
  }
2029
2419
  if (ahead > 0 && behind === 0) {
2030
- return ` ${pc9.yellow("↑")} ${label} ${pc9.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
2420
+ return ` ${pc10.yellow("↑")} ${label} ${pc10.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
2031
2421
  }
2032
2422
  if (behind > 0 && ahead === 0) {
2033
- return ` ${pc9.red("↓")} ${label} ${pc9.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
2423
+ return ` ${pc10.red("↓")} ${label} ${pc10.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
2034
2424
  }
2035
- return ` ${pc9.red("⚡")} ${label} ${pc9.yellow(`${ahead} ahead`)}${pc9.dim(", ")}${pc9.red(`${behind} behind`)} ${pc9.dim(base)}`;
2425
+ return ` ${pc10.red("⚡")} ${label} ${pc10.yellow(`${ahead} ahead`)}${pc10.dim(", ")}${pc10.red(`${behind} behind`)} ${pc10.dim(base)}`;
2036
2426
  }
2037
2427
 
2038
2428
  // src/commands/submit.ts
2039
- import { defineCommand as defineCommand7 } from "citty";
2040
- import pc10 from "picocolors";
2429
+ import { defineCommand as defineCommand8 } from "citty";
2430
+ import pc11 from "picocolors";
2041
2431
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
2042
- info(`Checking out ${pc10.bold(baseBranch)}...`);
2432
+ info(`Checking out ${pc11.bold(baseBranch)}...`);
2043
2433
  const coResult = await checkoutBranch2(baseBranch);
2044
2434
  if (coResult.exitCode !== 0) {
2045
2435
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
2046
2436
  process.exit(1);
2047
2437
  }
2048
- info(`Squash merging ${pc10.bold(featureBranch)} into ${pc10.bold(baseBranch)}...`);
2438
+ info(`Squash merging ${pc11.bold(featureBranch)} into ${pc11.bold(baseBranch)}...`);
2049
2439
  const mergeResult = await mergeSquash(featureBranch);
2050
2440
  if (mergeResult.exitCode !== 0) {
2051
2441
  error(`Squash merge failed: ${mergeResult.stderr}`);
@@ -2075,26 +2465,26 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
2075
2465
  error(`Commit failed: ${commitResult.stderr}`);
2076
2466
  process.exit(1);
2077
2467
  }
2078
- info(`Pushing ${pc10.bold(baseBranch)} to ${origin}...`);
2468
+ info(`Pushing ${pc11.bold(baseBranch)} to ${origin}...`);
2079
2469
  const pushResult = await pushBranch(origin, baseBranch);
2080
2470
  if (pushResult.exitCode !== 0) {
2081
2471
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
2082
2472
  process.exit(1);
2083
2473
  }
2084
- info(`Deleting local branch ${pc10.bold(featureBranch)}...`);
2474
+ info(`Deleting local branch ${pc11.bold(featureBranch)}...`);
2085
2475
  const delLocal = await forceDeleteBranch(featureBranch);
2086
2476
  if (delLocal.exitCode !== 0) {
2087
2477
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
2088
2478
  }
2089
- info(`Deleting remote branch ${pc10.bold(featureBranch)}...`);
2479
+ info(`Deleting remote branch ${pc11.bold(featureBranch)}...`);
2090
2480
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
2091
2481
  if (delRemote.exitCode !== 0) {
2092
2482
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
2093
2483
  }
2094
- success(`✅ Squash merged ${pc10.bold(featureBranch)} into ${pc10.bold(baseBranch)} and pushed.`);
2095
- info(`Run ${pc10.bold("contrib start")} to begin a new feature.`);
2484
+ success(`✅ Squash merged ${pc11.bold(featureBranch)} into ${pc11.bold(baseBranch)} and pushed.`);
2485
+ info(`Run ${pc11.bold("contrib start")} to begin a new feature.`);
2096
2486
  }
2097
- var submit_default = defineCommand7({
2487
+ var submit_default = defineCommand8({
2098
2488
  meta: {
2099
2489
  name: "submit",
2100
2490
  description: "Push current branch and create a pull request"
@@ -2134,7 +2524,7 @@ var submit_default = defineCommand7({
2134
2524
  process.exit(1);
2135
2525
  }
2136
2526
  if (protectedBranches.includes(currentBranch)) {
2137
- error(`Cannot submit ${protectedBranches.map((b) => pc10.bold(b)).join(" or ")} as a PR. Switch to your feature branch.`);
2527
+ error(`Cannot submit ${protectedBranches.map((b) => pc11.bold(b)).join(" or ")} as a PR. Switch to your feature branch.`);
2138
2528
  process.exit(1);
2139
2529
  }
2140
2530
  heading("\uD83D\uDE80 contrib submit");
@@ -2143,7 +2533,7 @@ var submit_default = defineCommand7({
2143
2533
  if (ghInstalled && ghAuthed) {
2144
2534
  const mergedPR = await getMergedPRForBranch(currentBranch);
2145
2535
  if (mergedPR) {
2146
- warn(`PR #${mergedPR.number} (${pc10.bold(mergedPR.title)}) was already merged.`);
2536
+ warn(`PR #${mergedPR.number} (${pc11.bold(mergedPR.title)}) was already merged.`);
2147
2537
  const localWork = await hasLocalWork(origin, currentBranch);
2148
2538
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
2149
2539
  if (hasWork) {
@@ -2151,7 +2541,7 @@ var submit_default = defineCommand7({
2151
2541
  warn("You have uncommitted changes in your working tree.");
2152
2542
  }
2153
2543
  if (localWork.unpushedCommits > 0) {
2154
- warn(`You have ${pc10.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
2544
+ warn(`You have ${pc11.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
2155
2545
  }
2156
2546
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
2157
2547
  const DISCARD = "Discard all changes and clean up";
@@ -2162,7 +2552,7 @@ var submit_default = defineCommand7({
2162
2552
  return;
2163
2553
  }
2164
2554
  if (action === SAVE_NEW_BRANCH) {
2165
- info(pc10.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
2555
+ info(pc11.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
2166
2556
  const description = await inputPrompt("What are you working on?");
2167
2557
  let newBranchName = description;
2168
2558
  if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
@@ -2171,8 +2561,8 @@ var submit_default = defineCommand7({
2171
2561
  if (suggested) {
2172
2562
  spinner.success("Branch name suggestion ready.");
2173
2563
  console.log(`
2174
- ${pc10.dim("AI suggestion:")} ${pc10.bold(pc10.cyan(suggested))}`);
2175
- const accepted = await confirmPrompt(`Use ${pc10.bold(suggested)} as your branch name?`);
2564
+ ${pc11.dim("AI suggestion:")} ${pc11.bold(pc11.cyan(suggested))}`);
2565
+ const accepted = await confirmPrompt(`Use ${pc11.bold(suggested)} as your branch name?`);
2176
2566
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
2177
2567
  } else {
2178
2568
  spinner.fail("AI did not return a suggestion.");
@@ -2180,37 +2570,45 @@ var submit_default = defineCommand7({
2180
2570
  }
2181
2571
  }
2182
2572
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
2183
- const prefix = await selectPrompt(`Choose a branch type for ${pc10.bold(newBranchName)}:`, config.branchPrefixes);
2573
+ const prefix = await selectPrompt(`Choose a branch type for ${pc11.bold(newBranchName)}:`, config.branchPrefixes);
2184
2574
  newBranchName = formatBranchName(prefix, newBranchName);
2185
2575
  }
2186
2576
  if (!isValidBranchName(newBranchName)) {
2187
2577
  error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
2188
2578
  process.exit(1);
2189
2579
  }
2580
+ const staleUpstream = await getUpstreamRef();
2581
+ const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
2190
2582
  const renameResult = await renameBranch(currentBranch, newBranchName);
2191
2583
  if (renameResult.exitCode !== 0) {
2192
2584
  error(`Failed to rename branch: ${renameResult.stderr}`);
2193
2585
  process.exit(1);
2194
2586
  }
2195
- success(`Renamed ${pc10.bold(currentBranch)} → ${pc10.bold(newBranchName)}`);
2587
+ success(`Renamed ${pc11.bold(currentBranch)} → ${pc11.bold(newBranchName)}`);
2588
+ await unsetUpstream();
2196
2589
  const syncSource2 = getSyncSource(config);
2197
- info(`Syncing ${pc10.bold(newBranchName)} with latest ${pc10.bold(baseBranch)}...`);
2590
+ info(`Syncing ${pc11.bold(newBranchName)} with latest ${pc11.bold(baseBranch)}...`);
2198
2591
  await fetchRemote(syncSource2.remote);
2199
- const savedUpstreamRef = await getUpstreamRef();
2200
- const rebaseResult = savedUpstreamRef && savedUpstreamRef !== syncSource2.ref ? await rebaseOnto(syncSource2.ref, savedUpstreamRef) : await rebase(syncSource2.ref);
2592
+ let rebaseResult;
2593
+ if (staleUpstreamHash) {
2594
+ rebaseResult = await rebaseOnto(syncSource2.ref, staleUpstreamHash);
2595
+ } else {
2596
+ const savedStrategy = await determineRebaseStrategy(newBranchName, syncSource2.ref);
2597
+ rebaseResult = savedStrategy.strategy === "onto" && savedStrategy.ontoOldBase ? await rebaseOnto(syncSource2.ref, savedStrategy.ontoOldBase) : await rebase(syncSource2.ref);
2598
+ }
2201
2599
  if (rebaseResult.exitCode !== 0) {
2202
2600
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
2203
- info(` ${pc10.bold("git rebase --continue")}`);
2601
+ info(` ${pc11.bold("git rebase --continue")}`);
2204
2602
  } else {
2205
- success(`Rebased ${pc10.bold(newBranchName)} onto ${pc10.bold(syncSource2.ref)}.`);
2603
+ success(`Rebased ${pc11.bold(newBranchName)} onto ${pc11.bold(syncSource2.ref)}.`);
2206
2604
  }
2207
- info(`All your changes are preserved. Run ${pc10.bold("contrib submit")} when ready to create a new PR.`);
2605
+ info(`All your changes are preserved. Run ${pc11.bold("contrib submit")} when ready to create a new PR.`);
2208
2606
  return;
2209
2607
  }
2210
2608
  warn("Discarding local changes...");
2211
2609
  }
2212
2610
  const syncSource = getSyncSource(config);
2213
- info(`Switching to ${pc10.bold(baseBranch)} and syncing...`);
2611
+ info(`Switching to ${pc11.bold(baseBranch)} and syncing...`);
2214
2612
  await fetchRemote(syncSource.remote);
2215
2613
  const coResult = await checkoutBranch2(baseBranch);
2216
2614
  if (coResult.exitCode !== 0) {
@@ -2218,46 +2616,22 @@ var submit_default = defineCommand7({
2218
2616
  process.exit(1);
2219
2617
  }
2220
2618
  await updateLocalBranch(baseBranch, syncSource.ref);
2221
- success(`Synced ${pc10.bold(baseBranch)} with ${pc10.bold(syncSource.ref)}.`);
2222
- info(`Deleting stale branch ${pc10.bold(currentBranch)}...`);
2619
+ success(`Synced ${pc11.bold(baseBranch)} with ${pc11.bold(syncSource.ref)}.`);
2620
+ info(`Deleting stale branch ${pc11.bold(currentBranch)}...`);
2223
2621
  const delResult = await forceDeleteBranch(currentBranch);
2224
2622
  if (delResult.exitCode === 0) {
2225
- success(`Deleted ${pc10.bold(currentBranch)}.`);
2623
+ success(`Deleted ${pc11.bold(currentBranch)}.`);
2226
2624
  } else {
2227
2625
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
2228
2626
  }
2229
2627
  console.log();
2230
- info(`You're now on ${pc10.bold(baseBranch)}. Run ${pc10.bold("contrib start")} to begin a new feature.`);
2628
+ info(`You're now on ${pc11.bold(baseBranch)}. Run ${pc11.bold("contrib start")} to begin a new feature.`);
2231
2629
  return;
2232
2630
  }
2233
2631
  }
2234
- info(`Pushing ${pc10.bold(currentBranch)} to ${origin}...`);
2235
- const pushResult = await pushSetUpstream(origin, currentBranch);
2236
- if (pushResult.exitCode !== 0) {
2237
- error(`Failed to push: ${pushResult.stderr}`);
2238
- process.exit(1);
2239
- }
2240
- if (!ghInstalled || !ghAuthed) {
2241
- const repoInfo = await getRepoInfoFromRemote(origin);
2242
- if (repoInfo) {
2243
- const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
2244
- console.log();
2245
- info("Create your PR manually:");
2246
- console.log(` ${pc10.cyan(prUrl)}`);
2247
- } else {
2248
- info("gh CLI not available. Create your PR manually on GitHub.");
2249
- }
2250
- return;
2251
- }
2252
- const existingPR = await getPRForBranch(currentBranch);
2253
- if (existingPR) {
2254
- success(`Pushed changes to existing PR #${existingPR.number}: ${pc10.bold(existingPR.title)}`);
2255
- console.log(` ${pc10.cyan(existingPR.url)}`);
2256
- return;
2257
- }
2258
2632
  let prTitle = null;
2259
2633
  let prBody = null;
2260
- if (!args["no-ai"]) {
2634
+ async function tryGenerateAI() {
2261
2635
  const [copilotError, commits, diff] = await Promise.all([
2262
2636
  checkCopilotAvailable(),
2263
2637
  getLog(baseBranch, "HEAD"),
@@ -2271,10 +2645,10 @@ var submit_default = defineCommand7({
2271
2645
  prBody = result.body;
2272
2646
  spinner.success("PR description generated.");
2273
2647
  console.log(`
2274
- ${pc10.dim("AI title:")} ${pc10.bold(pc10.cyan(prTitle))}`);
2648
+ ${pc11.dim("AI title:")} ${pc11.bold(pc11.cyan(prTitle))}`);
2275
2649
  console.log(`
2276
- ${pc10.dim("AI body preview:")}`);
2277
- console.log(pc10.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
2650
+ ${pc11.dim("AI body preview:")}`);
2651
+ console.log(pc11.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
2278
2652
  } else {
2279
2653
  spinner.fail("AI did not return a PR description.");
2280
2654
  }
@@ -2282,77 +2656,119 @@ ${pc10.dim("AI body preview:")}`);
2282
2656
  warn(`AI unavailable: ${copilotError}`);
2283
2657
  }
2284
2658
  }
2659
+ if (!args["no-ai"]) {
2660
+ await tryGenerateAI();
2661
+ }
2285
2662
  const CANCEL = "Cancel";
2286
2663
  const SQUASH_LOCAL = `Squash merge to ${baseBranch} locally (no PR)`;
2287
- if (prTitle && prBody) {
2288
- const choices = [
2289
- "Use AI description",
2290
- "Edit title",
2291
- "Write manually",
2292
- "Use gh --fill (auto-fill from commits)"
2293
- ];
2294
- if (config.role === "maintainer")
2295
- choices.push(SQUASH_LOCAL);
2296
- choices.push(CANCEL);
2297
- const action = await selectPrompt("What would you like to do with the PR description?", choices);
2298
- if (action === CANCEL) {
2299
- warn("Submit cancelled.");
2300
- return;
2301
- }
2302
- if (action === SQUASH_LOCAL) {
2303
- await performSquashMerge(origin, baseBranch, currentBranch, {
2304
- defaultMsg: prTitle ?? undefined,
2305
- model: args.model,
2306
- convention: config.commitConvention
2307
- });
2308
- return;
2309
- }
2310
- if (action === "Use AI description") {} else if (action === "Edit title") {
2311
- prTitle = await inputPrompt("PR title", prTitle);
2312
- } else if (action === "Write manually") {
2313
- prTitle = await inputPrompt("PR title");
2314
- prBody = await inputPrompt("PR body (markdown)");
2664
+ const REGENERATE = "Regenerate AI description";
2665
+ let submitAction = "cancel";
2666
+ const isMaintainer = config.role === "maintainer";
2667
+ let actionResolved = false;
2668
+ while (!actionResolved) {
2669
+ if (prTitle && prBody) {
2670
+ const choices = ["Use AI description"];
2671
+ if (isMaintainer)
2672
+ choices.push(SQUASH_LOCAL);
2673
+ choices.push("Edit title", "Write manually", "Use gh --fill (auto-fill from commits)", REGENERATE, CANCEL);
2674
+ const action = await selectPrompt("What would you like to do with the PR description?", choices);
2675
+ if (action === CANCEL) {
2676
+ submitAction = "cancel";
2677
+ actionResolved = true;
2678
+ } else if (action === REGENERATE) {
2679
+ prTitle = null;
2680
+ prBody = null;
2681
+ await tryGenerateAI();
2682
+ } else if (action === SQUASH_LOCAL) {
2683
+ submitAction = "squash";
2684
+ actionResolved = true;
2685
+ } else if (action === "Use AI description") {
2686
+ submitAction = "create-pr";
2687
+ actionResolved = true;
2688
+ } else if (action === "Edit title") {
2689
+ prTitle = await inputPrompt("PR title", prTitle);
2690
+ submitAction = "create-pr";
2691
+ actionResolved = true;
2692
+ } else if (action === "Write manually") {
2693
+ prTitle = await inputPrompt("PR title");
2694
+ prBody = await inputPrompt("PR body (markdown)");
2695
+ submitAction = "create-pr";
2696
+ actionResolved = true;
2697
+ } else {
2698
+ submitAction = "fill";
2699
+ actionResolved = true;
2700
+ }
2315
2701
  } else {
2316
- const fillResult = await createPRFill(baseBranch, args.draft);
2317
- if (fillResult.exitCode !== 0) {
2318
- error(`Failed to create PR: ${fillResult.stderr}`);
2319
- process.exit(1);
2702
+ const choices = [];
2703
+ if (isMaintainer)
2704
+ choices.push(SQUASH_LOCAL);
2705
+ if (!args["no-ai"])
2706
+ choices.push(REGENERATE);
2707
+ choices.push("Write title & body manually", "Use gh --fill (auto-fill from commits)", CANCEL);
2708
+ const action = await selectPrompt("How would you like to create the PR?", choices);
2709
+ if (action === CANCEL) {
2710
+ submitAction = "cancel";
2711
+ actionResolved = true;
2712
+ } else if (action === REGENERATE) {
2713
+ await tryGenerateAI();
2714
+ } else if (action === SQUASH_LOCAL) {
2715
+ submitAction = "squash";
2716
+ actionResolved = true;
2717
+ } else if (action === "Write title & body manually") {
2718
+ prTitle = await inputPrompt("PR title");
2719
+ prBody = await inputPrompt("PR body (markdown)");
2720
+ submitAction = "create-pr";
2721
+ actionResolved = true;
2722
+ } else {
2723
+ submitAction = "fill";
2724
+ actionResolved = true;
2320
2725
  }
2321
- success(`✅ PR created: ${fillResult.stdout.trim()}`);
2322
- return;
2323
- }
2324
- } else {
2325
- const choices = [
2326
- "Write title & body manually",
2327
- "Use gh --fill (auto-fill from commits)"
2328
- ];
2329
- if (config.role === "maintainer")
2330
- choices.push(SQUASH_LOCAL);
2331
- choices.push(CANCEL);
2332
- const action = await selectPrompt("How would you like to create the PR?", choices);
2333
- if (action === CANCEL) {
2334
- warn("Submit cancelled.");
2335
- return;
2336
- }
2337
- if (action === SQUASH_LOCAL) {
2338
- await performSquashMerge(origin, baseBranch, currentBranch, {
2339
- model: args.model,
2340
- convention: config.commitConvention
2341
- });
2342
- return;
2343
2726
  }
2344
- if (action === "Write title & body manually") {
2345
- prTitle = await inputPrompt("PR title");
2346
- prBody = await inputPrompt("PR body (markdown)");
2727
+ }
2728
+ if (submitAction === "cancel") {
2729
+ warn("Submit cancelled.");
2730
+ return;
2731
+ }
2732
+ if (submitAction === "squash") {
2733
+ await performSquashMerge(origin, baseBranch, currentBranch, {
2734
+ defaultMsg: prTitle ?? undefined,
2735
+ model: args.model,
2736
+ convention: config.commitConvention
2737
+ });
2738
+ return;
2739
+ }
2740
+ info(`Pushing ${pc11.bold(currentBranch)} to ${origin}...`);
2741
+ const pushResult = await pushSetUpstream(origin, currentBranch);
2742
+ if (pushResult.exitCode !== 0) {
2743
+ error(`Failed to push: ${pushResult.stderr}`);
2744
+ process.exit(1);
2745
+ }
2746
+ if (!ghInstalled || !ghAuthed) {
2747
+ const repoInfo = await getRepoInfoFromRemote(origin);
2748
+ if (repoInfo) {
2749
+ const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
2750
+ console.log();
2751
+ info("Create your PR manually:");
2752
+ console.log(` ${pc11.cyan(prUrl)}`);
2347
2753
  } else {
2348
- const fillResult = await createPRFill(baseBranch, args.draft);
2349
- if (fillResult.exitCode !== 0) {
2350
- error(`Failed to create PR: ${fillResult.stderr}`);
2351
- process.exit(1);
2352
- }
2353
- success(`✅ PR created: ${fillResult.stdout.trim()}`);
2354
- return;
2754
+ info("gh CLI not available. Create your PR manually on GitHub.");
2755
+ }
2756
+ return;
2757
+ }
2758
+ const existingPR = await getPRForBranch(currentBranch);
2759
+ if (existingPR) {
2760
+ success(`Pushed changes to existing PR #${existingPR.number}: ${pc11.bold(existingPR.title)}`);
2761
+ console.log(` ${pc11.cyan(existingPR.url)}`);
2762
+ return;
2763
+ }
2764
+ if (submitAction === "fill") {
2765
+ const fillResult = await createPRFill(baseBranch, args.draft);
2766
+ if (fillResult.exitCode !== 0) {
2767
+ error(`Failed to create PR: ${fillResult.stderr}`);
2768
+ process.exit(1);
2355
2769
  }
2770
+ success(`✅ PR created: ${fillResult.stdout.trim()}`);
2771
+ return;
2356
2772
  }
2357
2773
  if (!prTitle) {
2358
2774
  error("No PR title provided.");
@@ -2373,9 +2789,9 @@ ${pc10.dim("AI body preview:")}`);
2373
2789
  });
2374
2790
 
2375
2791
  // src/commands/sync.ts
2376
- import { defineCommand as defineCommand8 } from "citty";
2377
- import pc11 from "picocolors";
2378
- var sync_default = defineCommand8({
2792
+ import { defineCommand as defineCommand9 } from "citty";
2793
+ import pc12 from "picocolors";
2794
+ var sync_default = defineCommand9({
2379
2795
  meta: {
2380
2796
  name: "sync",
2381
2797
  description: "Sync your local branches with the remote"
@@ -2417,12 +2833,12 @@ var sync_default = defineCommand8({
2417
2833
  }
2418
2834
  const div = await getDivergence(baseBranch, syncSource.ref);
2419
2835
  if (div.ahead > 0 || div.behind > 0) {
2420
- info(`${pc11.bold(baseBranch)} is ${pc11.yellow(`${div.ahead} ahead`)} and ${pc11.red(`${div.behind} behind`)} ${syncSource.ref}`);
2836
+ info(`${pc12.bold(baseBranch)} is ${pc12.yellow(`${div.ahead} ahead`)} and ${pc12.red(`${div.behind} behind`)} ${syncSource.ref}`);
2421
2837
  } else {
2422
- info(`${pc11.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
2838
+ info(`${pc12.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
2423
2839
  }
2424
2840
  if (!args.yes) {
2425
- const ok = await confirmPrompt(`This will pull ${pc11.bold(syncSource.ref)} into local ${pc11.bold(baseBranch)}.`);
2841
+ const ok = await confirmPrompt(`This will pull ${pc12.bold(syncSource.ref)} into local ${pc12.bold(baseBranch)}.`);
2426
2842
  if (!ok)
2427
2843
  process.exit(0);
2428
2844
  }
@@ -2440,7 +2856,7 @@ var sync_default = defineCommand8({
2440
2856
  if (hasDevBranch(workflow) && role === "maintainer") {
2441
2857
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
2442
2858
  if (mainDiv.behind > 0) {
2443
- info(`Also syncing ${pc11.bold(config.mainBranch)}...`);
2859
+ info(`Also syncing ${pc12.bold(config.mainBranch)}...`);
2444
2860
  const mainCoResult = await checkoutBranch2(config.mainBranch);
2445
2861
  if (mainCoResult.exitCode === 0) {
2446
2862
  const mainPullResult = await pullBranch(origin, config.mainBranch);
@@ -2456,9 +2872,9 @@ var sync_default = defineCommand8({
2456
2872
 
2457
2873
  // src/commands/update.ts
2458
2874
  import { readFileSync as readFileSync4 } from "node:fs";
2459
- import { defineCommand as defineCommand9 } from "citty";
2460
- import pc12 from "picocolors";
2461
- var update_default = defineCommand9({
2875
+ import { defineCommand as defineCommand10 } from "citty";
2876
+ import pc13 from "picocolors";
2877
+ var update_default = defineCommand10({
2462
2878
  meta: {
2463
2879
  name: "update",
2464
2880
  description: "Rebase current branch onto the latest base branch"
@@ -2493,7 +2909,7 @@ var update_default = defineCommand9({
2493
2909
  process.exit(1);
2494
2910
  }
2495
2911
  if (protectedBranches.includes(currentBranch)) {
2496
- error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc12.bold(b)).join(" or ")} branches.`);
2912
+ error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc13.bold(b)).join(" or ")} branches.`);
2497
2913
  process.exit(1);
2498
2914
  }
2499
2915
  if (await hasUncommittedChanges()) {
@@ -2503,8 +2919,8 @@ var update_default = defineCommand9({
2503
2919
  heading("\uD83D\uDD03 contrib update");
2504
2920
  const mergedPR = await getMergedPRForBranch(currentBranch);
2505
2921
  if (mergedPR) {
2506
- warn(`PR #${mergedPR.number} (${pc12.bold(mergedPR.title)}) has already been merged.`);
2507
- info(`Link: ${pc12.underline(mergedPR.url)}`);
2922
+ warn(`PR #${mergedPR.number} (${pc13.bold(mergedPR.title)}) has already been merged.`);
2923
+ info(`Link: ${pc13.underline(mergedPR.url)}`);
2508
2924
  const localWork = await hasLocalWork(syncSource.remote, currentBranch);
2509
2925
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
2510
2926
  if (hasWork) {
@@ -2517,13 +2933,13 @@ var update_default = defineCommand9({
2517
2933
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
2518
2934
  const DISCARD = "Discard all changes and clean up";
2519
2935
  const CANCEL = "Cancel";
2520
- const action = await selectPrompt(`${pc12.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
2936
+ const action = await selectPrompt(`${pc13.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
2521
2937
  if (action === CANCEL) {
2522
2938
  info("No changes made. You are still on your current branch.");
2523
2939
  return;
2524
2940
  }
2525
2941
  if (action === SAVE_NEW_BRANCH) {
2526
- info(pc12.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
2942
+ info(pc13.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
2527
2943
  const description = await inputPrompt("What are you working on?");
2528
2944
  let newBranchName = description;
2529
2945
  if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
@@ -2532,8 +2948,8 @@ var update_default = defineCommand9({
2532
2948
  if (suggested) {
2533
2949
  spinner.success("Branch name suggestion ready.");
2534
2950
  console.log(`
2535
- ${pc12.dim("AI suggestion:")} ${pc12.bold(pc12.cyan(suggested))}`);
2536
- const accepted = await confirmPrompt(`Use ${pc12.bold(suggested)} as your branch name?`);
2951
+ ${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(suggested))}`);
2952
+ const accepted = await confirmPrompt(`Use ${pc13.bold(suggested)} as your branch name?`);
2537
2953
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
2538
2954
  } else {
2539
2955
  spinner.fail("AI did not return a suggestion.");
@@ -2541,29 +2957,37 @@ var update_default = defineCommand9({
2541
2957
  }
2542
2958
  }
2543
2959
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
2544
- const prefix = await selectPrompt(`Choose a branch type for ${pc12.bold(newBranchName)}:`, config.branchPrefixes);
2960
+ const prefix = await selectPrompt(`Choose a branch type for ${pc13.bold(newBranchName)}:`, config.branchPrefixes);
2545
2961
  newBranchName = formatBranchName(prefix, newBranchName);
2546
2962
  }
2547
2963
  if (!isValidBranchName(newBranchName)) {
2548
2964
  error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
2549
2965
  process.exit(1);
2550
2966
  }
2967
+ const staleUpstream = await getUpstreamRef();
2968
+ const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
2551
2969
  const renameResult = await renameBranch(currentBranch, newBranchName);
2552
2970
  if (renameResult.exitCode !== 0) {
2553
2971
  error(`Failed to rename branch: ${renameResult.stderr}`);
2554
2972
  process.exit(1);
2555
2973
  }
2556
- success(`Renamed ${pc12.bold(currentBranch)} → ${pc12.bold(newBranchName)}`);
2974
+ success(`Renamed ${pc13.bold(currentBranch)} → ${pc13.bold(newBranchName)}`);
2975
+ await unsetUpstream();
2557
2976
  await fetchRemote(syncSource.remote);
2558
- const savedUpstreamRef = await getUpstreamRef();
2559
- const rebaseResult2 = savedUpstreamRef && savedUpstreamRef !== syncSource.ref ? await rebaseOnto(syncSource.ref, savedUpstreamRef) : await rebase(syncSource.ref);
2977
+ let rebaseResult2;
2978
+ if (staleUpstreamHash) {
2979
+ rebaseResult2 = await rebaseOnto(syncSource.ref, staleUpstreamHash);
2980
+ } else {
2981
+ const savedStrategy = await determineRebaseStrategy(newBranchName, syncSource.ref);
2982
+ rebaseResult2 = savedStrategy.strategy === "onto" && savedStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, savedStrategy.ontoOldBase) : await rebase(syncSource.ref);
2983
+ }
2560
2984
  if (rebaseResult2.exitCode !== 0) {
2561
2985
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
2562
- info(` ${pc12.bold("git rebase --continue")}`);
2986
+ info(` ${pc13.bold("git rebase --continue")}`);
2563
2987
  } else {
2564
- success(`Rebased ${pc12.bold(newBranchName)} onto ${pc12.bold(syncSource.ref)}.`);
2988
+ success(`Rebased ${pc13.bold(newBranchName)} onto ${pc13.bold(syncSource.ref)}.`);
2565
2989
  }
2566
- info(`All your changes are preserved. Run ${pc12.bold("contrib submit")} when ready to create a new PR.`);
2990
+ info(`All your changes are preserved. Run ${pc13.bold("contrib submit")} when ready to create a new PR.`);
2567
2991
  return;
2568
2992
  }
2569
2993
  warn("Discarding local changes...");
@@ -2575,18 +2999,21 @@ var update_default = defineCommand9({
2575
2999
  process.exit(1);
2576
3000
  }
2577
3001
  await updateLocalBranch(baseBranch, syncSource.ref);
2578
- success(`Synced ${pc12.bold(baseBranch)} with ${pc12.bold(syncSource.ref)}.`);
2579
- info(`Deleting stale branch ${pc12.bold(currentBranch)}...`);
3002
+ success(`Synced ${pc13.bold(baseBranch)} with ${pc13.bold(syncSource.ref)}.`);
3003
+ info(`Deleting stale branch ${pc13.bold(currentBranch)}...`);
2580
3004
  await forceDeleteBranch(currentBranch);
2581
- success(`Deleted ${pc12.bold(currentBranch)}.`);
2582
- info(`Run ${pc12.bold("contrib start")} to begin a new feature branch.`);
3005
+ success(`Deleted ${pc13.bold(currentBranch)}.`);
3006
+ info(`Run ${pc13.bold("contrib start")} to begin a new feature branch.`);
2583
3007
  return;
2584
3008
  }
2585
- info(`Updating ${pc12.bold(currentBranch)} with latest ${pc12.bold(baseBranch)}...`);
3009
+ info(`Updating ${pc13.bold(currentBranch)} with latest ${pc13.bold(baseBranch)}...`);
2586
3010
  await fetchRemote(syncSource.remote);
2587
3011
  await updateLocalBranch(baseBranch, syncSource.ref);
2588
- const upstreamRef = await getUpstreamRef();
2589
- const rebaseResult = upstreamRef && upstreamRef !== syncSource.ref ? await rebaseOnto(syncSource.ref, upstreamRef) : await rebase(syncSource.ref);
3012
+ const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
3013
+ if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
3014
+ info(pc13.dim(`Using --onto rebase (branch was based on a different ref)`));
3015
+ }
3016
+ const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
2590
3017
  if (rebaseResult.exitCode !== 0) {
2591
3018
  warn("Rebase hit conflicts. Resolve them manually.");
2592
3019
  console.log();
@@ -2613,10 +3040,10 @@ ${content.slice(0, 2000)}
2613
3040
  if (suggestion) {
2614
3041
  spinner.success("AI conflict guidance ready.");
2615
3042
  console.log(`
2616
- ${pc12.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
2617
- console.log(pc12.dim("─".repeat(60)));
3043
+ ${pc13.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
3044
+ console.log(pc13.dim("─".repeat(60)));
2618
3045
  console.log(suggestion);
2619
- console.log(pc12.dim("─".repeat(60)));
3046
+ console.log(pc13.dim("─".repeat(60)));
2620
3047
  console.log();
2621
3048
  } else {
2622
3049
  spinner.fail("AI could not analyze the conflicts.");
@@ -2624,22 +3051,22 @@ ${pc12.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
2624
3051
  }
2625
3052
  }
2626
3053
  }
2627
- console.log(pc12.bold("To resolve:"));
3054
+ console.log(pc13.bold("To resolve:"));
2628
3055
  console.log(` 1. Fix conflicts in the affected files`);
2629
- console.log(` 2. ${pc12.cyan("git add <resolved-files>")}`);
2630
- console.log(` 3. ${pc12.cyan("git rebase --continue")}`);
3056
+ console.log(` 2. ${pc13.cyan("git add <resolved-files>")}`);
3057
+ console.log(` 3. ${pc13.cyan("git rebase --continue")}`);
2631
3058
  console.log();
2632
- console.log(` Or abort: ${pc12.cyan("git rebase --abort")}`);
3059
+ console.log(` Or abort: ${pc13.cyan("git rebase --abort")}`);
2633
3060
  process.exit(1);
2634
3061
  }
2635
- success(`✅ ${pc12.bold(currentBranch)} has been rebased onto latest ${pc12.bold(baseBranch)}`);
3062
+ success(`✅ ${pc13.bold(currentBranch)} has been rebased onto latest ${pc13.bold(baseBranch)}`);
2636
3063
  }
2637
3064
  });
2638
3065
 
2639
3066
  // src/commands/validate.ts
2640
- import { defineCommand as defineCommand10 } from "citty";
2641
- import pc13 from "picocolors";
2642
- var validate_default = defineCommand10({
3067
+ import { defineCommand as defineCommand11 } from "citty";
3068
+ import pc14 from "picocolors";
3069
+ var validate_default = defineCommand11({
2643
3070
  meta: {
2644
3071
  name: "validate",
2645
3072
  description: "Validate a commit message against the configured convention"
@@ -2669,7 +3096,7 @@ var validate_default = defineCommand10({
2669
3096
  }
2670
3097
  const errors = getValidationError(convention);
2671
3098
  for (const line of errors) {
2672
- console.error(pc13.red(` ✗ ${line}`));
3099
+ console.error(pc14.red(` ✗ ${line}`));
2673
3100
  }
2674
3101
  process.exit(1);
2675
3102
  }
@@ -2677,76 +3104,19 @@ var validate_default = defineCommand10({
2677
3104
 
2678
3105
  // src/ui/banner.ts
2679
3106
  import figlet from "figlet";
2680
- import pc14 from "picocolors";
2681
- // package.json
2682
- var package_default = {
2683
- name: "contribute-now",
2684
- version: "0.2.0-dev.33be40f",
2685
- description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
2686
- type: "module",
2687
- bin: {
2688
- contrib: "dist/index.js",
2689
- contribute: "dist/index.js"
2690
- },
2691
- files: [
2692
- "dist"
2693
- ],
2694
- scripts: {
2695
- build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
2696
- cli: "bun run src/index.ts --",
2697
- dev: "bun src/index.ts",
2698
- test: "bun test",
2699
- lint: "biome check .",
2700
- "lint:fix": "biome check --write .",
2701
- format: "biome format --write .",
2702
- "www:dev": "bun run --cwd www dev",
2703
- "www:build": "bun run --cwd www build",
2704
- "www:preview": "bun run --cwd www preview"
2705
- },
2706
- engines: {
2707
- node: ">=18",
2708
- bun: ">=1.0"
2709
- },
2710
- keywords: [
2711
- "git",
2712
- "workflow",
2713
- "squash-merge",
2714
- "sync",
2715
- "cli",
2716
- "contribute",
2717
- "fork",
2718
- "dev-branch",
2719
- "clean-commit"
2720
- ],
2721
- author: "Waren Gonzaga",
2722
- license: "GPL-3.0",
2723
- repository: {
2724
- type: "git",
2725
- url: "git+https://github.com/warengonzaga/contribute-now.git"
2726
- },
2727
- dependencies: {
2728
- "@clack/prompts": "^1.0.1",
2729
- "@github/copilot-sdk": "^0.1.25",
2730
- "@wgtechlabs/log-engine": "^2.3.1",
2731
- citty: "^0.1.6",
2732
- figlet: "^1.10.0",
2733
- picocolors: "^1.1.1"
2734
- },
2735
- devDependencies: {
2736
- "@biomejs/biome": "^2.4.4",
2737
- "@types/bun": "latest",
2738
- "@types/figlet": "^1.7.0",
2739
- typescript: "^5.7.0"
2740
- }
2741
- };
2742
-
2743
- // src/ui/banner.ts
2744
- var LOGO;
3107
+ import pc15 from "picocolors";
3108
+ var LOGO_BIG;
2745
3109
  try {
2746
- LOGO = figlet.textSync(`Contribute
3110
+ LOGO_BIG = figlet.textSync(`Contribute
2747
3111
  Now`, { font: "ANSI Shadow" });
2748
3112
  } catch {
2749
- LOGO = "Contribute Now";
3113
+ LOGO_BIG = "Contribute Now";
3114
+ }
3115
+ var LOGO_SMALL;
3116
+ try {
3117
+ LOGO_SMALL = figlet.textSync("Contribute Now", { font: "Slant" });
3118
+ } catch {
3119
+ LOGO_SMALL = "Contribute Now";
2750
3120
  }
2751
3121
  function getVersion() {
2752
3122
  return package_default.version ?? "unknown";
@@ -2754,23 +3124,30 @@ function getVersion() {
2754
3124
  function getAuthor() {
2755
3125
  return typeof package_default.author === "string" ? package_default.author : "unknown";
2756
3126
  }
2757
- function showBanner(showLinks = false) {
2758
- console.log(pc14.cyan(`
2759
- ${LOGO}`));
2760
- console.log(` ${pc14.dim(`v${getVersion()}`)} ${pc14.dim("—")} ${pc14.dim(`Built by ${getAuthor()}`)}`);
2761
- if (showLinks) {
3127
+ function showBanner(variant = "small") {
3128
+ const logo = variant === "big" ? LOGO_BIG : LOGO_SMALL;
3129
+ console.log(pc15.cyan(`
3130
+ ${logo}`));
3131
+ console.log(` ${pc15.dim(`v${getVersion()}`)} ${pc15.dim("—")} ${pc15.dim(`Built by ${getAuthor()}`)}`);
3132
+ if (variant === "big") {
2762
3133
  console.log();
2763
- console.log(` ${pc14.yellow("Star")} ${pc14.cyan("https://github.com/warengonzaga/contribute-now")}`);
2764
- console.log(` ${pc14.green("Contribute")} ${pc14.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
2765
- console.log(` ${pc14.magenta("Sponsor")} ${pc14.cyan("https://warengonzaga.com/sponsor")}`);
3134
+ console.log(` ${pc15.yellow("Star")} ${pc15.cyan("https://github.com/warengonzaga/contribute-now")}`);
3135
+ console.log(` ${pc15.green("Contribute")} ${pc15.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
3136
+ console.log(` ${pc15.magenta("Sponsor")} ${pc15.cyan("https://warengonzaga.com/sponsor")}`);
2766
3137
  }
2767
3138
  console.log();
2768
3139
  }
2769
3140
 
2770
3141
  // src/index.ts
2771
- var isHelp = process.argv.includes("--help") || process.argv.includes("-h");
2772
- showBanner(isHelp);
2773
- var main = defineCommand11({
3142
+ var isVersion = process.argv.includes("--version") || process.argv.includes("-v");
3143
+ if (!isVersion) {
3144
+ const subCommands = ["setup", "sync", "start", "commit", "update", "submit", "clean", "status", "hook", "validate", "doctor"];
3145
+ const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
3146
+ const hasSubCommand = subCommands.some((cmd) => process.argv.includes(cmd));
3147
+ const useBigBanner = isHelp || !hasSubCommand;
3148
+ showBanner(useBigBanner ? "big" : "small");
3149
+ }
3150
+ var main = defineCommand12({
2774
3151
  meta: {
2775
3152
  name: "contrib",
2776
3153
  version: getVersion(),
@@ -2793,7 +3170,8 @@ var main = defineCommand11({
2793
3170
  clean: clean_default,
2794
3171
  status: status_default,
2795
3172
  hook: hook_default,
2796
- validate: validate_default
3173
+ validate: validate_default,
3174
+ doctor: doctor_default
2797
3175
  },
2798
3176
  run({ args }) {
2799
3177
  if (args.version) {