contribute-now 0.4.1 → 0.5.0-dev.914e35d

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 (3) hide show
  1. package/README.md +39 -12
  2. package/dist/index.js +850 -227
  3. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/index.ts
6
- import { defineCommand as defineCommand14, runMain } from "citty";
6
+ import { defineCommand as defineCommand16, runMain } from "citty";
7
7
 
8
8
  // src/commands/branch.ts
9
9
  import { defineCommand } from "citty";
@@ -547,6 +547,76 @@ async function getLogEntries(options) {
547
547
  return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
548
548
  });
549
549
  }
550
+ async function getLocalCommitsGraph(options) {
551
+ const count = options?.count ?? 20;
552
+ const upstream = options?.upstream;
553
+ if (!upstream)
554
+ return [];
555
+ const args = [
556
+ "log",
557
+ "--oneline",
558
+ "--graph",
559
+ "--decorate",
560
+ `--max-count=${count}`,
561
+ "--color=never",
562
+ `${upstream}..HEAD`
563
+ ];
564
+ const { exitCode, stdout } = await run(args);
565
+ if (exitCode !== 0)
566
+ return [];
567
+ return stdout.trimEnd().split(`
568
+ `).filter(Boolean);
569
+ }
570
+ async function getLocalCommitsEntries(options) {
571
+ const count = options?.count ?? 20;
572
+ const upstream = options?.upstream;
573
+ if (!upstream)
574
+ return [];
575
+ const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `${upstream}..HEAD`];
576
+ const { exitCode, stdout } = await run(args);
577
+ if (exitCode !== 0)
578
+ return [];
579
+ return stdout.trimEnd().split(`
580
+ `).filter(Boolean).map((line) => {
581
+ const [hash = "", subject = "", refs = ""] = line.split("||");
582
+ return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
583
+ });
584
+ }
585
+ async function getRemoteOnlyCommitsGraph(options) {
586
+ const count = options?.count ?? 20;
587
+ const upstream = options?.upstream;
588
+ if (!upstream)
589
+ return [];
590
+ const args = [
591
+ "log",
592
+ "--oneline",
593
+ "--graph",
594
+ "--decorate",
595
+ `--max-count=${count}`,
596
+ "--color=never",
597
+ `HEAD..${upstream}`
598
+ ];
599
+ const { exitCode, stdout } = await run(args);
600
+ if (exitCode !== 0)
601
+ return [];
602
+ return stdout.trimEnd().split(`
603
+ `).filter(Boolean);
604
+ }
605
+ async function getRemoteOnlyCommitsEntries(options) {
606
+ const count = options?.count ?? 20;
607
+ const upstream = options?.upstream;
608
+ if (!upstream)
609
+ return [];
610
+ const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `HEAD..${upstream}`];
611
+ const { exitCode, stdout } = await run(args);
612
+ if (exitCode !== 0)
613
+ return [];
614
+ return stdout.trimEnd().split(`
615
+ `).filter(Boolean).map((line) => {
616
+ const [hash = "", subject = "", refs = ""] = line.split("||");
617
+ return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
618
+ });
619
+ }
550
620
  async function getLocalBranches() {
551
621
  const { exitCode, stdout } = await run(["branch", "-vv", "--no-color"]);
552
622
  if (exitCode !== 0)
@@ -575,6 +645,17 @@ async function getRemoteBranches() {
575
645
  return stdout.trimEnd().split(`
576
646
  `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.includes(" -> "));
577
647
  }
648
+ async function isBranchMergedInto(branch, base) {
649
+ const { exitCode } = await run(["merge-base", "--is-ancestor", branch, base]);
650
+ return exitCode === 0;
651
+ }
652
+ async function getLastCommitDate(branch) {
653
+ const { exitCode, stdout } = await run(["log", "-1", "--format=%aI", branch]);
654
+ if (exitCode !== 0)
655
+ return null;
656
+ const date = stdout.trim();
657
+ return date || null;
658
+ }
578
659
 
579
660
  // src/utils/logger.ts
580
661
  import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
@@ -1141,19 +1222,22 @@ function extractJson(raw) {
1141
1222
  }
1142
1223
  return text2;
1143
1224
  }
1144
- async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
1225
+ async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
1145
1226
  try {
1146
1227
  const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1147
1228
  const multiFileHint = stagedFiles.length > 1 ? `
1148
1229
 
1149
1230
  IMPORTANT: Multiple files are staged. Generate ONE commit message that captures the high-level purpose of ALL changes together. Focus on the overall intent, not individual file changes. Be specific but concise — do not list every file.` : "";
1231
+ const squashHint = context === "squash-merge" ? `
1232
+
1233
+ CONTEXT: This is a squash merge of an entire feature branch into the base branch. All commits are being combined into ONE single commit. Generate a single high-level summary that describes the overall feature or change — NOT a list of individual commits. Think: what capability was added or what problem was solved? Be specific but concise.` : "";
1150
1234
  const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
1151
1235
  const userMessage = `Generate a commit message for these staged changes:
1152
1236
 
1153
1237
  Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
1154
1238
 
1155
1239
  Diff:
1156
- ${diffContent}${multiFileHint}`;
1240
+ ${diffContent}${multiFileHint}${squashHint}`;
1157
1241
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
1158
1242
  return result?.trim() ?? null;
1159
1243
  } catch {
@@ -1885,6 +1969,27 @@ ${pc6.bold("Changed files:")}`);
1885
1969
  }
1886
1970
  }
1887
1971
  info(`Staged files: ${stagedFiles.join(", ")}`);
