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.
- package/README.md +39 -12
- package/dist/index.js +850 -227
- 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
|
|
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.
|
|
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
|
-
"
|
|
2185
|
-
"
|
|
2186
|
-
"
|
|
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
|
|
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
|
-
|
|
2713
|
-
|
|
2714
|
-
if (
|
|
2715
|
-
console.log(
|
|
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
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
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
|
|
2725
|
-
if (
|
|
2726
|
-
|
|
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
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
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
|
-
|
|
2740
|
-
|
|
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/
|
|
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 =
|
|
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: ${
|
|
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: ${
|
|
2962
|
-
const confirmed = await confirmPrompt(`Role detected as ${
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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: ${
|
|
3035
|
-
info(`Convention: ${
|
|
3036
|
-
info(`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: ${
|
|
3461
|
+
info(`Main: ${pc11.bold(config.mainBranch)} | Dev: ${pc11.bold(config.devBranch)}`);
|
|
3039
3462
|
} else {
|
|
3040
|
-
info(`Main: ${
|
|
3463
|
+
info(`Main: ${pc11.bold(config.mainBranch)}`);
|
|
3041
3464
|
}
|
|
3042
|
-
info(`Origin: ${
|
|
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: ${
|
|
3047
|
-
info(`Convention: ${
|
|
3048
|
-
info(`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: ${
|
|
3473
|
+
info(`Main: ${pc11.bold(config.mainBranch)} | Dev: ${pc11.bold(config.devBranch)}`);
|
|
3051
3474
|
} else {
|
|
3052
|
-
info(`Main: ${
|
|
3475
|
+
info(`Main: ${pc11.bold(config.mainBranch)}`);
|
|
3053
3476
|
}
|
|
3054
|
-
info(`Origin: ${
|
|
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
|
|
3059
|
-
import
|
|
3060
|
-
var start_default =
|
|
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
|
-
${
|
|
3117
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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: ${
|
|
3558
|
+
info(`Creating branch: ${pc12.bold(branchName)}`);
|
|
3136
3559
|
if (await branchExists(branchName)) {
|
|
3137
|
-
error(`Branch ${
|
|
3138
|
-
info(` Use ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
3589
|
+
success(`✅ Created ${pc12.bold(branchName)} from ${pc12.bold(syncSource.ref)}`);
|
|
3167
3590
|
return;
|
|
3168
3591
|
}
|
|
3169
|
-
error(`Failed to update ${
|
|
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 ${
|
|
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
|
|
3184
|
-
import
|
|
3185
|
-
var status_default =
|
|
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(` ${
|
|
3202
|
-
console.log(` ${
|
|
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(` ${
|
|
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
|
-
|
|
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 +
|
|
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(
|
|
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(` ${
|
|
3668
|
+
console.log(` ${pc13.green("Staged for commit:")}`);
|
|
3236
3669
|
for (const { file, status } of fileStatus.staged) {
|
|
3237
|
-
console.log(` ${
|
|
3670
|
+
console.log(` ${pc13.green("+")} ${pc13.dim(`${status}:`)} ${file}`);
|
|
3238
3671
|
}
|
|
3239
3672
|
}
|
|
3240
3673
|
if (fileStatus.modified.length > 0) {
|
|
3241
|
-
console.log(` ${
|
|
3674
|
+
console.log(` ${pc13.yellow("Unstaged changes:")}`);
|
|
3242
3675
|
for (const { file, status } of fileStatus.modified) {
|
|
3243
|
-
console.log(` ${
|
|
3676
|
+
console.log(` ${pc13.yellow("~")} ${pc13.dim(`${status}:`)} ${file}`);
|
|
3244
3677
|
}
|
|
3245
3678
|
}
|
|
3246
3679
|
if (fileStatus.untracked.length > 0) {
|
|
3247
|
-
console.log(` ${
|
|
3680
|
+
console.log(` ${pc13.red("Untracked files:")}`);
|
|
3248
3681
|
for (const file of fileStatus.untracked) {
|
|
3249
|
-
console.log(` ${
|
|
3682
|
+
console.log(` ${pc13.red("?")} ${file}`);
|
|
3250
3683
|
}
|
|
3251
3684
|
}
|
|
3252
3685
|
} else if (!dirty) {
|
|
3253
|
-
console.log(` ${
|
|
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 ${
|
|
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 ${
|
|
3261
|
-
}
|
|
3262
|
-
if (
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
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(` ${
|
|
3709
|
+
console.log(` ${pc13.dim("\uD83D\uDCA1 Tip:")}`);
|
|
3271
3710
|
for (const tip of tips) {
|
|
3272
|
-
console.log(` ${
|
|
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 =
|
|
3718
|
+
const label = pc13.bold(branch.padEnd(20));
|
|
3280
3719
|
if (ahead === 0 && behind === 0) {
|
|
3281
|
-
return ` ${
|
|
3720
|
+
return ` ${pc13.green("✓")} ${label} ${pc13.dim(`in sync with ${base}`)}`;
|
|
3282
3721
|
}
|
|
3283
3722
|
if (ahead > 0 && behind === 0) {
|
|
3284
|
-
return ` ${
|
|
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 ` ${
|
|
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
|
-
|
|
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
|
|
3294
|
-
import
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
3326
|
-
let finalMsg;
|
|
3808
|
+
let finalMsg = null;
|
|
3327
3809
|
if (message) {
|
|
3328
|
-
|
|
3329
|
-
|
|
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",
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
3359
|
-
info(`Run ${
|
|
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 =
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
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
|
-
${
|
|
3444
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
3977
|
+
success(`Created ${pc14.bold(newBranchName)} with your changes.`);
|
|
3470
3978
|
await updateLocalBranch(currentBranch, remoteRef);
|
|
3471
|
-
info(`Reset ${
|
|
3979
|
+
info(`Reset ${pc14.bold(currentBranch)} back to ${pc14.bold(remoteRef)} — no damage done.`);
|
|
3472
3980
|
console.log();
|
|
3473
|
-
success(`You're now on ${
|
|
3474
|
-
info(`Run ${
|
|
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} (${
|
|
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 ${
|
|
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(
|
|
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
|
-
${
|
|
3512
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
4046
|
+
success(`Renamed ${pc14.bold(currentBranch)} → ${pc14.bold(newBranchName)}`);
|
|
3539
4047
|
await unsetUpstream();
|
|
3540
4048
|
const syncSource2 = getSyncSource(config);
|
|
3541
|
-
info(`Syncing ${
|
|
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(` ${
|
|
4060
|
+
info(` ${pc14.bold("git rebase --continue")}`);
|
|
3553
4061
|
} else {
|
|
3554
|
-
success(`Rebased ${
|
|
4062
|
+
success(`Rebased ${pc14.bold(newBranchName)} onto ${pc14.bold(syncSource2.ref)}.`);
|
|
3555
4063
|
}
|
|
3556
|
-
info(`All your changes are preserved. Run ${
|
|
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 ${
|
|
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 ${
|
|
3572
|
-
info(`Deleting stale branch ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
${
|
|
4127
|
+
${pc14.dim("AI title:")} ${pc14.bold(pc14.cyan(prTitle))}`);
|
|
3601
4128
|
console.log(`
|
|
3602
|
-
${
|
|
3603
|
-
console.log(
|
|
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 ${
|
|
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(` ${
|
|
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
|
|
3752
|
-
import
|
|
3753
|
-
var sync_default =
|
|
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 ${
|
|
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 ${
|
|
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(`${
|
|
4431
|
+
info(`${pc16.bold(baseBranch)} is ${pc16.yellow(`${div.ahead} ahead`)} and ${pc16.red(`${div.behind} behind`)} ${syncSource.ref}`);
|
|
3813
4432
|
} else {
|
|
3814
|
-
info(`${
|
|
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 ${
|
|
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(
|
|
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
|
-
${
|
|
3849
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
3883
|
-
success(`✅ ${
|
|
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 ${
|
|
3886
|
-
info(`Run ${
|
|
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 ${
|
|
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 ${
|
|
3909
|
-
info(`Use ${
|
|
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 ${
|
|
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
|
|
3934
|
-
import
|
|
3935
|
-
var update_default =
|
|
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 ${
|
|
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 ${
|
|
3982
|
-
info(`Use ${
|
|
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 ${
|
|
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(
|
|
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
|
-
${
|
|
4014
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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 ${
|
|
4654
|
+
success(`Created ${pc17.bold(newBranchName)} with your changes.`);
|
|
4036
4655
|
await updateLocalBranch(currentBranch, remoteRef);
|
|
4037
|
-
info(`Reset ${
|
|
4656
|
+
info(`Reset ${pc17.bold(currentBranch)} back to ${pc17.bold(remoteRef)} — no damage done.`);
|
|
4038
4657
|
console.log();
|
|
4039
|
-
success(`You're now on ${
|
|
4040
|
-
info(`Run ${
|
|
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} (${
|
|
4051
|
-
info(`Link: ${
|
|
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(`${
|
|
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(
|
|
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
|
-
${
|
|
4080
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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(` ${
|
|
4737
|
+
info(` ${pc17.bold("git rebase --continue")}`);
|
|
4119
4738
|
} else {
|
|
4120
|
-
success(`Rebased ${
|
|
4739
|
+
success(`Rebased ${pc17.bold(newBranchName)} onto ${pc17.bold(syncSource.ref)}.`);
|
|
4121
4740
|
}
|
|
4122
|
-
info(`All your changes are preserved. Run ${
|
|
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 ${
|
|
4136
|
-
info(`Deleting stale branch ${
|
|
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 ${
|
|
4139
|
-
info(`Run ${
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
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
|
-
${
|
|
4182
|
-
console.log(
|
|
4800
|
+
${pc17.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
4801
|
+
console.log(pc17.dim("─".repeat(60)));
|
|
4183
4802
|
console.log(suggestion);
|
|
4184
|
-
console.log(
|
|
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(
|
|
4811
|
+
console.log(pc17.bold("To resolve:"));
|
|
4193
4812
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
4194
|
-
console.log(` 2. ${
|
|
4195
|
-
console.log(` 3. ${
|
|
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: ${
|
|
4816
|
+
console.log(` Or abort: ${pc17.cyan("git rebase --abort")}`);
|
|
4198
4817
|
process.exit(1);
|
|
4199
4818
|
}
|
|
4200
|
-
success(`✅ ${
|
|
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
|
|
4206
|
-
import
|
|
4207
|
-
var validate_default =
|
|
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(
|
|
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
|
|
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(
|
|
4886
|
+
console.log(pc19.cyan(`
|
|
4268
4887
|
${logo}`));
|
|
4269
|
-
console.log(` ${
|
|
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(` ${
|
|
4273
|
-
console.log(` ${
|
|
4274
|
-
console.log(` ${
|
|
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 =
|
|
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,
|