contribute-now 0.4.1 → 0.5.0-dev.9ad3d01

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 +414 -46
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -204,17 +204,19 @@ Checks include:
204
204
 
205
205
  ### `contrib log`
206
206
 
207
- Show a colorized, workflow-aware commit log with graph visualization.
207
+ Show a colorized, workflow-aware commit log. By default it shows only **local unpushed commits** — the changes you've made since the last push (or since branching off the base branch). Use flags to switch between different views.
208
208
 
209
209
  ```bash
210
- contrib log # last 20 commits with graph
211
- contrib log -n 50 # last 50 commits
212
- contrib log --all # all branches
213
- contrib log --no-graph # flat view without graph lines
210
+ contrib log # local unpushed commits (default)
211
+ contrib log --remote # commits on remote not yet pulled
212
+ contrib log --full # full history for the current branch
213
+ contrib log --all # commits across all branches
214
+ contrib log -n 50 # change the commit limit (default: 20)
214
215
  contrib log -b feature/x # log for a specific branch
216
+ contrib log --no-graph # flat view without graph lines
215
217
  ```
216
218
 
217
- Protected branches (main, dev) are highlighted, and the current branch is color-coded for quick orientation.
219
+ When no upstream tracking is set (branch hasn't been pushed yet), the command automatically compares against the base branch from your config (e.g., `origin/dev`). Protected branches are highlighted, and the current branch is color-coded for quick orientation.
218
220
 
219
221
  ---
220
222
 
@@ -352,15 +354,40 @@ bun test # run tests
352
354
  bun run lint # check code quality
353
355
  ```
354
356
 
355
- ## Contributing
357
+ ## 🎯 Contributing
356
358
 
357
- Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow, commit convention, and PR guidelines.
359
+ Contributions are welcome, create a pull request to this repo and I will review your code. Please consider to submit your pull request to the `dev` branch. Thank you!
358
360
 
359
- ## License
361
+ Read the project's [contributing guide](./CONTRIBUTING.md) for more info.
360
362
 
361
- [GPL-3.0](LICENSE) © [Waren Gonzaga](https://warengonzaga.com)
363
+ ## 🐛 Issues
362
364
 
363
- ---
365
+ Please report any issues and bugs by [creating a new issue here](https://github.com/warengonzaga/contribute-now/issues/new/choose), also make sure you're reporting an issue that doesn't exist. Any help to improve the project would be appreciated. Thanks! 🙏✨
366
+
367
+ ## 🙏 Sponsor
368
+
369
+ Like this project? **Leave a star**! ⭐⭐⭐⭐⭐
370
+
371
+ Want to support my work and get some perks? [Become a sponsor](https://github.com/sponsors/warengonzaga)! 💖
372
+
373
+ Or, you just love what I do? [Buy me a coffee](https://buymeacoffee.com/warengonzaga)! ☕
374
+
375
+ Recognized my open-source contributions? [Nominate me](https://stars.github.com/nominate) as GitHub Star! 💫
364
376
 
365
- 💻💖☕ Made with ❤️ by [Waren Gonzaga](https://github.com/warengonzaga)
377
+ ## 📋 Code of Conduct
378
+
379
+ Read the project's [code of conduct](./CODE_OF_CONDUCT.md).
380
+
381
+ ## 📃 License
382
+
383
+ This project is licensed under [GNU General Public License v3.0](https://opensource.org/licenses/GPL-3.0).
384
+
385
+ ## 📝 Author
386
+
387
+ This project is created by **[Waren Gonzaga](https://github.com/warengonzaga)**, with the help of awesome [contributors](https://github.com/warengonzaga/contribute-now/graphs/contributors).
388
+
389
+ [![contributors](https://contrib.rocks/image?repo=warengonzaga/contribute-now)](https://github.com/warengonzaga/contribute-now/graphs/contributors)
390
+
391
+ ---
366
392
 
393
+ 💻💖☕ by [Waren Gonzaga](https://warengonzaga.com) & [YHWH](https://www.youtube.com/watch?v=VOZbswniA-g) 🙏 — Without *Him*, none of this exists, *even me*.
package/dist/index.js CHANGED
@@ -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.9ad3d01",
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) {
2716
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.`));
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) {
@@ -3221,10 +3492,20 @@ var status_default = defineCommand9({
3221
3492
  const devLine = formatStatus(config.devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
3222
3493
  console.log(devLine);
3223
3494
  }
3224
- if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
3495
+ const protectedBranches = getProtectedBranches(config);
3496
+ const isFeatureBranch = currentBranch && !protectedBranches.includes(currentBranch);
3497
+ let branchStatus = null;
3498
+ if (isFeatureBranch) {
3225
3499
  const branchDiv = await getDivergence(currentBranch, baseBranch);
3226
3500
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
3227
3501
  console.log(branchLine + pc12.dim(` (current ${pc12.green("*")})`));
3502
+ branchStatus = await detectBranchStatus(currentBranch, baseBranch);
3503
+ if (branchStatus.merged) {
3504
+ console.log(` ${pc12.green("✓")} ${pc12.green("Branch merged")} — ${pc12.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
3505
+ }
3506
+ if (branchStatus.stale) {
3507
+ console.log(` ${pc12.yellow("⏳")} ${pc12.yellow("Branch is stale")} — ${pc12.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
3508
+ }
3228
3509
  } else if (currentBranch) {
3229
3510
  console.log(pc12.dim(` (on ${pc12.bold(currentBranch)} branch)`));
3230
3511
  }
@@ -3259,10 +3540,16 @@ var status_default = defineCommand9({
3259
3540
  if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
3260
3541
  tips.push(`Run ${pc12.bold("contrib commit")} to stage and commit changes`);
3261
3542
  }
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`);
3543
+ if (isFeatureBranch && branchStatus) {
3544
+ if (branchStatus.merged) {
3545
+ tips.push(`Run ${pc12.bold("contrib clean")} to delete this merged branch`);
3546
+ } else if (branchStatus.stale) {
3547
+ tips.push(`Run ${pc12.bold("contrib sync")} to rebase on latest changes, or ${pc12.bold("contrib clean")} if no longer needed`);
3548
+ } else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
3549
+ const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
3550
+ if (branchDiv.ahead > 0) {
3551
+ tips.push(`Run ${pc12.bold("contrib submit")} to push and create/update your PR`);
3552
+ }
3266
3553
  }
3267
3554
  }
3268
3555
  if (tips.length > 0) {
@@ -3288,6 +3575,48 @@ function formatStatus(branch, base, ahead, behind) {
3288
3575
  }
3289
3576
  return ` ${pc12.red("⚡")} ${label} ${pc12.yellow(`${ahead} ahead`)}${pc12.dim(", ")}${pc12.red(`${behind} behind`)} ${pc12.dim(base)}`;
3290
3577
  }
3578
+ var STALE_THRESHOLD_DAYS = 14;
3579
+ async function detectBranchStatus(branch, baseBranch) {
3580
+ const result = { merged: false, mergedReason: null, stale: false, staleDaysAgo: null };
3581
+ const div = await getDivergence(branch, baseBranch);
3582
+ const hasWork = div.ahead > 0;
3583
+ if (hasWork) {
3584
+ if (await isBranchMergedInto(branch, baseBranch)) {
3585
+ result.merged = true;
3586
+ result.mergedReason = `all commits reachable from ${baseBranch}`;
3587
+ return result;
3588
+ }
3589
+ const mergedBranches = await getMergedBranches(baseBranch);
3590
+ if (mergedBranches.includes(branch)) {
3591
+ result.merged = true;
3592
+ result.mergedReason = `listed in merged branches of ${baseBranch}`;
3593
+ return result;
3594
+ }
3595
+ }
3596
+ const goneBranches = await getGoneBranches();
3597
+ if (goneBranches.includes(branch)) {
3598
+ result.merged = true;
3599
+ result.mergedReason = "remote branch deleted (likely squash-merged)";
3600
+ return result;
3601
+ }
3602
+ if (await checkGhInstalled()) {
3603
+ const mergedPR = await getMergedPRForBranch(branch);
3604
+ if (mergedPR) {
3605
+ result.merged = true;
3606
+ result.mergedReason = `PR #${mergedPR.number} was merged`;
3607
+ return result;
3608
+ }
3609
+ }
3610
+ const lastDate = await getLastCommitDate(branch);
3611
+ if (lastDate) {
3612
+ const daysAgo = Math.floor((Date.now() - new Date(lastDate).getTime()) / (1000 * 60 * 60 * 24));
3613
+ if (daysAgo >= STALE_THRESHOLD_DAYS) {
3614
+ result.stale = true;
3615
+ result.staleDaysAgo = daysAgo;
3616
+ }
3617
+ }
3618
+ return result;
3619
+ }
3291
3620
 
3292
3621
  // src/commands/submit.ts
3293
3622
  import { defineCommand as defineCommand10 } from "citty";
@@ -3311,10 +3640,12 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
3311
3640
  if (!copilotError) {
3312
3641
  const spinner = createSpinner("Generating AI commit message for squash merge...");
3313
3642
  const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
3314
- const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit");
3643
+ const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
3315
3644
  if (aiMsg) {
3316
3645
  message = aiMsg;
3317
3646
  spinner.success("AI commit message generated.");
3647
+ console.log(`
3648
+ ${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(message))}`);
3318
3649
  } else {
3319
3650
  spinner.fail("AI did not return a commit message.");
3320
3651
  }
@@ -3322,13 +3653,38 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
3322
3653
  warn(`AI unavailable: ${copilotError}`);
3323
3654
  }
3324
3655
  }
3325
- const fallback = message || `squash merge ${featureBranch}`;
3326
- let finalMsg;
3656
+ let finalMsg = null;
3327
3657
  if (message) {
3328
- console.log(` ${pc13.dim("Commit message:")} ${pc13.bold(message)}`);
3329
- finalMsg = message;
3658
+ while (!finalMsg) {
3659
+ const action = await selectPrompt("What would you like to do?", [
3660
+ "Accept this message",
3661
+ "Edit this message",
3662
+ "Regenerate",
3663
+ "Write manually"
3664
+ ]);
3665
+ if (action === "Accept this message") {
3666
+ finalMsg = message;
3667
+ } else if (action === "Edit this message") {
3668
+ finalMsg = await inputPrompt("Edit commit message", message);
3669
+ } else if (action === "Regenerate") {
3670
+ const spinner = createSpinner("Regenerating commit message...");
3671
+ const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
3672
+ const regen = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
3673
+ if (regen) {
3674
+ message = regen;
3675
+ spinner.success("Commit message regenerated.");
3676
+ console.log(`
3677
+ ${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(regen))}`);
3678
+ } else {
3679
+ spinner.fail("Regeneration failed.");
3680
+ finalMsg = await inputPrompt("Enter commit message");
3681
+ }
3682
+ } else {
3683
+ finalMsg = await inputPrompt("Enter commit message");
3684
+ }
3685
+ }
3330
3686
  } else {
3331
- finalMsg = await inputPrompt("Commit message", fallback);
3687
+ finalMsg = await inputPrompt("Commit message", `squash merge ${featureBranch}`);
3332
3688
  }
3333
3689
  const commitResult = await commitWithMessage(finalMsg);
3334
3690
  if (commitResult.exitCode !== 0) {
@@ -3581,6 +3937,25 @@ var submit_default = defineCommand10({
3581
3937
  return;
3582
3938
  }
3583
3939
  }
3940
+ if (ghInstalled && ghAuthed) {
3941
+ const existingPR = await getPRForBranch(currentBranch);
3942
+ if (existingPR) {
3943
+ info(`Pushing ${pc13.bold(currentBranch)} to ${origin}...`);
3944
+ const pushResult2 = await pushSetUpstream(origin, currentBranch);
3945
+ if (pushResult2.exitCode !== 0) {
3946
+ error(`Failed to push: ${pushResult2.stderr}`);
3947
+ if (pushResult2.stderr.includes("rejected") || pushResult2.stderr.includes("non-fast-forward")) {
3948
+ warn("The remote branch has diverged. Try:");
3949
+ info(` git pull --rebase ${origin} ${currentBranch}`);
3950
+ info(" Then run `contrib submit` again.");
3951
+ }
3952
+ process.exit(1);
3953
+ }
3954
+ success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
3955
+ console.log(` ${pc13.cyan(existingPR.url)}`);
3956
+ return;
3957
+ }
3958
+ }
3584
3959
  let prTitle = null;
3585
3960
  let prBody = null;
3586
3961
  async function tryGenerateAI() {
@@ -3683,7 +4058,6 @@ ${pc13.dim("AI body preview:")}`);
3683
4058
  }
3684
4059
  if (submitAction === "squash") {
3685
4060
  await performSquashMerge(origin, baseBranch, currentBranch, {
3686
- defaultMsg: prTitle ?? undefined,
3687
4061
  model: args.model,
3688
4062
  convention: config.commitConvention
3689
4063
  });
@@ -3714,12 +4088,6 @@ ${pc13.dim("AI body preview:")}`);
3714
4088
  }
3715
4089
  return;
3716
4090
  }
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
4091
  if (submitAction === "fill") {
3724
4092
  const fillResult = await createPRFill(baseBranch, args.draft);
3725
4093
  if (fillResult.exitCode !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contribute-now",
3
- "version": "0.4.1",
3
+ "version": "0.5.0-dev.9ad3d01",
4
4
  "description": "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,9 +18,10 @@
18
18
  "lint": "biome check .",
19
19
  "lint:fix": "biome check --write .",
20
20
  "format": "biome format --write .",
21
- "www:dev": "bun run --cwd www dev",
22
- "www:build": "bun run --cwd www build",
23
- "www:preview": "bun run --cwd www preview"
21
+ "landing:install": "bun install --cwd landing",
22
+ "landing:dev": "bun run --cwd landing dev",
23
+ "landing:build": "bun run --cwd landing build",
24
+ "landing:preview": "bun run --cwd landing preview"
24
25
  },
25
26
  "engines": {
26
27
  "node": ">=18",