1972
+ const LARGE_COMMIT_THRESHOLD = 10;
1973
+ if (stagedFiles.length >= LARGE_COMMIT_THRESHOLD && !args.group) {
1974
+ const dirs = new Set(stagedFiles.map((f) => f.split("/")[0]));
1975
+ if (dirs.size > 1) {
1976
+ console.log();
1977
+ warn(`You're staging ${pc6.bold(String(stagedFiles.length))} files across ${pc6.bold(String(dirs.size))} directories in a single commit.`);
1978
+ info(pc6.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
1979
+ const choice = await selectPrompt("How would you like to proceed?", [
1980
+ "Continue as single commit",
1981
+ "Switch to group mode (AI splits into atomic commits)",
1982
+ "Cancel"
1983
+ ]);
1984
+ if (choice === "Cancel") {
1985
+ process.exit(0);
1986
+ }
1987
+ if (choice === "Switch to group mode (AI splits into atomic commits)") {
1988
+ await runGroupCommit(args.model, config);
1989
+ return;
1990
+ }
1991
+ }
1992
+ }
1888
1993
  let commitMessage = null;
1889
1994
  const useAI = !args["no-ai"];
1890
1995
  if (useAI) {
@@ -2163,7 +2268,7 @@ import pc7 from "picocolors";
2163
2268
  // package.json
2164
2269
  var package_default = {
2165
2270
  name: "contribute-now",
2166
- version: "0.4.1",
2271
+ version: "0.5.0-dev.914e35d",
2167
2272
  description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
2168
2273
  type: "module",
2169
2274
  bin: {
@@ -2181,9 +2286,10 @@ var package_default = {
2181
2286
  lint: "biome check .",
2182
2287
  "lint:fix": "biome check --write .",
2183
2288
  format: "biome format --write .",
2184
- "www:dev": "bun run --cwd www dev",
2185
- "www:build": "bun run --cwd www build",
2186
- "www:preview": "bun run --cwd www preview"
2289
+ "landing:install": "bun install --cwd landing",
2290
+ "landing:dev": "bun run --cwd landing dev",
2291
+ "landing:build": "bun run --cwd landing build",
2292
+ "landing:preview": "bun run --cwd landing preview"
2187
2293
  },
2188
2294
  engines: {
2189
2295
  node: ">=18",
@@ -2681,7 +2787,19 @@ var log_default = defineCommand6({
2681
2787
  all: {
2682
2788
  type: "boolean",
2683
2789
  alias: "a",
2684
- description: "Show all branches, not just current",
2790
+ description: "Show commits from all branches",
2791
+ default: false
2792
+ },
2793
+ remote: {
2794
+ type: "boolean",
2795
+ alias: "r",
2796
+ description: "Show only remote commits not yet pulled locally",
2797
+ default: false
2798
+ },
2799
+ full: {
2800
+ type: "boolean",
2801
+ alias: "f",
2802
+ description: "Show full commit history for the current branch",
2685
2803
  default: false
2686
2804
  },
2687
2805
  graph: {
@@ -2703,44 +2821,197 @@ var log_default = defineCommand6({
2703
2821
  }
2704
2822
  const config = readConfig();
2705
2823
  const count = args.count ? Number.parseInt(args.count, 10) : 20;
2706
- const showAll = args.all;
2707
2824
  const showGraph = args.graph;
2708
2825
  const targetBranch = args.branch;
2826
+ let mode = "local";
2827
+ if (args.all)
2828
+ mode = "all";
2829
+ else if (args.remote)
2830
+ mode = "remote";
2831
+ else if (args.full || targetBranch)
2832
+ mode = "full";
2709
2833
  const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
2710
2834
  const currentBranch = await getCurrentBranch();
2835
+ const upstream = await getUpstreamRef();
2836
+ let compareRef = upstream;
2837
+ let usingFallback = false;
2838
+ if (!compareRef) {
2839
+ const fallback = await resolveBaseBranchRef(config);
2840
+ if (fallback) {
2841
+ compareRef = fallback;
2842
+ usingFallback = true;
2843
+ }
2844
+ }
2711
2845
  heading("\uD83D\uDCDC commit log");
2712
- if (showGraph) {
2713
- const lines = await getLogGraph({ count, all: showAll, branch: targetBranch });
2714
- if (lines.length === 0) {
2715
- console.log(pc9.dim(" No commits found."));
2846
+ printModeHeader(mode, currentBranch, compareRef, usingFallback);
2847
+ if (mode === "local" || mode === "remote") {
2848
+ if (!compareRef) {
2849
+ console.log();
2850
+ console.log(pc9.yellow(" ⚠ Could not determine a comparison branch."));
2851
+ console.log(pc9.dim(" No upstream tracking set and no remote base branch found."));
2852
+ console.log(pc9.dim(` Use ${pc9.bold("contrib log --full")} to see the full commit history instead.`));
2716
2853
  console.log();
2854
+ printGuidance();
2717
2855
  return;
2718
2856
  }
2719
- console.log();
2720
- for (const line of lines) {
2721
- console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
2857
+ const hasCommits = await renderScopedLog({ mode, count, upstream: compareRef, showGraph, protectedBranches, currentBranch });
2858
+ if (!hasCommits) {
2859
+ printGuidance();
2860
+ return;
2722
2861
  }
2723
2862
  } else {
2724
- const entries = await getLogEntries({ count, all: showAll, branch: targetBranch });
2725
- if (entries.length === 0) {
2726
- console.log(pc9.dim(" No commits found."));
2727
- console.log();
2863
+ const hasCommits = await renderFullLog({ count, all: mode === "all", showGraph, targetBranch, protectedBranches, currentBranch });
2864
+ if (!hasCommits) {
2865
+ printGuidance();
2728
2866
  return;
2729
2867
  }
2730
- console.log();
2731
- for (const entry of entries) {
2732
- const hashStr = pc9.yellow(entry.hash);
2733
- const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
2734
- const subjectStr = colorizeSubject(entry.subject);
2735
- console.log(` ${hashStr}${refsStr} ${subjectStr}`);
2868
+ }
2869
+ printFooter(mode, count, targetBranch);
2870
+ printGuidance();
2871
+ }
2872
+ });
2873
+ async function resolveBaseBranchRef(config) {
2874
+ if (!config) {
2875
+ for (const candidate2 of ["origin/main", "origin/master"]) {
2876
+ if (await branchExists(candidate2))
2877
+ return candidate2;
2878
+ }
2879
+ return null;
2880
+ }
2881
+ const baseBranch = getBaseBranch(config);
2882
+ const remote = config.origin ?? "origin";
2883
+ const candidate = `${remote}/${baseBranch}`;
2884
+ if (await branchExists(candidate))
2885
+ return candidate;
2886
+ for (const fallback of ["origin/main", "origin/master"]) {
2887
+ if (fallback !== candidate && await branchExists(fallback))
2888
+ return fallback;
2889
+ }
2890
+ return null;
2891
+ }
2892
+ function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
2893
+ const branch = currentBranch ?? "HEAD";
2894
+ const fallbackNote = usingFallback ? pc9.yellow(" (no upstream — comparing against base branch)") : "";
2895
+ console.log();
2896
+ switch (mode) {
2897
+ case "local":
2898
+ console.log(pc9.dim(` mode: ${pc9.bold("local")} — unpushed commits on ${pc9.bold(branch)}`) + fallbackNote);
2899
+ if (compareRef) {
2900
+ console.log(pc9.dim(` comparing: ${pc9.bold(compareRef)} ➜ ${pc9.bold("HEAD")}`));
2901
+ }
2902
+ break;
2903
+ case "remote":
2904
+ console.log(pc9.dim(` mode: ${pc9.bold("remote")} — commits on remote not yet pulled into ${pc9.bold(branch)}`) + fallbackNote);
2905
+ if (compareRef) {
2906
+ console.log(pc9.dim(` comparing: ${pc9.bold("HEAD")} ➜ ${pc9.bold(compareRef)}`));
2736
2907
  }
2908
+ break;
2909
+ case "full":
2910
+ console.log(pc9.dim(` mode: ${pc9.bold("full")} — complete commit history for ${pc9.bold(branch)}`));
2911
+ break;
2912
+ case "all":
2913
+ console.log(pc9.dim(` mode: ${pc9.bold("all")} — commits across all branches`));
2914
+ break;
2915
+ }
2916
+ }
2917
+ async function renderScopedLog(options) {
2918
+ const { mode, count, upstream, showGraph, protectedBranches, currentBranch } = options;
2919
+ if (showGraph) {
2920
+ const graphFn = mode === "local" ? getLocalCommitsGraph : getRemoteOnlyCommitsGraph;
2921
+ const lines = await graphFn({ count, upstream });
2922
+ if (lines.length === 0) {
2923
+ printEmptyState(mode);
2924
+ return false;
2737
2925
  }
2738
2926
  console.log();
2739
- console.log(pc9.dim(` Showing ${count} most recent commits${showAll ? " (all branches)" : targetBranch ? ` (${targetBranch})` : ""}`));
2740
- console.log(pc9.dim(` Use ${pc9.bold("contrib log -n 50")} for more, or ${pc9.bold("contrib log --all")} for all branches`));
2927
+ for (const line of lines) {
2928
+ console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
2929
+ }
2930
+ } else {
2931
+ const entryFn = mode === "local" ? getLocalCommitsEntries : getRemoteOnlyCommitsEntries;
2932
+ const entries = await entryFn({ count, upstream });
2933
+ if (entries.length === 0) {
2934
+ printEmptyState(mode);
2935
+ return false;
2936
+ }
2741
2937
  console.log();
2938
+ for (const entry of entries) {
2939
+ const hashStr = pc9.yellow(entry.hash);
2940
+ const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
2941
+ const subjectStr = colorizeSubject(entry.subject);
2942
+ console.log(` ${hashStr}${refsStr} ${subjectStr}`);
2943
+ }
2742
2944
  }
2743
- });
2945
+ return true;
2946
+ }
2947
+ function printEmptyState(mode) {
2948
+ console.log();
2949
+ if (mode === "local") {
2950
+ console.log(pc9.dim(" No local unpushed commits — you're up to date with remote!"));
2951
+ } else {
2952
+ console.log(pc9.dim(" No remote-only commits — your local branch is up to date!"));
2953
+ }
2954
+ console.log();
2955
+ }
2956
+ async function renderFullLog(options) {
2957
+ const { count, all, showGraph, targetBranch, protectedBranches, currentBranch } = options;
2958
+ if (showGraph) {
2959
+ const lines = await getLogGraph({ count, all, branch: targetBranch });
2960
+ if (lines.length === 0) {
2961
+ console.log(pc9.dim(" No commits found."));
2962
+ console.log();
2963
+ return false;
2964
+ }
2965
+ console.log();
2966
+ for (const line of lines) {
2967
+ console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
2968
+ }
2969
+ } else {
2970
+ const entries = await getLogEntries({ count, all, branch: targetBranch });
2971
+ if (entries.length === 0) {
2972
+ console.log(pc9.dim(" No commits found."));
2973
+ console.log();
2974
+ return false;
2975
+ }
2976
+ console.log();
2977
+ for (const entry of entries) {
2978
+ const hashStr = pc9.yellow(entry.hash);
2979
+ const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
2980
+ const subjectStr = colorizeSubject(entry.subject);
2981
+ console.log(` ${hashStr}${refsStr} ${subjectStr}`);
2982
+ }
2983
+ }
2984
+ return true;
2985
+ }
2986
+ function printFooter(mode, count, targetBranch) {
2987
+ console.log();
2988
+ switch (mode) {
2989
+ case "local":
2990
+ console.log(pc9.dim(` Showing up to ${count} unpushed commits`));
2991
+ break;
2992
+ case "remote":
2993
+ console.log(pc9.dim(` Showing up to ${count} remote-only commits`));
2994
+ break;
2995
+ case "full":
2996
+ console.log(pc9.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
2997
+ break;
2998
+ case "all":
2999
+ console.log(pc9.dim(` Showing ${count} most recent commits (all branches)`));
3000
+ break;
3001
+ }
3002
+ }
3003
+ function printGuidance() {
3004
+ console.log();
3005
+ console.log(pc9.dim(" ─── quick guide ───"));
3006
+ console.log(pc9.dim(` ${pc9.bold("contrib log")} local unpushed commits (default)`));
3007
+ console.log(pc9.dim(` ${pc9.bold("contrib log --remote")} commits on remote not yet pulled`));
3008
+ console.log(pc9.dim(` ${pc9.bold("contrib log --full")} full history for the current branch`));
3009
+ console.log(pc9.dim(` ${pc9.bold("contrib log --all")} commits across all branches`));
3010
+ console.log(pc9.dim(` ${pc9.bold("contrib log -n 50")} change the commit limit (default: 20)`));
3011
+ console.log(pc9.dim(` ${pc9.bold("contrib log -b dev")} view log for a specific branch`));
3012
+ console.log(pc9.dim(` ${pc9.bold("contrib log --no-graph")} flat list without graph lines`));
3013
+ console.log();
3014
+ }
2744
3015
  function colorizeGraphLine(line, protectedBranches, currentBranch) {
2745
3016
  const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
2746
3017
  if (!match) {
@@ -2819,9 +3090,161 @@ function colorizeSubject(subject) {
2819
3090
  return pc9.white(subject);
2820
3091
  }
2821
3092
 
2822
- // src/commands/setup.ts
3093
+ // src/commands/save.ts
2823
3094
  import { defineCommand as defineCommand7 } from "citty";
2824
3095
  import pc10 from "picocolors";
3096
+ import { execFile as execFileCb4 } from "node:child_process";
3097
+ function gitRun(args) {
3098
+ return new Promise((resolve) => {
3099
+ execFileCb4("git", args, (err, stdout, stderr) => {
3100
+ resolve({
3101
+ exitCode: err ? err.code === "ENOENT" ? 127 : err.status ?? 1 : 0,
3102
+ stdout: stdout ?? "",
3103
+ stderr: stderr ?? ""
3104
+ });
3105
+ });
3106
+ });
3107
+ }
3108
+ var save_default = defineCommand7({
3109
+ meta: {
3110
+ name: "save",
3111
+ description: "Save, restore, or manage uncommitted changes"
3112
+ },
3113
+ args: {
3114
+ action: {
3115
+ type: "positional",
3116
+ description: "Action: save (default), restore, list, drop",
3117
+ required: false
3118
+ },
3119
+ message: {
3120
+ type: "string",
3121
+ alias: "m",
3122
+ description: "Description for saved changes"
3123
+ }
3124
+ },
3125
+ async run({ args }) {
3126
+ if (!await isGitRepo()) {
3127
+ error("Not inside a git repository.");
3128
+ process.exit(1);
3129
+ }
3130
+ const action = args.action ?? "save";
3131
+ switch (action) {
3132
+ case "save":
3133
+ await handleSave(args.message);
3134
+ break;
3135
+ case "restore":
3136
+ await handleRestore();
3137
+ break;
3138
+ case "list":
3139
+ await handleList();
3140
+ break;
3141
+ case "drop":
3142
+ await handleDrop();
3143
+ break;
3144
+ default:
3145
+ error(`Unknown action: ${action}. Use save, restore, list, or drop.`);
3146
+ process.exit(1);
3147
+ }
3148
+ }
3149
+ });
3150
+ async function handleSave(message) {
3151
+ heading("\uD83D\uDCBE contrib save");
3152
+ const currentBranch = await getCurrentBranch();
3153
+ const label = message ?? `work-in-progress on ${currentBranch ?? "unknown"}`;
3154
+ const stashMsg = `contrib-save: ${label}`;
3155
+ const result = await gitRun(["stash", "push", "-m", stashMsg]);
3156
+ if (result.exitCode !== 0) {
3157
+ error(`Failed to save: ${result.stderr}`);
3158
+ process.exit(1);
3159
+ }
3160
+ if (result.stdout.includes("No local changes to save")) {
3161
+ info("No uncommitted changes to save.");
3162
+ return;
3163
+ }
3164
+ success(`Saved: ${pc10.dim(label)}`);
3165
+ info(`Use ${pc10.bold("contrib save restore")} to bring them back.`);
3166
+ }
3167
+ async function handleRestore() {
3168
+ heading("\uD83D\uDCBE contrib save restore");
3169
+ const stashes = await getStashList();
3170
+ if (stashes.length === 0) {
3171
+ info("No saved changes found.");
3172
+ return;
3173
+ }
3174
+ if (stashes.length === 1) {
3175
+ const result2 = await gitRun(["stash", "pop", "stash@{0}"]);
3176
+ if (result2.exitCode !== 0) {
3177
+ error(`Failed to restore: ${result2.stderr}`);
3178
+ warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
3179
+ process.exit(1);
3180
+ }
3181
+ success(`Restored: ${pc10.dim(stashes[0].message)}`);
3182
+ return;
3183
+ }
3184
+ const choices = stashes.map((s) => `${s.index} ${s.message}`);
3185
+ const selected = await selectPrompt("Which save to restore?", choices);
3186
+ const idx = selected.split(/\s{2,}/)[0].trim();
3187
+ const result = await gitRun(["stash", "pop", `stash@{${idx}}`]);
3188
+ if (result.exitCode !== 0) {
3189
+ error(`Failed to restore: ${result.stderr}`);
3190
+ warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
3191
+ process.exit(1);
3192
+ }
3193
+ const match = stashes.find((s) => String(s.index) === idx);
3194
+ success(`Restored: ${pc10.dim(match?.message ?? "saved changes")}`);
3195
+ }
3196
+ async function handleList() {
3197
+ heading("\uD83D\uDCBE contrib save list");
3198
+ const stashes = await getStashList();
3199
+ if (stashes.length === 0) {
3200
+ info("No saved changes.");
3201
+ return;
3202
+ }
3203
+ console.log();
3204
+ for (const s of stashes) {
3205
+ const idx = pc10.dim(`[${s.index}]`);
3206
+ const msg = s.message;
3207
+ console.log(` ${idx} ${msg}`);
3208
+ }
3209
+ console.log();
3210
+ info(`Use ${pc10.bold("contrib save restore")} to bring changes back.`);
3211
+ info(`Use ${pc10.bold("contrib save drop")} to discard saved changes.`);
3212
+ }
3213
+ async function handleDrop() {
3214
+ heading("\uD83D\uDCBE contrib save drop");
3215
+ const stashes = await getStashList();
3216
+ if (stashes.length === 0) {
3217
+ info("No saved changes to drop.");
3218
+ return;
3219
+ }
3220
+ const choices = stashes.map((s) => `${s.index} ${s.message}`);
3221
+ const selected = await selectPrompt("Which save to drop?", choices);
3222
+ const idx = selected.split(/\s{2,}/)[0].trim();
3223
+ const result = await gitRun(["stash", "drop", `stash@{${idx}}`]);
3224
+ if (result.exitCode !== 0) {
3225
+ error(`Failed to drop: ${result.stderr}`);
3226
+ process.exit(1);
3227
+ }
3228
+ const match = stashes.find((s) => String(s.index) === idx);
3229
+ success(`Dropped: ${pc10.dim(match?.message ?? "saved changes")}`);
3230
+ }
3231
+ async function getStashList() {
3232
+ const result = await gitRun(["stash", "list"]);
3233
+ if (result.exitCode !== 0 || !result.stdout.trim())
3234
+ return [];
3235
+ return result.stdout.trimEnd().split(`
3236
+ `).filter(Boolean).map((line) => {
3237
+ const idxMatch = line.match(/^stash@\{(\d+)\}/);
3238
+ const index = idxMatch ? Number.parseInt(idxMatch[1], 10) : 0;
3239
+ const parts = line.split(": ");
3240
+ const message = parts.length > 2 ? parts.slice(2).join(": ") : parts[parts.length - 1];
3241
+ return { index, message };
3242
+ });
3243
+ }
3244
+
3245
+ // src/commands/setup.ts
3246
+ import { defineCommand as defineCommand8 } from "citty";
3247
+ import pc11 from "picocolors";
2825
3248
  async function shouldContinueSetupWithExistingConfig(options) {
2826
3249
  const {
2827
3250
  existingConfig,
@@ -2859,7 +3282,7 @@ async function shouldContinueSetupWithExistingConfig(options) {
2859
3282
  }
2860
3283
  return true;
2861
3284
  }
2862
- var setup_default = defineCommand7({
3285
+ var setup_default = defineCommand8({
2863
3286
  meta: {
2864
3287
  name: "setup",
2865
3288
  description: "Initialize contribute-now config for this repo (.contributerc.json)"
@@ -2894,7 +3317,7 @@ var setup_default = defineCommand7({
2894
3317
  workflow = "github-flow";
2895
3318
  else if (workflowChoice.startsWith("Git Flow"))
2896
3319
  workflow = "git-flow";
2897
- info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
3320
+ info(`Workflow: ${pc11.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
2898
3321
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
2899
3322
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
2900
3323
  CONVENTION_DESCRIPTIONS.conventional,
@@ -2958,15 +3381,15 @@ var setup_default = defineCommand7({
2958
3381
  detectedRole = roleChoice;
2959
3382
  detectionSource = "user selection";
2960
3383
  } else {
2961
- info(`Detected role: ${pc10.bold(detectedRole)} (via ${detectionSource})`);
2962
- const confirmed = await confirmPrompt(`Role detected as ${pc10.bold(detectedRole)}. Is this correct?`);
3384
+ info(`Detected role: ${pc11.bold(detectedRole)} (via ${detectionSource})`);
3385
+ const confirmed = await confirmPrompt(`Role detected as ${pc11.bold(detectedRole)}. Is this correct?`);
2963
3386
  if (!confirmed) {
2964
3387
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
2965
3388
  detectedRole = roleChoice;
2966
3389
  }
2967
3390
  }
2968
3391
  const defaultConfig = getDefaultConfig();
2969
- info(pc10.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
3392
+ info(pc11.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
2970
3393
  const mainBranchDefault = defaultConfig.mainBranch;
2971
3394
  const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
2972
3395
  let devBranch;
@@ -2992,7 +3415,7 @@ var setup_default = defineCommand7({
2992
3415
  error("Setup cannot continue without the upstream remote for contributors.");
2993
3416
  process.exit(1);
2994
3417
  }
2995
- success(`Added remote ${pc10.bold(upstreamRemote)} → ${upstreamUrl}`);
3418
+ success(`Added remote ${pc11.bold(upstreamRemote)} → ${upstreamUrl}`);
2996
3419
  } else {
2997
3420
  error("An upstream remote URL is required for contributors.");
2998
3421
  info("Add it manually: git remote add upstream <url>");
@@ -3013,17 +3436,17 @@ var setup_default = defineCommand7({
3013
3436
  writeConfig(config);
3014
3437
  success(`✅ Config written to .contributerc.json`);
3015
3438
  const syncRemote = config.role === "contributor" ? config.upstream : config.origin;
3016
- info(`Fetching ${pc10.bold(syncRemote)} to verify branch configuration...`);
3439
+ info(`Fetching ${pc11.bold(syncRemote)} to verify branch configuration...`);
3017
3440
  await fetchRemote(syncRemote);
3018
3441
  const mainRef = `${syncRemote}/${config.mainBranch}`;
3019
3442
  if (!await refExists(mainRef)) {
3020
- warn(`Main branch ref ${pc10.bold(mainRef)} not found on remote.`);
3443
+ warn(`Main branch ref ${pc11.bold(mainRef)} not found on remote.`);
3021
3444
  warn("Config was saved — verify the branch name and re-run setup if needed.");
3022
3445
  }
3023
3446
  if (config.devBranch) {
3024
3447
  const devRef = `${syncRemote}/${config.devBranch}`;
3025
3448
  if (!await refExists(devRef)) {
3026
- warn(`Dev branch ref ${pc10.bold(devRef)} not found on remote.`);
3449
+ warn(`Dev branch ref ${pc11.bold(devRef)} not found on remote.`);
3027
3450
  warn("Config was saved — verify the branch name and re-run setup if needed.");
3028
3451
  }
3029
3452
  }
@@ -3031,33 +3454,33 @@ var setup_default = defineCommand7({
3031
3454
  info("Added .contributerc.json to .gitignore to avoid committing personal config.");
3032
3455
  }
3033
3456
  console.log();
3034
- info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3035
- info(`Convention: ${pc10.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
3036
- info(`Role: ${pc10.bold(config.role)}`);
3457
+ info(`Workflow: ${pc11.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3458
+ info(`Convention: ${pc11.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
3459
+ info(`Role: ${pc11.bold(config.role)}`);
3037
3460
  if (config.devBranch) {
3038
- info(`Main: ${pc10.bold(config.mainBranch)} | Dev: ${pc10.bold(config.devBranch)}`);
3461
+ info(`Main: ${pc11.bold(config.mainBranch)} | Dev: ${pc11.bold(config.devBranch)}`);
3039
3462
  } else {
3040
- info(`Main: ${pc10.bold(config.mainBranch)}`);
3463
+ info(`Main: ${pc11.bold(config.mainBranch)}`);
3041
3464
  }
3042
- info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
3465
+ info(`Origin: ${pc11.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc11.bold(config.upstream)}` : ""}`);
3043
3466
  }
3044
3467
  });
3045
3468
  function logConfigSummary(config) {
3046
- info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3047
- info(`Convention: ${pc10.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
3048
- info(`Role: ${pc10.bold(config.role)}`);
3469
+ info(`Workflow: ${pc11.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3470
+ info(`Convention: ${pc11.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
3471
+ info(`Role: ${pc11.bold(config.role)}`);
3049
3472
  if (config.devBranch) {
3050
- info(`Main: ${pc10.bold(config.mainBranch)} | Dev: ${pc10.bold(config.devBranch)}`);
3473
+ info(`Main: ${pc11.bold(config.mainBranch)} | Dev: ${pc11.bold(config.devBranch)}`);
3051
3474
  } else {
3052
- info(`Main: ${pc10.bold(config.mainBranch)}`);
3475
+ info(`Main: ${pc11.bold(config.mainBranch)}`);
3053
3476
  }
3054
- info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
3477
+ info(`Origin: ${pc11.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc11.bold(config.upstream)}` : ""}`);
3055
3478
  }
3056
3479
 
3057
3480
  // src/commands/start.ts
3058
- import { defineCommand as defineCommand8 } from "citty";
3059
- import pc11 from "picocolors";
3060
- var start_default = defineCommand8({
3481
+ import { defineCommand as defineCommand9 } from "citty";
3482
+ import pc12 from "picocolors";
3483
+ var start_default = defineCommand9({
3061
3484
  meta: {
3062
3485
  name: "start",
3063
3486
  description: "Create a new feature branch from the latest base branch"
@@ -3113,8 +3536,8 @@ var start_default = defineCommand8({
3113
3536
  if (suggested) {
3114
3537
  spinner.success("Branch name suggestion ready.");
3115
3538
  console.log(`
3116
- ${pc11.dim("AI suggestion:")} ${pc11.bold(pc11.cyan(suggested))}`);
3117
- const accepted = await confirmPrompt(`Use ${pc11.bold(suggested)} as your branch name?`);
3539
+ ${pc12.dim("AI suggestion:")} ${pc12.bold(pc12.cyan(suggested))}`);
3540
+ const accepted = await confirmPrompt(`Use ${pc12.bold(suggested)} as your branch name?`);
3118
3541
  if (accepted) {
3119
3542
  branchName = suggested;
3120
3543
  } else {
@@ -3125,28 +3548,28 @@ var start_default = defineCommand8({
3125
3548
  }
3126
3549
  }
3127
3550
  if (!hasPrefix(branchName, branchPrefixes)) {
3128
- const prefix = await selectPrompt(`Choose a branch type for ${pc11.bold(branchName)}:`, branchPrefixes);
3551
+ const prefix = await selectPrompt(`Choose a branch type for ${pc12.bold(branchName)}:`, branchPrefixes);
3129
3552
  branchName = formatBranchName(prefix, branchName);
3130
3553
  }
3131
3554
  if (!isValidBranchName(branchName)) {
3132
3555
  error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
3133
3556
  process.exit(1);
3134
3557
  }
3135
- info(`Creating branch: ${pc11.bold(branchName)}`);
3558
+ info(`Creating branch: ${pc12.bold(branchName)}`);
3136
3559
  if (await branchExists(branchName)) {
3137
- error(`Branch ${pc11.bold(branchName)} already exists.`);
3138
- info(` Use ${pc11.bold(`git checkout ${branchName}`)} to switch to it, or choose a different name.`);
3560
+ error(`Branch ${pc12.bold(branchName)} already exists.`);
3561
+ info(` Use ${pc12.bold(`git checkout ${branchName}`)} to switch to it, or choose a different name.`);
3139
3562
  process.exit(1);
3140
3563
  }
3141
3564
  await fetchRemote(syncSource.remote);
3142
3565
  if (!await refExists(syncSource.ref)) {
3143
- warn(`Remote ref ${pc11.bold(syncSource.ref)} not found. Creating branch from local ${pc11.bold(baseBranch)}.`);
3566
+ warn(`Remote ref ${pc12.bold(syncSource.ref)} not found. Creating branch from local ${pc12.bold(baseBranch)}.`);
3144
3567
  }
3145
3568
  const currentBranch = await getCurrentBranch();
3146
3569
  if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
3147
3570
  const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
3148
3571
  if (ahead > 0) {
3149
- warn(`You are on ${pc11.bold(baseBranch)} with ${pc11.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${pc11.bold(syncSource.ref)}.`);
3572
+ warn(`You are on ${pc12.bold(baseBranch)} with ${pc12.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${pc12.bold(syncSource.ref)}.`);
3150
3573
  info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
3151
3574
  const proceed = await confirmPrompt("Discard local commits and sync to remote?");
3152
3575
  if (!proceed) {
@@ -3163,10 +3586,10 @@ var start_default = defineCommand8({
3163
3586
  error(`Failed to create branch: ${result2.stderr}`);
3164
3587
  process.exit(1);
3165
3588
  }
3166
- success(`✅ Created ${pc11.bold(branchName)} from ${pc11.bold(syncSource.ref)}`);
3589
+ success(`✅ Created ${pc12.bold(branchName)} from ${pc12.bold(syncSource.ref)}`);
3167
3590
  return;
3168
3591
  }
3169
- error(`Failed to update ${pc11.bold(baseBranch)}: ${updateResult.stderr}`);
3592
+ error(`Failed to update ${pc12.bold(baseBranch)}: ${updateResult.stderr}`);
3170
3593
  info("Make sure your base branch exists locally or the remote ref is available.");
3171
3594
  process.exit(1);
3172
3595
  }
@@ -3175,14 +3598,14 @@ var start_default = defineCommand8({
3175
3598
  error(`Failed to create branch: ${result.stderr}`);
3176
3599
  process.exit(1);
3177
3600
  }
3178
- success(`✅ Created ${pc11.bold(branchName)} from latest ${pc11.bold(baseBranch)}`);
3601
+ success(`✅ Created ${pc12.bold(branchName)} from latest ${pc12.bold(baseBranch)}`);
3179
3602
  }
3180
3603
  });
3181
3604
 
3182
3605
  // src/commands/status.ts
3183
- import { defineCommand as defineCommand9 } from "citty";
3184
- import pc12 from "picocolors";
3185
- var status_default = defineCommand9({
3606
+ import { defineCommand as defineCommand10 } from "citty";
3607
+ import pc13 from "picocolors";
3608
+ var status_default = defineCommand10({
3186
3609
  meta: {
3187
3610
  name: "status",
3188
3611
  description: "Show sync status of branches"
@@ -3198,8 +3621,8 @@ var status_default = defineCommand9({
3198
3621
  process.exit(1);
3199
3622
  }
3200
3623
  heading("\uD83D\uDCCA contribute-now status");
3201
- console.log(` ${pc12.dim("Workflow:")} ${pc12.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3202
- console.log(` ${pc12.dim("Role:")} ${pc12.bold(config.role)}`);
3624
+ console.log(` ${pc13.dim("Workflow:")} ${pc13.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3625
+ console.log(` ${pc13.dim("Role:")} ${pc13.bold(config.role)}`);
3203
3626
  console.log();
3204
3627
  await fetchAll();
3205
3628
  const currentBranch = await getCurrentBranch();
@@ -3208,7 +3631,7 @@ var status_default = defineCommand9({
3208
3631
  const isContributor = config.role === "contributor";
3209
3632
  const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
3210
3633
  if (dirty) {
3211
- console.log(` ${pc12.yellow("⚠")} ${pc12.yellow("Uncommitted changes in working tree")}`);
3634
+ console.log(` ${pc13.yellow("⚠")} ${pc13.yellow("Uncommitted changes in working tree")}`);
3212
3635
  console.log();
3213
3636
  }
3214
3637
  const mainRemote = `${origin}/${mainBranch}`;
@@ -3221,85 +3644,143 @@ var status_default = defineCommand9({
3221
3644
  const devLine = formatStatus(config.devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
3222
3645
  console.log(devLine);
3223
3646
  }
3224
- if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
3647
+ const protectedBranches = getProtectedBranches(config);
3648
+ const isFeatureBranch = currentBranch && !protectedBranches.includes(currentBranch);
3649
+ let branchStatus = null;
3650
+ if (isFeatureBranch) {
3225
3651
  const branchDiv = await getDivergence(currentBranch, baseBranch);
3226
3652
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
3227
- console.log(branchLine + pc12.dim(` (current ${pc12.green("*")})`));
3653
+ console.log(branchLine + pc13.dim(` (current ${pc13.green("*")})`));
3654
+ branchStatus = await detectBranchStatus(currentBranch, baseBranch);
3655
+ if (branchStatus.merged) {
3656
+ console.log(` ${pc13.green("✓")} ${pc13.green("Branch merged")} — ${pc13.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
3657
+ }
3658
+ if (branchStatus.stale) {
3659
+ console.log(` ${pc13.yellow("⏳")} ${pc13.yellow("Branch is stale")} — ${pc13.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
3660
+ }
3228
3661
  } else if (currentBranch) {
3229
- console.log(pc12.dim(` (on ${pc12.bold(currentBranch)} branch)`));
3662
+ console.log(pc13.dim(` (on ${pc13.bold(currentBranch)} branch)`));
3230
3663
  }
3231
3664
  const hasFiles = fileStatus.staged.length > 0 || fileStatus.modified.length > 0 || fileStatus.untracked.length > 0;
3232
3665
  if (hasFiles) {
3233
3666
  console.log();
3234
3667
  if (fileStatus.staged.length > 0) {
3235
- console.log(` ${pc12.green("Staged for commit:")}`);
3668
+ console.log(` ${pc13.green("Staged for commit:")}`);
3236
3669
  for (const { file, status } of fileStatus.staged) {
3237
- console.log(` ${pc12.green("+")} ${pc12.dim(`${status}:`)} ${file}`);
3670
+ console.log(` ${pc13.green("+")} ${pc13.dim(`${status}:`)} ${file}`);
3238
3671
  }
3239
3672
  }
3240
3673
  if (fileStatus.modified.length > 0) {
3241
- console.log(` ${pc12.yellow("Unstaged changes:")}`);
3674
+ console.log(` ${pc13.yellow("Unstaged changes:")}`);
3242
3675
  for (const { file, status } of fileStatus.modified) {
3243
- console.log(` ${pc12.yellow("~")} ${pc12.dim(`${status}:`)} ${file}`);
3676
+ console.log(` ${pc13.yellow("~")} ${pc13.dim(`${status}:`)} ${file}`);
3244
3677
  }
3245
3678
  }
3246
3679
  if (fileStatus.untracked.length > 0) {
3247
- console.log(` ${pc12.red("Untracked files:")}`);
3680
+ console.log(` ${pc13.red("Untracked files:")}`);
3248
3681
  for (const file of fileStatus.untracked) {
3249
- console.log(` ${pc12.red("?")} ${file}`);
3682
+ console.log(` ${pc13.red("?")} ${file}`);
3250
3683
  }
3251
3684
  }
3252
3685
  } else if (!dirty) {
3253
- console.log(` ${pc12.green("✓")} ${pc12.dim("Working tree clean")}`);
3686
+ console.log(` ${pc13.green("✓")} ${pc13.dim("Working tree clean")}`);
3254
3687
  }
3255
3688
  const tips = [];
3256
3689
  if (fileStatus.staged.length > 0) {
3257
- tips.push(`Run ${pc12.bold("contrib commit")} to commit staged changes`);
3690
+ tips.push(`Run ${pc13.bold("contrib commit")} to commit staged changes`);
3258
3691
  }
3259
3692
  if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
3260
- tips.push(`Run ${pc12.bold("contrib commit")} to stage and commit changes`);
3261
- }
3262
- if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0 && currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
3263
- const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
3264
- if (branchDiv.ahead > 0) {
3265
- tips.push(`Run ${pc12.bold("contrib submit")} to push and create/update your PR`);
3693
+ tips.push(`Run ${pc13.bold("contrib commit")} to stage and commit changes`);
3694
+ }
3695
+ if (isFeatureBranch && branchStatus) {
3696
+ if (branchStatus.merged) {
3697
+ tips.push(`Run ${pc13.bold("contrib clean")} to delete this merged branch`);
3698
+ } else if (branchStatus.stale) {
3699
+ tips.push(`Run ${pc13.bold("contrib sync")} to rebase on latest changes, or ${pc13.bold("contrib clean")} if no longer needed`);
3700
+ } else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
3701
+ const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
3702
+ if (branchDiv.ahead > 0) {
3703
+ tips.push(`Run ${pc13.bold("contrib submit")} to push and create/update your PR`);
3704
+ }
3266
3705
  }
3267
3706
  }
3268
3707
  if (tips.length > 0) {
3269
3708
  console.log();
3270
- console.log(` ${pc12.dim("\uD83D\uDCA1 Tip:")}`);
3709
+ console.log(` ${pc13.dim("\uD83D\uDCA1 Tip:")}`);
3271
3710
  for (const tip of tips) {
3272
- console.log(` ${pc12.dim(tip)}`);
3711
+ console.log(` ${pc13.dim(tip)}`);
3273
3712
  }
3274
3713
  }
3275
3714
  console.log();
3276
3715
  }
3277
3716
  });
3278
3717
  function formatStatus(branch, base, ahead, behind) {
3279
- const label = pc12.bold(branch.padEnd(20));
3718
+ const label = pc13.bold(branch.padEnd(20));
3280
3719
  if (ahead === 0 && behind === 0) {
3281
- return ` ${pc12.green("✓")} ${label} ${pc12.dim(`in sync with ${base}`)}`;
3720
+ return ` ${pc13.green("✓")} ${label} ${pc13.dim(`in sync with ${base}`)}`;
3282
3721
  }
3283
3722
  if (ahead > 0 && behind === 0) {
3284
- return ` ${pc12.yellow("↑")} ${label} ${pc12.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
3723
+ return ` ${pc13.yellow("↑")} ${label} ${pc13.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
3285
3724
  }
3286
3725
  if (behind > 0 && ahead === 0) {
3287
- return ` ${pc12.red("↓")} ${label} ${pc12.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
3726
+ return ` ${pc13.red("↓")} ${label} ${pc13.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
3727
+ }
3728
+ return ` ${pc13.red("⚡")} ${label} ${pc13.yellow(`${ahead} ahead`)}${pc13.dim(", ")}${pc13.red(`${behind} behind`)} ${pc13.dim(base)}`;
3729
+ }
3730
+ var STALE_THRESHOLD_DAYS = 14;
3731
+ async function detectBranchStatus(branch, baseBranch) {
3732
+ const result = { merged: false, mergedReason: null, stale: false, staleDaysAgo: null };
3733
+ const div = await getDivergence(branch, baseBranch);
3734
+ const hasWork = div.ahead > 0;
3735
+ if (hasWork) {
3736
+ if (await isBranchMergedInto(branch, baseBranch)) {
3737
+ result.merged = true;
3738
+ result.mergedReason = `all commits reachable from ${baseBranch}`;
3739
+ return result;
3740
+ }
3741
+ const mergedBranches = await getMergedBranches(baseBranch);
3742
+ if (mergedBranches.includes(branch)) {
3743
+ result.merged = true;
3744
+ result.mergedReason = `listed in merged branches of ${baseBranch}`;
3745
+ return result;
3746
+ }
3747
+ }
3748
+ const goneBranches = await getGoneBranches();
3749
+ if (goneBranches.includes(branch)) {
3750
+ result.merged = true;
3751
+ result.mergedReason = "remote branch deleted (likely squash-merged)";
3752
+ return result;
3753
+ }
3754
+ if (await checkGhInstalled()) {
3755
+ const mergedPR = await getMergedPRForBranch(branch);
3756
+ if (mergedPR) {
3757
+ result.merged = true;
3758
+ result.mergedReason = `PR #${mergedPR.number} was merged`;
3759
+ return result;
3760
+ }
3288
3761
  }
3289
- return ` ${pc12.red("⚡")} ${label} ${pc12.yellow(`${ahead} ahead`)}${pc12.dim(", ")}${pc12.red(`${behind} behind`)} ${pc12.dim(base)}`;
3762
+ const lastDate = await getLastCommitDate(branch);
3763
+ if (lastDate) {
3764
+ const daysAgo = Math.floor((Date.now() - new Date(lastDate).getTime()) / (1000 * 60 * 60 * 24));
3765
+ if (daysAgo >= STALE_THRESHOLD_DAYS) {
3766
+ result.stale = true;
3767
+ result.staleDaysAgo = daysAgo;
3768
+ }
3769
+ }
3770
+ return result;
3290
3771
  }
3291
3772
 
3292
3773
  // src/commands/submit.ts
3293
- import { defineCommand as defineCommand10 } from "citty";
3294
- import pc13 from "picocolors";
3774
+ import { defineCommand as defineCommand11 } from "citty";
3775
+ import pc14 from "picocolors";
3295
3776
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
3296
- info(`Checking out ${pc13.bold(baseBranch)}...`);
3777
+ info(`Checking out ${pc14.bold(baseBranch)}...`);
3297
3778
  const coResult = await checkoutBranch(baseBranch);
3298
3779
  if (coResult.exitCode !== 0) {
3299
3780
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
3300
3781
  process.exit(1);
3301
3782
  }
3302
- info(`Squash merging ${pc13.bold(featureBranch)} into ${pc13.bold(baseBranch)}...`);
3783
+ info(`Squash merging ${pc14.bold(featureBranch)} into ${pc14.bold(baseBranch)}...`);
3303
3784
  const mergeResult = await mergeSquash(featureBranch);
3304
3785
  if (mergeResult.exitCode !== 0) {
3305
3786
  error(`Squash merge failed: ${mergeResult.stderr}`);
@@ -3311,10 +3792,12 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
3311
3792
  if (!copilotError) {
3312
3793
  const spinner = createSpinner("Generating AI commit message for squash merge...");
3313
3794
  const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
3314
- const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit");
3795
+ const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
3315
3796
  if (aiMsg) {
3316
3797
  message = aiMsg;
3317
3798
  spinner.success("AI commit message generated.");
3799
+ console.log(`
3800
+ ${pc14.dim("AI suggestion:")} ${pc14.bold(pc14.cyan(message))}`);
3318
3801
  } else {
3319
3802
  spinner.fail("AI did not return a commit message.");
3320
3803
  }
@@ -3322,26 +3805,51 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
3322
3805
  warn(`AI unavailable: ${copilotError}`);
3323
3806
  }
3324
3807
  }
3325
- const fallback = message || `squash merge ${featureBranch}`;
3326
- let finalMsg;
3808
+ let finalMsg = null;
3327
3809
  if (message) {
3328
- console.log(` ${pc13.dim("Commit message:")} ${pc13.bold(message)}`);
3329
- finalMsg = message;
3810
+ while (!finalMsg) {
3811
+ const action = await selectPrompt("What would you like to do?", [
3812
+ "Accept this message",
3813
+ "Edit this message",
3814
+ "Regenerate",
3815
+ "Write manually"
3816
+ ]);
3817
+ if (action === "Accept this message") {
3818
+ finalMsg = message;
3819
+ } else if (action === "Edit this message") {
3820
+ finalMsg = await inputPrompt("Edit commit message", message);
3821
+ } else if (action === "Regenerate") {
3822
+ const spinner = createSpinner("Regenerating commit message...");
3823
+ const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
3824
+ const regen = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
3825
+ if (regen) {
3826
+ message = regen;
3827
+ spinner.success("Commit message regenerated.");
3828
+ console.log(`
3829
+ ${pc14.dim("AI suggestion:")} ${pc14.bold(pc14.cyan(regen))}`);
3830
+ } else {
3831
+ spinner.fail("Regeneration failed.");
3832
+ finalMsg = await inputPrompt("Enter commit message");
3833
+ }
3834
+ } else {
3835
+ finalMsg = await inputPrompt("Enter commit message");
3836
+ }
3837
+ }
3330
3838
  } else {
3331
- finalMsg = await inputPrompt("Commit message", fallback);
3839
+ finalMsg = await inputPrompt("Commit message", `squash merge ${featureBranch}`);
3332
3840
  }
3333
3841
  const commitResult = await commitWithMessage(finalMsg);
3334
3842
  if (commitResult.exitCode !== 0) {
3335
3843
  error(`Commit failed: ${commitResult.stderr}`);
3336
3844
  process.exit(1);
3337
3845
  }
3338
- info(`Pushing ${pc13.bold(baseBranch)} to ${origin}...`);
3846
+ info(`Pushing ${pc14.bold(baseBranch)} to ${origin}...`);
3339
3847
  const pushResult = await pushBranch(origin, baseBranch);
3340
3848
  if (pushResult.exitCode !== 0) {
3341
3849
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
3342
3850
  process.exit(1);
3343
3851
  }
3344
- info(`Deleting local branch ${pc13.bold(featureBranch)}...`);
3852
+ info(`Deleting local branch ${pc14.bold(featureBranch)}...`);
3345
3853
  const delLocal = await forceDeleteBranch(featureBranch);
3346
3854
  if (delLocal.exitCode !== 0) {
3347
3855
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
@@ -3349,16 +3857,16 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
3349
3857
  const remoteBranchRef = `${origin}/${featureBranch}`;
3350
3858
  const remoteExists = await branchExists(remoteBranchRef);
3351
3859
  if (remoteExists) {
3352
- info(`Deleting remote branch ${pc13.bold(featureBranch)}...`);
3860
+ info(`Deleting remote branch ${pc14.bold(featureBranch)}...`);
3353
3861
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
3354
3862
  if (delRemote.exitCode !== 0) {
3355
3863
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
3356
3864
  }
3357
3865
  }
3358
- success(`✅ Squash merged ${pc13.bold(featureBranch)} into ${pc13.bold(baseBranch)} and pushed.`);
3359
- info(`Run ${pc13.bold("contrib start")} to begin a new feature.`);
3866
+ success(`✅ Squash merged ${pc14.bold(featureBranch)} into ${pc14.bold(baseBranch)} and pushed.`);
3867
+ info(`Run ${pc14.bold("contrib start")} to begin a new feature.`);
3360
3868
  }
3361
- var submit_default = defineCommand10({
3869
+ var submit_default = defineCommand11({
3362
3870
  meta: {
3363
3871
  name: "submit",
3364
3872
  description: "Push current branch and create a pull request"
@@ -3400,7 +3908,7 @@ var submit_default = defineCommand10({
3400
3908
  }
3401
3909
  if (protectedBranches.includes(currentBranch)) {
3402
3910
  heading("\uD83D\uDE80 contrib submit");
3403
- warn(`You're on ${pc13.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
3911
+ warn(`You're on ${pc14.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
3404
3912
  await fetchAll();
3405
3913
  const remoteRef = `${origin}/${currentBranch}`;
3406
3914
  const localWork = await hasLocalWork(origin, currentBranch);
@@ -3409,11 +3917,11 @@ var submit_default = defineCommand10({
3409
3917
  const hasAnything = hasCommits || dirty;
3410
3918
  if (!hasAnything) {
3411
3919
  error("No local changes or commits to move. Switch to a feature branch first.");
3412
- info(` Run ${pc13.bold("contrib start")} to create a new feature branch.`);
3920
+ info(` Run ${pc14.bold("contrib start")} to create a new feature branch.`);
3413
3921
  process.exit(1);
3414
3922
  }
3415
3923
  if (hasCommits) {
3416
- info(`Found ${pc13.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${pc13.bold(currentBranch)}.`);
3924
+ info(`Found ${pc14.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${pc14.bold(currentBranch)}.`);
3417
3925
  }
3418
3926
  if (dirty) {
3419
3927
  info("You also have uncommitted changes in the working tree.");
@@ -3429,7 +3937,7 @@ var submit_default = defineCommand10({
3429
3937
  info("No changes made. You are still on your current branch.");
3430
3938
  return;
3431
3939
  }
3432
- info(pc13.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
3940
+ info(pc14.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
3433
3941
  const description = await inputPrompt("What are you going to work on?");
3434
3942
  let newBranchName = description;
3435
3943
  if (looksLikeNaturalLanguage(description)) {
@@ -3440,8 +3948,8 @@ var submit_default = defineCommand10({
3440
3948
  if (suggested) {
3441
3949
  spinner.success("Branch name suggestion ready.");
3442
3950
  console.log(`
3443
- ${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(suggested))}`);
3444
- const accepted = await confirmPrompt(`Use ${pc13.bold(suggested)} as your branch name?`);
3951
+ ${pc14.dim("AI suggestion:")} ${pc14.bold(pc14.cyan(suggested))}`);
3952
+ const accepted = await confirmPrompt(`Use ${pc14.bold(suggested)} as your branch name?`);
3445
3953
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
3446
3954
  } else {
3447
3955
  spinner.fail("AI did not return a suggestion.");
@@ -3450,7 +3958,7 @@ var submit_default = defineCommand10({
3450
3958
  }
3451
3959
  }
3452
3960
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
3453
- const prefix = await selectPrompt(`Choose a branch type for ${pc13.bold(newBranchName)}:`, config.branchPrefixes);
3961
+ const prefix = await selectPrompt(`Choose a branch type for ${pc14.bold(newBranchName)}:`, config.branchPrefixes);
3454
3962
  newBranchName = formatBranchName(prefix, newBranchName);
3455
3963
  }
3456
3964
  if (!isValidBranchName(newBranchName)) {
@@ -3458,7 +3966,7 @@ var submit_default = defineCommand10({
3458
3966
  process.exit(1);
3459
3967
  }
3460
3968
  if (await branchExists(newBranchName)) {
3461
- error(`Branch ${pc13.bold(newBranchName)} already exists. Choose a different name.`);
3969
+ error(`Branch ${pc14.bold(newBranchName)} already exists. Choose a different name.`);
3462
3970
  process.exit(1);
3463
3971
  }
3464
3972
  const branchResult = await createBranch(newBranchName);
@@ -3466,12 +3974,12 @@ var submit_default = defineCommand10({
3466
3974
  error(`Failed to create branch: ${branchResult.stderr}`);
3467
3975
  process.exit(1);
3468
3976
  }
3469
- success(`Created ${pc13.bold(newBranchName)} with your changes.`);
3977
+ success(`Created ${pc14.bold(newBranchName)} with your changes.`);
3470
3978
  await updateLocalBranch(currentBranch, remoteRef);
3471
- info(`Reset ${pc13.bold(currentBranch)} back to ${pc13.bold(remoteRef)} — no damage done.`);
3979
+ info(`Reset ${pc14.bold(currentBranch)} back to ${pc14.bold(remoteRef)} — no damage done.`);
3472
3980
  console.log();
3473
- success(`You're now on ${pc13.bold(newBranchName)} with all your work intact.`);
3474
- info(`Run ${pc13.bold("contrib submit")} again to push and create your PR.`);
3981
+ success(`You're now on ${pc14.bold(newBranchName)} with all your work intact.`);
3982
+ info(`Run ${pc14.bold("contrib submit")} again to push and create your PR.`);
3475
3983
  return;
3476
3984
  }
3477
3985
  heading("\uD83D\uDE80 contrib submit");
@@ -3480,7 +3988,7 @@ var submit_default = defineCommand10({
3480
3988
  if (ghInstalled && ghAuthed) {
3481
3989
  const mergedPR = await getMergedPRForBranch(currentBranch);
3482
3990
  if (mergedPR) {
3483
- warn(`PR #${mergedPR.number} (${pc13.bold(mergedPR.title)}) was already merged.`);
3991
+ warn(`PR #${mergedPR.number} (${pc14.bold(mergedPR.title)}) was already merged.`);
3484
3992
  const localWork = await hasLocalWork(origin, currentBranch);
3485
3993
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
3486
3994
  if (hasWork) {
@@ -3488,7 +3996,7 @@ var submit_default = defineCommand10({
3488
3996
  warn("You have uncommitted changes in your working tree.");
3489
3997
  }
3490
3998
  if (localWork.unpushedCommits > 0) {
3491
- warn(`You have ${pc13.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
3999
+ warn(`You have ${pc14.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
3492
4000
  }
3493
4001
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
3494
4002
  const DISCARD = "Discard all changes and clean up";
@@ -3499,7 +4007,7 @@ var submit_default = defineCommand10({
3499
4007
  return;
3500
4008
  }
3501
4009
  if (action === SAVE_NEW_BRANCH) {
3502
- info(pc13.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
4010
+ info(pc14.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
3503
4011
  const description = await inputPrompt("What are you going to work on?");
3504
4012
  let newBranchName = description;
3505
4013
  if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
@@ -3508,8 +4016,8 @@ var submit_default = defineCommand10({
3508
4016
  if (suggested) {
3509
4017
  spinner.success("Branch name suggestion ready.");
3510
4018
  console.log(`
3511
- ${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(suggested))}`);
3512
- const accepted = await confirmPrompt(`Use ${pc13.bold(suggested)} as your branch name?`);
4019
+ ${pc14.dim("AI suggestion:")} ${pc14.bold(pc14.cyan(suggested))}`);
4020
+ const accepted = await confirmPrompt(`Use ${pc14.bold(suggested)} as your branch name?`);
3513
4021
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
3514
4022
  } else {
3515
4023
  spinner.fail("AI did not return a suggestion.");
@@ -3517,7 +4025,7 @@ var submit_default = defineCommand10({
3517
4025
  }
3518
4026
  }
3519
4027
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
3520
- const prefix = await selectPrompt(`Choose a branch type for ${pc13.bold(newBranchName)}:`, config.branchPrefixes);
4028
+ const prefix = await selectPrompt(`Choose a branch type for ${pc14.bold(newBranchName)}:`, config.branchPrefixes);
3521
4029
  newBranchName = formatBranchName(prefix, newBranchName);
3522
4030
  }
3523
4031
  if (!isValidBranchName(newBranchName)) {
@@ -3527,7 +4035,7 @@ var submit_default = defineCommand10({
3527
4035
  const staleUpstream = await getUpstreamRef();
3528
4036
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
3529
4037
  if (await branchExists(newBranchName)) {
3530
- error(`Branch ${pc13.bold(newBranchName)} already exists. Choose a different name.`);
4038
+ error(`Branch ${pc14.bold(newBranchName)} already exists. Choose a different name.`);
3531
4039
  process.exit(1);
3532
4040
  }
3533
4041
  const renameResult = await renameBranch(currentBranch, newBranchName);
@@ -3535,10 +4043,10 @@ var submit_default = defineCommand10({
3535
4043
  error(`Failed to rename branch: ${renameResult.stderr}`);
3536
4044
  process.exit(1);
3537
4045
  }
3538
- success(`Renamed ${pc13.bold(currentBranch)} → ${pc13.bold(newBranchName)}`);
4046
+ success(`Renamed ${pc14.bold(currentBranch)} → ${pc14.bold(newBranchName)}`);
3539
4047
  await unsetUpstream();
3540
4048
  const syncSource2 = getSyncSource(config);
3541
- info(`Syncing ${pc13.bold(newBranchName)} with latest ${pc13.bold(baseBranch)}...`);
4049
+ info(`Syncing ${pc14.bold(newBranchName)} with latest ${pc14.bold(baseBranch)}...`);
3542
4050
  await fetchRemote(syncSource2.remote);
3543
4051
  let rebaseResult;
3544
4052
  if (staleUpstreamHash) {
@@ -3549,17 +4057,17 @@ var submit_default = defineCommand10({
3549
4057
  }
3550
4058
  if (rebaseResult.exitCode !== 0) {
3551
4059
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
3552
- info(` ${pc13.bold("git rebase --continue")}`);
4060
+ info(` ${pc14.bold("git rebase --continue")}`);
3553
4061
  } else {
3554
- success(`Rebased ${pc13.bold(newBranchName)} onto ${pc13.bold(syncSource2.ref)}.`);
4062
+ success(`Rebased ${pc14.bold(newBranchName)} onto ${pc14.bold(syncSource2.ref)}.`);
3555
4063
  }
3556
- info(`All your changes are preserved. Run ${pc13.bold("contrib submit")} when ready to create a new PR.`);
4064
+ info(`All your changes are preserved. Run ${pc14.bold("contrib submit")} when ready to create a new PR.`);
3557
4065
  return;
3558
4066
  }
3559
4067
  warn("Discarding local changes...");
3560
4068
  }
3561
4069
  const syncSource = getSyncSource(config);
3562
- info(`Switching to ${pc13.bold(baseBranch)} and syncing...`);
4070
+ info(`Switching to ${pc14.bold(baseBranch)} and syncing...`);
3563
4071
  await fetchRemote(syncSource.remote);
3564
4072
  await resetHard("HEAD");
3565
4073
  const coResult = await checkoutBranch(baseBranch);
@@ -3568,16 +4076,35 @@ var submit_default = defineCommand10({
3568
4076
  process.exit(1);
3569
4077
  }
3570
4078
  await updateLocalBranch(baseBranch, syncSource.ref);
3571
- success(`Synced ${pc13.bold(baseBranch)} with ${pc13.bold(syncSource.ref)}.`);
3572
- info(`Deleting stale branch ${pc13.bold(currentBranch)}...`);
4079
+ success(`Synced ${pc14.bold(baseBranch)} with ${pc14.bold(syncSource.ref)}.`);
4080
+ info(`Deleting stale branch ${pc14.bold(currentBranch)}...`);
3573
4081
  const delResult = await forceDeleteBranch(currentBranch);
3574
4082
  if (delResult.exitCode === 0) {
3575
- success(`Deleted ${pc13.bold(currentBranch)}.`);
4083
+ success(`Deleted ${pc14.bold(currentBranch)}.`);
3576
4084
  } else {
3577
4085
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
3578
4086
  }
3579
4087
  console.log();
3580
- info(`You're now on ${pc13.bold(baseBranch)}. Run ${pc13.bold("contrib start")} to begin a new feature.`);
4088
+ info(`You're now on ${pc14.bold(baseBranch)}. Run ${pc14.bold("contrib start")} to begin a new feature.`);
4089
+ return;
4090
+ }
4091
+ }
4092
+ if (ghInstalled && ghAuthed) {
4093
+ const existingPR = await getPRForBranch(currentBranch);
4094
+ if (existingPR) {
4095
+ info(`Pushing ${pc14.bold(currentBranch)} to ${origin}...`);
4096
+ const pushResult2 = await pushSetUpstream(origin, currentBranch);
4097
+ if (pushResult2.exitCode !== 0) {
4098
+ error(`Failed to push: ${pushResult2.stderr}`);
4099
+ if (pushResult2.stderr.includes("rejected") || pushResult2.stderr.includes("non-fast-forward")) {
4100
+ warn("The remote branch has diverged. Try:");
4101
+ info(` git pull --rebase ${origin} ${currentBranch}`);
4102
+ info(" Then run `contrib submit` again.");
4103
+ }
4104
+ process.exit(1);
4105
+ }
4106
+ success(`Pushed changes to existing PR #${existingPR.number}: ${pc14.bold(existingPR.title)}`);
4107
+ console.log(` ${pc14.cyan(existingPR.url)}`);
3581
4108
  return;
3582
4109
  }
3583
4110
  }
@@ -3597,10 +4124,10 @@ var submit_default = defineCommand10({
3597
4124
  prBody = result.body;
3598
4125
  spinner.success("PR description generated.");
3599
4126
  console.log(`
3600
- ${pc13.dim("AI title:")} ${pc13.bold(pc13.cyan(prTitle))}`);
4127
+ ${pc14.dim("AI title:")} ${pc14.bold(pc14.cyan(prTitle))}`);
3601
4128
  console.log(`
3602
- ${pc13.dim("AI body preview:")}`);
3603
- console.log(pc13.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
4129
+ ${pc14.dim("AI body preview:")}`);
4130
+ console.log(pc14.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
3604
4131
  } else {
3605
4132
  spinner.fail("AI did not return a PR description.");
3606
4133
  }
@@ -3683,13 +4210,12 @@ ${pc13.dim("AI body preview:")}`);
3683
4210
  }
3684
4211
  if (submitAction === "squash") {
3685
4212
  await performSquashMerge(origin, baseBranch, currentBranch, {
3686
- defaultMsg: prTitle ?? undefined,
3687
4213
  model: args.model,
3688
4214
  convention: config.commitConvention
3689
4215
  });
3690
4216
  return;
3691
4217
  }
3692
- info(`Pushing ${pc13.bold(currentBranch)} to ${origin}...`);
4218
+ info(`Pushing ${pc14.bold(currentBranch)} to ${origin}...`);
3693
4219
  const pushResult = await pushSetUpstream(origin, currentBranch);
3694
4220
  if (pushResult.exitCode !== 0) {
3695
4221
  error(`Failed to push: ${pushResult.stderr}`);
@@ -3708,18 +4234,12 @@ ${pc13.dim("AI body preview:")}`);
3708
4234
  const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
3709
4235
  console.log();
3710
4236
  info("Create your PR manually:");
3711
- console.log(` ${pc13.cyan(prUrl)}`);
4237
+ console.log(` ${pc14.cyan(prUrl)}`);
3712
4238
  } else {
3713
4239
  info("gh CLI not available. Create your PR manually on GitHub.");
3714
4240
  }
3715
4241
  return;
3716
4242
  }
3717
- const existingPR = await getPRForBranch(currentBranch);
3718
- if (existingPR) {
3719
- success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
3720
- console.log(` ${pc13.cyan(existingPR.url)}`);
3721
- return;
3722
- }
3723
4243
  if (submitAction === "fill") {
3724
4244
  const fillResult = await createPRFill(baseBranch, args.draft);
3725
4245
  if (fillResult.exitCode !== 0) {
@@ -3747,10 +4267,109 @@ ${pc13.dim("AI body preview:")}`);
3747
4267
  }
3748
4268
  });
3749
4269
 
4270
+ // src/commands/switch.ts
4271
+ import { defineCommand as defineCommand12 } from "citty";
4272
+ import pc15 from "picocolors";
4273
+ var switch_default = defineCommand12({
4274
+ meta: {
4275
+ name: "switch",
4276
+ description: "Switch to another branch with stash protection for uncommitted changes"
4277
+ },
4278
+ args: {
4279
+ name: {
4280
+ type: "positional",
4281
+ description: "Branch name to switch to (interactive picker if omitted)",
4282
+ required: false
4283
+ }
4284
+ },
4285
+ async run({ args }) {
4286
+ if (!await isGitRepo()) {
4287
+ error("Not inside a git repository.");
4288
+ process.exit(1);
4289
+ }
4290
+ const config = readConfig();
4291
+ const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
4292
+ const currentBranch = await getCurrentBranch();
4293
+ heading("\uD83D\uDD00 contrib switch");
4294
+ let targetBranch = args.name;
4295
+ if (!targetBranch) {
4296
+ const localBranches = await getLocalBranches();
4297
+ if (localBranches.length === 0) {
4298
+ error("No local branches found.");
4299
+ process.exit(1);
4300
+ }
4301
+ const choices = localBranches.filter((b) => b.name !== currentBranch).map((b) => {
4302
+ const labels = [];
4303
+ if (protectedBranches.includes(b.name))
4304
+ labels.push(pc15.red("protected"));
4305
+ if (b.upstream)
4306
+ labels.push(pc15.dim(`→ ${b.upstream}`));
4307
+ if (b.gone)
4308
+ labels.push(pc15.red("remote gone"));
4309
+ const suffix = labels.length > 0 ? ` ${labels.join(" · ")}` : "";
4310
+ return `${b.name}${suffix}`;
4311
+ });
4312
+ if (choices.length === 0) {
4313
+ info("You are already on the only local branch.");
4314
+ process.exit(0);
4315
+ }
4316
+ const selected = await selectPrompt("Switch to which branch?", choices);
4317
+ targetBranch = selected.split(/\s{2,}/)[0].trim();
4318
+ }
4319
+ if (targetBranch === currentBranch) {
4320
+ info(`Already on ${pc15.bold(targetBranch)}.`);
4321
+ return;
4322
+ }
4323
+ if (await hasUncommittedChanges()) {
4324
+ warn("You have uncommitted changes.");
4325
+ const action = await selectPrompt("How would you like to handle them?", [
4326
+ "Save changes and switch",
4327
+ "Cancel"
4328
+ ]);
4329
+ if (action === "Cancel") {
4330
+ info("Switch cancelled.");
4331
+ return;
4332
+ }
4333
+ const { execFile } = await import("node:child_process");
4334
+ const { promisify } = await import("node:util");
4335
+ const exec = promisify(execFile);
4336
+ const stashMsg = `contrib-save: auto-save from ${currentBranch}`;
4337
+ try {
4338
+ await exec("git", ["stash", "push", "-m", stashMsg]);
4339
+ info(`Saved changes: ${pc15.dim(stashMsg)}`);
4340
+ } catch {
4341
+ error("Failed to save changes. Please commit or save manually.");
4342
+ process.exit(1);
4343
+ }
4344
+ const result2 = await checkoutBranch(targetBranch);
4345
+ if (result2.exitCode !== 0) {
4346
+ error(`Failed to switch to ${targetBranch}: ${result2.stderr}`);
4347
+ try {
4348
+ await exec("git", ["stash", "pop"]);
4349
+ info("Restored saved changes.");
4350
+ } catch {
4351
+ warn("Could not restore save automatically. Use `contrib save restore` to recover.");
4352
+ }
4353
+ process.exit(1);
4354
+ }
4355
+ success(`Switched to ${pc15.bold(targetBranch)}`);
4356
+ info(`Your changes from ${pc15.bold(currentBranch ?? "previous branch")} are saved.`);
4357
+ info(`Use ${pc15.bold("contrib save restore")} to bring them back.`);
4358
+ return;
4359
+ }
4360
+ const result = await checkoutBranch(targetBranch);
4361
+ if (result.exitCode !== 0) {
4362
+ error(`Failed to switch to ${targetBranch}: ${result.stderr}`);
4363
+ process.exit(1);
4364
+ }
4365
+ success(`Switched to ${pc15.bold(targetBranch)}`);
4366
+ }
4367
+ });
4368
+
3750
4369
  // src/commands/sync.ts
3751
- import { defineCommand as defineCommand11 } from "citty";
3752
- import pc14 from "picocolors";
3753
- var sync_default = defineCommand11({
4370
+ import { defineCommand as defineCommand13 } from "citty";
4371
+ import pc16 from "picocolors";
4372
+ var sync_default = defineCommand13({
3754
4373
  meta: {
3755
4374
  name: "sync",
3756
4375
  description: "Sync your local branches with the remote"
@@ -3801,24 +4420,24 @@ var sync_default = defineCommand11({
3801
4420
  await fetchRemote(origin);
3802
4421
  }
3803
4422
  if (!await refExists(syncSource.ref)) {
3804
- error(`Remote ref ${pc14.bold(syncSource.ref)} does not exist.`);
4423
+ error(`Remote ref ${pc16.bold(syncSource.ref)} does not exist.`);
3805
4424
  info("This can happen if the branch was renamed or deleted on the remote.");
3806
- info(`Check your config: the base branch may need updating via ${pc14.bold("contrib setup")}.`);
4425
+ info(`Check your config: the base branch may need updating via ${pc16.bold("contrib setup")}.`);
3807
4426
  process.exit(1);
3808
4427
  }
3809
4428
  let allowMergeCommit = false;
3810
4429
  const div = await getDivergence(baseBranch, syncSource.ref);
3811
4430
  if (div.ahead > 0 || div.behind > 0) {
3812
- info(`${pc14.bold(baseBranch)} is ${pc14.yellow(`${div.ahead} ahead`)} and ${pc14.red(`${div.behind} behind`)} ${syncSource.ref}`);
4431
+ info(`${pc16.bold(baseBranch)} is ${pc16.yellow(`${div.ahead} ahead`)} and ${pc16.red(`${div.behind} behind`)} ${syncSource.ref}`);
3813
4432
  } else {
3814
- info(`${pc14.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
4433
+ info(`${pc16.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
3815
4434
  }
3816
4435
  if (div.ahead > 0) {
3817
4436
  const currentBranch = await getCurrentBranch();
3818
4437
  const protectedBranches = getProtectedBranches(config);
3819
4438
  const isOnProtected = currentBranch && protectedBranches.includes(currentBranch);
3820
4439
  if (isOnProtected) {
3821
- warn(`You have ${pc14.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${pc14.bold(baseBranch)} that aren't on the remote.`);
4440
+ warn(`You have ${pc16.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${pc16.bold(baseBranch)} that aren't on the remote.`);
3822
4441
  info("Pulling now could create a merge commit, which breaks clean history.");
3823
4442
  console.log();
3824
4443
  const MOVE_BRANCH = "Move my commits to a new feature branch, then sync";
@@ -3834,7 +4453,7 @@ var sync_default = defineCommand11({
3834
4453
  return;
3835
4454
  }
3836
4455
  if (action === MOVE_BRANCH) {
3837
- info(pc14.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
4456
+ info(pc16.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
3838
4457
  const description = await inputPrompt("What are you going to work on?");
3839
4458
  let newBranchName = description;
3840
4459
  if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
@@ -3845,8 +4464,8 @@ var sync_default = defineCommand11({
3845
4464
  if (suggested) {
3846
4465
  spinner.success("Branch name suggestion ready.");
3847
4466
  console.log(`
3848
- ${pc14.dim("AI suggestion:")} ${pc14.bold(pc14.cyan(suggested))}`);
3849
- const accepted = await confirmPrompt(`Use ${pc14.bold(suggested)} as your branch name?`);
4467
+ ${pc16.dim("AI suggestion:")} ${pc16.bold(pc16.cyan(suggested))}`);
4468
+ const accepted = await confirmPrompt(`Use ${pc16.bold(suggested)} as your branch name?`);
3850
4469
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
3851
4470
  } else {
3852
4471
  spinner.fail("AI did not return a suggestion.");
@@ -3855,7 +4474,7 @@ var sync_default = defineCommand11({
3855
4474
  }
3856
4475
  }
3857
4476
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
3858
- const prefix = await selectPrompt(`Choose a branch type for ${pc14.bold(newBranchName)}:`, config.branchPrefixes);
4477
+ const prefix = await selectPrompt(`Choose a branch type for ${pc16.bold(newBranchName)}:`, config.branchPrefixes);
3859
4478
  newBranchName = formatBranchName(prefix, newBranchName);
3860
4479
  }
3861
4480
  if (!isValidBranchName(newBranchName)) {
@@ -3863,7 +4482,7 @@ var sync_default = defineCommand11({
3863
4482
  process.exit(1);
3864
4483
  }
3865
4484
  if (await branchExists(newBranchName)) {
3866
- error(`Branch ${pc14.bold(newBranchName)} already exists. Choose a different name.`);
4485
+ error(`Branch ${pc16.bold(newBranchName)} already exists. Choose a different name.`);
3867
4486
  process.exit(1);
3868
4487
  }
3869
4488
  const branchResult = await createBranch(newBranchName);
@@ -3871,7 +4490,7 @@ var sync_default = defineCommand11({
3871
4490
  error(`Failed to create branch: ${branchResult.stderr}`);
3872
4491
  process.exit(1);
3873
4492
  }
3874
- success(`Created ${pc14.bold(newBranchName)} with your commits.`);
4493
+ success(`Created ${pc16.bold(newBranchName)} with your commits.`);
3875
4494
  const coResult2 = await checkoutBranch(baseBranch);
3876
4495
  if (coResult2.exitCode !== 0) {
3877
4496
  error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
@@ -3879,11 +4498,11 @@ var sync_default = defineCommand11({
3879
4498
  }
3880
4499
  const remoteRef = syncSource.ref;
3881
4500
  await updateLocalBranch(baseBranch, remoteRef);
3882
- success(`Reset ${pc14.bold(baseBranch)} to ${pc14.bold(remoteRef)}.`);
3883
- success(`✅ ${pc14.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
4501
+ success(`Reset ${pc16.bold(baseBranch)} to ${pc16.bold(remoteRef)}.`);
4502
+ success(`✅ ${pc16.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
3884
4503
  console.log();
3885
- info(`Your commits are safe on ${pc14.bold(newBranchName)}.`);
3886
- info(`Run ${pc14.bold(`git checkout ${newBranchName}`)} then ${pc14.bold("contrib update")} to rebase onto the synced ${pc14.bold(baseBranch)}.`);
4504
+ info(`Your commits are safe on ${pc16.bold(newBranchName)}.`);
4505
+ info(`Run ${pc16.bold(`git checkout ${newBranchName}`)} then ${pc16.bold("contrib update")} to rebase onto the synced ${pc16.bold(baseBranch)}.`);
3887
4506
  return;
3888
4507
  }
3889
4508
  allowMergeCommit = true;
@@ -3891,7 +4510,7 @@ var sync_default = defineCommand11({
3891
4510
  }
3892
4511
  }
3893
4512
  if (!args.yes) {
3894
- const ok = await confirmPrompt(`This will pull ${pc14.bold(syncSource.ref)} into local ${pc14.bold(baseBranch)}.`);
4513
+ const ok = await confirmPrompt(`This will pull ${pc16.bold(syncSource.ref)} into local ${pc16.bold(baseBranch)}.`);
3895
4514
  if (!ok)
3896
4515
  process.exit(0);
3897
4516
  }
@@ -3905,8 +4524,8 @@ var sync_default = defineCommand11({
3905
4524
  if (allowMergeCommit) {
3906
4525
  error(`Pull failed: ${pullResult.stderr.trim()}`);
3907
4526
  } else {
3908
- error(`Fast-forward pull failed. Your local ${pc14.bold(baseBranch)} may have diverged.`);
3909
- info(`Use ${pc14.bold("contrib sync")} again and choose "Move my commits to a new feature branch" to fix this.`);
4527
+ error(`Fast-forward pull failed. Your local ${pc16.bold(baseBranch)} may have diverged.`);
4528
+ info(`Use ${pc16.bold("contrib sync")} again and choose "Move my commits to a new feature branch" to fix this.`);
3910
4529
  }
3911
4530
  process.exit(1);
3912
4531
  }
@@ -3914,7 +4533,7 @@ var sync_default = defineCommand11({
3914
4533
  if (hasDevBranch(workflow) && role === "maintainer") {
3915
4534
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
3916
4535
  if (mainDiv.behind > 0) {
3917
- info(`Also syncing ${pc14.bold(config.mainBranch)}...`);
4536
+ info(`Also syncing ${pc16.bold(config.mainBranch)}...`);
3918
4537
  const mainCoResult = await checkoutBranch(config.mainBranch);
3919
4538
  if (mainCoResult.exitCode === 0) {
3920
4539
  const mainPullResult = await pullFastForwardOnly(origin, config.mainBranch);
@@ -3930,9 +4549,9 @@ var sync_default = defineCommand11({
3930
4549
 
3931
4550
  // src/commands/update.ts
3932
4551
  import { readFileSync as readFileSync4 } from "node:fs";
3933
- import { defineCommand as defineCommand12 } from "citty";
3934
- import pc15 from "picocolors";
3935
- var update_default = defineCommand12({
4552
+ import { defineCommand as defineCommand14 } from "citty";
4553
+ import pc17 from "picocolors";
4554
+ var update_default = defineCommand14({
3936
4555
  meta: {
3937
4556
  name: "update",
3938
4557
  description: "Rebase current branch onto the latest base branch"
@@ -3969,7 +4588,7 @@ var update_default = defineCommand12({
3969
4588
  }
3970
4589
  if (protectedBranches.includes(currentBranch)) {
3971
4590
  heading("\uD83D\uDD03 contrib update");
3972
- warn(`You're on ${pc15.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
4591
+ warn(`You're on ${pc17.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
3973
4592
  await fetchAll();
3974
4593
  const { origin } = config;
3975
4594
  const remoteRef = `${origin}/${currentBranch}`;
@@ -3978,12 +4597,12 @@ var update_default = defineCommand12({
3978
4597
  const hasCommits = localWork.unpushedCommits > 0;
3979
4598
  const hasAnything = hasCommits || dirty;
3980
4599
  if (!hasAnything) {
3981
- info(`No local changes found on ${pc15.bold(currentBranch)}.`);
3982
- info(`Use ${pc15.bold("contrib sync")} to sync protected branches, or ${pc15.bold("contrib start")} to create a feature branch.`);
4600
+ info(`No local changes found on ${pc17.bold(currentBranch)}.`);
4601
+ info(`Use ${pc17.bold("contrib sync")} to sync protected branches, or ${pc17.bold("contrib start")} to create a feature branch.`);
3983
4602
  process.exit(1);
3984
4603
  }
3985
4604
  if (hasCommits) {
3986
- info(`Found ${pc15.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${pc15.bold(currentBranch)}.`);
4605
+ info(`Found ${pc17.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${pc17.bold(currentBranch)}.`);
3987
4606
  }
3988
4607
  if (dirty) {
3989
4608
  info("You also have uncommitted changes in the working tree.");
@@ -3999,7 +4618,7 @@ var update_default = defineCommand12({
3999
4618
  info("No changes made. You are still on your current branch.");
4000
4619
  return;
4001
4620
  }
4002
- info(pc15.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
4621
+ info(pc17.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
4003
4622
  const description = await inputPrompt("What are you going to work on?");
4004
4623
  let newBranchName = description;
4005
4624
  if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
@@ -4010,8 +4629,8 @@ var update_default = defineCommand12({
4010
4629
  if (suggested) {
4011
4630
  spinner.success("Branch name suggestion ready.");
4012
4631
  console.log(`
4013
- ${pc15.dim("AI suggestion:")} ${pc15.bold(pc15.cyan(suggested))}`);
4014
- const accepted = await confirmPrompt(`Use ${pc15.bold(suggested)} as your branch name?`);
4632
+ ${pc17.dim("AI suggestion:")} ${pc17.bold(pc17.cyan(suggested))}`);
4633
+ const accepted = await confirmPrompt(`Use ${pc17.bold(suggested)} as your branch name?`);
4015
4634
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
4016
4635
  } else {
4017
4636
  spinner.fail("AI did not return a suggestion.");
@@ -4020,7 +4639,7 @@ var update_default = defineCommand12({
4020
4639
  }
4021
4640
  }
4022
4641
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
4023
- const prefix = await selectPrompt(`Choose a branch type for ${pc15.bold(newBranchName)}:`, config.branchPrefixes);
4642
+ const prefix = await selectPrompt(`Choose a branch type for ${pc17.bold(newBranchName)}:`, config.branchPrefixes);
4024
4643
  newBranchName = formatBranchName(prefix, newBranchName);
4025
4644
  }
4026
4645
  if (!isValidBranchName(newBranchName)) {
@@ -4032,12 +4651,12 @@ var update_default = defineCommand12({
4032
4651
  error(`Failed to create branch: ${branchResult.stderr}`);
4033
4652
  process.exit(1);
4034
4653
  }
4035
- success(`Created ${pc15.bold(newBranchName)} with your changes.`);
4654
+ success(`Created ${pc17.bold(newBranchName)} with your changes.`);
4036
4655
  await updateLocalBranch(currentBranch, remoteRef);
4037
- info(`Reset ${pc15.bold(currentBranch)} back to ${pc15.bold(remoteRef)} — no damage done.`);
4656
+ info(`Reset ${pc17.bold(currentBranch)} back to ${pc17.bold(remoteRef)} — no damage done.`);
4038
4657
  console.log();
4039
- success(`You're now on ${pc15.bold(newBranchName)} with all your work intact.`);
4040
- info(`Run ${pc15.bold("contrib update")} again to rebase onto latest ${pc15.bold(baseBranch)}.`);
4658
+ success(`You're now on ${pc17.bold(newBranchName)} with all your work intact.`);
4659
+ info(`Run ${pc17.bold("contrib update")} again to rebase onto latest ${pc17.bold(baseBranch)}.`);
4041
4660
  return;
4042
4661
  }
4043
4662
  if (await hasUncommittedChanges()) {
@@ -4047,8 +4666,8 @@ var update_default = defineCommand12({
4047
4666
  heading("\uD83D\uDD03 contrib update");
4048
4667
  const mergedPR = await getMergedPRForBranch(currentBranch);
4049
4668
  if (mergedPR) {
4050
- warn(`PR #${mergedPR.number} (${pc15.bold(mergedPR.title)}) has already been merged.`);
4051
- info(`Link: ${pc15.underline(mergedPR.url)}`);
4669
+ warn(`PR #${mergedPR.number} (${pc17.bold(mergedPR.title)}) has already been merged.`);
4670
+ info(`Link: ${pc17.underline(mergedPR.url)}`);
4052
4671
  const localWork = await hasLocalWork(syncSource.remote, currentBranch);
4053
4672
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
4054
4673
  if (hasWork) {
@@ -4061,13 +4680,13 @@ var update_default = defineCommand12({
4061
4680
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
4062
4681
  const DISCARD = "Discard all changes and clean up";
4063
4682
  const CANCEL = "Cancel";
4064
- const action = await selectPrompt(`${pc15.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
4683
+ const action = await selectPrompt(`${pc17.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
4065
4684
  if (action === CANCEL) {
4066
4685
  info("No changes made. You are still on your current branch.");
4067
4686
  return;
4068
4687
  }
4069
4688
  if (action === SAVE_NEW_BRANCH) {
4070
- info(pc15.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
4689
+ info(pc17.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
4071
4690
  const description = await inputPrompt("What are you going to work on?");
4072
4691
  let newBranchName = description;
4073
4692
  if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
@@ -4076,8 +4695,8 @@ var update_default = defineCommand12({
4076
4695
  if (suggested) {
4077
4696
  spinner.success("Branch name suggestion ready.");
4078
4697
  console.log(`
4079
- ${pc15.dim("AI suggestion:")} ${pc15.bold(pc15.cyan(suggested))}`);
4080
- const accepted = await confirmPrompt(`Use ${pc15.bold(suggested)} as your branch name?`);
4698
+ ${pc17.dim("AI suggestion:")} ${pc17.bold(pc17.cyan(suggested))}`);
4699
+ const accepted = await confirmPrompt(`Use ${pc17.bold(suggested)} as your branch name?`);
4081
4700
  newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
4082
4701
  } else {
4083
4702
  spinner.fail("AI did not return a suggestion.");
@@ -4085,7 +4704,7 @@ var update_default = defineCommand12({
4085
4704
  }
4086
4705
  }
4087
4706
  if (!hasPrefix(newBranchName, config.branchPrefixes)) {
4088
- const prefix = await selectPrompt(`Choose a branch type for ${pc15.bold(newBranchName)}:`, config.branchPrefixes);
4707
+ const prefix = await selectPrompt(`Choose a branch type for ${pc17.bold(newBranchName)}:`, config.branchPrefixes);
4089
4708
  newBranchName = formatBranchName(prefix, newBranchName);
4090
4709
  }
4091
4710
  if (!isValidBranchName(newBranchName)) {
@@ -4095,7 +4714,7 @@ var update_default = defineCommand12({
4095
4714
  const staleUpstream = await getUpstreamRef();
4096
4715
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
4097
4716
  if (await branchExists(newBranchName)) {
4098
- error(`Branch ${pc15.bold(newBranchName)} already exists. Choose a different name.`);
4717
+ error(`Branch ${pc17.bold(newBranchName)} already exists. Choose a different name.`);
4099
4718
  process.exit(1);
4100
4719
  }
4101
4720
  const renameResult = await renameBranch(currentBranch, newBranchName);
@@ -4103,7 +4722,7 @@ var update_default = defineCommand12({
4103
4722
  error(`Failed to rename branch: ${renameResult.stderr}`);
4104
4723
  process.exit(1);
4105
4724
  }
4106
- success(`Renamed ${pc15.bold(currentBranch)} → ${pc15.bold(newBranchName)}`);
4725
+ success(`Renamed ${pc17.bold(currentBranch)} → ${pc17.bold(newBranchName)}`);
4107
4726
  await unsetUpstream();
4108
4727
  await fetchRemote(syncSource.remote);
4109
4728
  let rebaseResult2;
@@ -4115,11 +4734,11 @@ var update_default = defineCommand12({
4115
4734
  }
4116
4735
  if (rebaseResult2.exitCode !== 0) {
4117
4736
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
4118
- info(` ${pc15.bold("git rebase --continue")}`);
4737
+ info(` ${pc17.bold("git rebase --continue")}`);
4119
4738
  } else {
4120
- success(`Rebased ${pc15.bold(newBranchName)} onto ${pc15.bold(syncSource.ref)}.`);
4739
+ success(`Rebased ${pc17.bold(newBranchName)} onto ${pc17.bold(syncSource.ref)}.`);
4121
4740
  }
4122
- info(`All your changes are preserved. Run ${pc15.bold("contrib submit")} when ready to create a new PR.`);
4741
+ info(`All your changes are preserved. Run ${pc17.bold("contrib submit")} when ready to create a new PR.`);
4123
4742
  return;
4124
4743
  }
4125
4744
  warn("Discarding local changes...");
@@ -4132,24 +4751,24 @@ var update_default = defineCommand12({
4132
4751
  process.exit(1);
4133
4752
  }
4134
4753
  await updateLocalBranch(baseBranch, syncSource.ref);
4135
- success(`Synced ${pc15.bold(baseBranch)} with ${pc15.bold(syncSource.ref)}.`);
4136
- info(`Deleting stale branch ${pc15.bold(currentBranch)}...`);
4754
+ success(`Synced ${pc17.bold(baseBranch)} with ${pc17.bold(syncSource.ref)}.`);
4755
+ info(`Deleting stale branch ${pc17.bold(currentBranch)}...`);
4137
4756
  await forceDeleteBranch(currentBranch);
4138
- success(`Deleted ${pc15.bold(currentBranch)}.`);
4139
- info(`Run ${pc15.bold("contrib start")} to begin a new feature branch.`);
4757
+ success(`Deleted ${pc17.bold(currentBranch)}.`);
4758
+ info(`Run ${pc17.bold("contrib start")} to begin a new feature branch.`);
4140
4759
  return;
4141
4760
  }
4142
- info(`Updating ${pc15.bold(currentBranch)} with latest ${pc15.bold(baseBranch)}...`);
4761
+ info(`Updating ${pc17.bold(currentBranch)} with latest ${pc17.bold(baseBranch)}...`);
4143
4762
  await fetchRemote(syncSource.remote);
4144
4763
  if (!await refExists(syncSource.ref)) {
4145
- error(`Remote ref ${pc15.bold(syncSource.ref)} does not exist.`);
4764
+ error(`Remote ref ${pc17.bold(syncSource.ref)} does not exist.`);
4146
4765
  error("Run `git fetch --all` and verify your remote configuration.");
4147
4766
  process.exit(1);
4148
4767
  }
4149
4768
  await updateLocalBranch(baseBranch, syncSource.ref);
4150
4769
  const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
4151
4770
  if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
4152
- info(pc15.dim(`Using --onto rebase (branch was based on a different ref)`));
4771
+ info(pc17.dim(`Using --onto rebase (branch was based on a different ref)`));
4153
4772
  }
4154
4773
  const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
4155
4774
  if (rebaseResult.exitCode !== 0) {
@@ -4178,10 +4797,10 @@ ${content.slice(0, 2000)}
4178
4797
  if (suggestion) {
4179
4798
  spinner.success("AI conflict guidance ready.");
4180
4799
  console.log(`
4181
- ${pc15.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
4182
- console.log(pc15.dim("─".repeat(60)));
4800
+ ${pc17.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
4801
+ console.log(pc17.dim("─".repeat(60)));
4183
4802
  console.log(suggestion);
4184
- console.log(pc15.dim("─".repeat(60)));
4803
+ console.log(pc17.dim("─".repeat(60)));
4185
4804
  console.log();
4186
4805
  } else {
4187
4806
  spinner.fail("AI could not analyze the conflicts.");
@@ -4189,22 +4808,22 @@ ${pc15.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
4189
4808
  }
4190
4809
  }
4191
4810
  }
4192
- console.log(pc15.bold("To resolve:"));
4811
+ console.log(pc17.bold("To resolve:"));
4193
4812
  console.log(` 1. Fix conflicts in the affected files`);
4194
- console.log(` 2. ${pc15.cyan("git add <resolved-files>")}`);
4195
- console.log(` 3. ${pc15.cyan("git rebase --continue")}`);
4813
+ console.log(` 2. ${pc17.cyan("git add <resolved-files>")}`);
4814
+ console.log(` 3. ${pc17.cyan("git rebase --continue")}`);
4196
4815
  console.log();
4197
- console.log(` Or abort: ${pc15.cyan("git rebase --abort")}`);
4816
+ console.log(` Or abort: ${pc17.cyan("git rebase --abort")}`);
4198
4817
  process.exit(1);
4199
4818
  }
4200
- success(`✅ ${pc15.bold(currentBranch)} has been rebased onto latest ${pc15.bold(baseBranch)}`);
4819
+ success(`✅ ${pc17.bold(currentBranch)} has been rebased onto latest ${pc17.bold(baseBranch)}`);
4201
4820
  }
4202
4821
  });
4203
4822
 
4204
4823
  // src/commands/validate.ts
4205
- import { defineCommand as defineCommand13 } from "citty";
4206
- import pc16 from "picocolors";
4207
- var validate_default = defineCommand13({
4824
+ import { defineCommand as defineCommand15 } from "citty";
4825
+ import pc18 from "picocolors";
4826
+ var validate_default = defineCommand15({
4208
4827
  meta: {
4209
4828
  name: "validate",
4210
4829
  description: "Validate a commit message against the configured convention"
@@ -4234,7 +4853,7 @@ var validate_default = defineCommand13({
4234
4853
  }
4235
4854
  const errors = getValidationError(convention);
4236
4855
  for (const line of errors) {
4237
- console.error(pc16.red(` ✗ ${line}`));
4856
+ console.error(pc18.red(` ✗ ${line}`));
4238
4857
  }
4239
4858
  process.exit(1);
4240
4859
  }
@@ -4242,7 +4861,7 @@ var validate_default = defineCommand13({
4242
4861
 
4243
4862
  // src/ui/banner.ts
4244
4863
  import figlet from "figlet";
4245
- import pc17 from "picocolors";
4864
+ import pc19 from "picocolors";
4246
4865
  var LOGO_BIG;
4247
4866
  try {
4248
4867
  LOGO_BIG = figlet.textSync(`Contribute
@@ -4264,14 +4883,14 @@ function getAuthor() {
4264
4883
  }
4265
4884
  function showBanner(variant = "small") {
4266
4885
  const logo = variant === "big" ? LOGO_BIG : LOGO_SMALL;
4267
- console.log(pc17.cyan(`
4886
+ console.log(pc19.cyan(`
4268
4887
  ${logo}`));
4269
- console.log(` ${pc17.dim(`v${getVersion()}`)} ${pc17.dim("—")} ${pc17.dim(`Built by ${getAuthor()}`)}`);
4888
+ console.log(` ${pc19.dim(`v${getVersion()}`)} ${pc19.dim("—")} ${pc19.dim(`Built by ${getAuthor()}`)}`);
4270
4889
  if (variant === "big") {
4271
4890
  console.log();
4272
- console.log(` ${pc17.yellow("Star")} ${pc17.cyan("https://github.com/warengonzaga/contribute-now")}`);
4273
- console.log(` ${pc17.green("Contribute")} ${pc17.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
4274
- console.log(` ${pc17.magenta("Sponsor")} ${pc17.cyan("https://warengonzaga.com/sponsor")}`);
4891
+ console.log(` ${pc19.yellow("Star")} ${pc19.cyan("https://github.com/warengonzaga/contribute-now")}`);
4892
+ console.log(` ${pc19.green("Contribute")} ${pc19.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
4893
+ console.log(` ${pc19.magenta("Sponsor")} ${pc19.cyan("https://warengonzaga.com/sponsor")}`);
4275
4894
  }
4276
4895
  console.log();
4277
4896
  }
@@ -4286,6 +4905,8 @@ if (!isVersion) {
4286
4905
  "commit",
4287
4906
  "update",
4288
4907
  "submit",
4908
+ "switch",
4909
+ "save",
4289
4910
  "clean",
4290
4911
  "status",
4291
4912
  "log",
@@ -4299,7 +4920,7 @@ if (!isVersion) {
4299
4920
  const useBigBanner = isHelp || !hasSubCommand;
4300
4921
  showBanner(useBigBanner ? "big" : "small");
4301
4922
  }
4302
- var main = defineCommand14({
4923
+ var main = defineCommand16({
4303
4924
  meta: {
4304
4925
  name: "contrib",
4305
4926
  version: getVersion(),
@@ -4319,6 +4940,8 @@ var main = defineCommand14({
4319
4940
  commit: commit_default,
4320
4941
  update: update_default,
4321
4942
  submit: submit_default,
4943
+ switch: switch_default,
4944
+ save: save_default,
4322
4945
  branch: branch_default,
4323
4946
  clean: clean_default,
4324
4947
  status: status_default,