contribute-now 0.4.0-dev.c791252 → 0.4.0-dev.d48d9e6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +252 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { defineCommand } from "citty";
10
10
  import pc2 from "picocolors";
11
11
 
12
12
  // src/utils/config.ts
13
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  var CONFIG_FILENAME = ".contributerc.json";
16
16
  function getConfigPath(cwd = process.cwd()) {
@@ -86,6 +86,23 @@ function isGitignored(cwd = process.cwd()) {
86
86
  return false;
87
87
  }
88
88
  }
89
+ function ensureGitignored(cwd = process.cwd()) {
90
+ if (isGitignored(cwd))
91
+ return false;
92
+ const gitignorePath = join(cwd, ".gitignore");
93
+ const line = `${CONFIG_FILENAME}
94
+ `;
95
+ if (!existsSync(gitignorePath)) {
96
+ writeFileSync(gitignorePath, line, "utf-8");
97
+ return true;
98
+ }
99
+ const content = readFileSync(gitignorePath, "utf-8");
100
+ const needsLeadingNewline = content.length > 0 && !content.endsWith(`
101
+ `);
102
+ appendFileSync(gitignorePath, `${needsLeadingNewline ? `
103
+ ` : ""}${line}`, "utf-8");
104
+ return true;
105
+ }
89
106
  function getDefaultConfig() {
90
107
  return {
91
108
  workflow: "clean-flow",
@@ -953,6 +970,68 @@ function withTimeout(promise, ms) {
953
970
  }
954
971
  var COPILOT_TIMEOUT_MS = 30000;
955
972
  var COPILOT_LONG_TIMEOUT_MS = 90000;
973
+ var BATCH_CONFIG = {
974
+ LARGE_CHANGESET_THRESHOLD: 15,
975
+ COMPACT_PER_FILE_CHARS: 300,
976
+ MAX_COMPACT_PAYLOAD: 1e4,
977
+ FALLBACK_BATCH_SIZE: 15
978
+ };
979
+ function parseDiffByFile(rawDiff) {
980
+ const sections = new Map;
981
+ const headerPattern = /^diff --git a\/(.+?) b\//gm;
982
+ const positions = [];
983
+ for (let match = headerPattern.exec(rawDiff);match !== null; match = headerPattern.exec(rawDiff)) {
984
+ positions.push({ file: match[1], start: match.index });
985
+ }
986
+ for (let i = 0;i < positions.length; i++) {
987
+ const { file, start } = positions[i];
988
+ const end = i + 1 < positions.length ? positions[i + 1].start : rawDiff.length;
989
+ sections.set(file, rawDiff.slice(start, end));
990
+ }
991
+ return sections;
992
+ }
993
+ function extractDiffStats(diffSection) {
994
+ let added = 0;
995
+ let removed = 0;
996
+ for (const line of diffSection.split(`
997
+ `)) {
998
+ if (line.startsWith("+") && !line.startsWith("+++"))
999
+ added++;
1000
+ if (line.startsWith("-") && !line.startsWith("---"))
1001
+ removed++;
1002
+ }
1003
+ return { added, removed };
1004
+ }
1005
+ function createCompactDiff(files, rawDiff, maxTotalChars = BATCH_CONFIG.MAX_COMPACT_PAYLOAD) {
1006
+ if (files.length === 0)
1007
+ return "";
1008
+ const diffSections = parseDiffByFile(rawDiff);
1009
+ const perFileBudget = Math.min(BATCH_CONFIG.COMPACT_PER_FILE_CHARS, Math.floor(maxTotalChars / files.length));
1010
+ const parts = [];
1011
+ for (const file of files) {
1012
+ const section = diffSections.get(file);
1013
+ if (section) {
1014
+ const stats = extractDiffStats(section);
1015
+ const header = `[${file}] (+${stats.added}/-${stats.removed})`;
1016
+ if (section.length <= perFileBudget) {
1017
+ parts.push(`${header}
1018
+ ${section}`);
1019
+ } else {
1020
+ const truncated = section.slice(0, perFileBudget - header.length - 20);
1021
+ parts.push(`${header}
1022
+ ${truncated}
1023
+ ...(truncated)`);
1024
+ }
1025
+ } else {
1026
+ parts.push(`[${file}] (new/binary file — no diff available)`);
1027
+ }
1028
+ }
1029
+ const result = parts.join(`
1030
+
1031
+ `);
1032
+ return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
1033
+ ...(truncated)` : result;
1034
+ }
956
1035
  async function checkCopilotAvailable() {
957
1036
  try {
958
1037
  const client = await getManagedClient();
@@ -1053,16 +1132,18 @@ function extractJson(raw) {
1053
1132
  }
1054
1133
  async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
1055
1134
  try {
1135
+ const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1056
1136
  const multiFileHint = stagedFiles.length > 1 ? `
1057
1137
 
1058
1138
  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.` : "";
1139
+ const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
1059
1140
  const userMessage = `Generate a commit message for these staged changes:
1060
1141
 
1061
- Files: ${stagedFiles.join(", ")}
1142
+ Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
1062
1143
 
1063
1144
  Diff:
1064
- ${diff.slice(0, 4000)}${multiFileHint}`;
1065
- const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
1145
+ ${diffContent}${multiFileHint}`;
1146
+ const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
1066
1147
  return result?.trim() ?? null;
1067
1148
  } catch {
1068
1149
  return null;
@@ -1111,16 +1192,23 @@ ${conflictDiff.slice(0, 4000)}`;
1111
1192
  }
1112
1193
  }
1113
1194
  async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
1195
+ const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1196
+ const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
1197
+ const largeHint = isLarge ? `
1198
+
1199
+ NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
1114
1200
  const userMessage = `Group these changed files into logical atomic commits:
1115
1201
 
1116
1202
  Files:
1117
1203
  ${files.join(`
1118
1204
  `)}
1119
1205
 
1120
- Diffs (truncated):
1121
- ${diffs.slice(0, 6000)}`;
1206
+ Diffs:
1207
+ ${diffContent}${largeHint}`;
1122
1208
  const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
1123
1209
  if (!result) {
1210
+ if (isLarge)
1211
+ return generateCommitGroupsInBatches(files, diffs, model, convention);
1124
1212
  throw new Error("AI returned an empty response");
1125
1213
  }
1126
1214
  const cleaned = extractJson(result);
@@ -1128,10 +1216,14 @@ ${diffs.slice(0, 6000)}`;
1128
1216
  try {
1129
1217
  parsed = JSON.parse(cleaned);
1130
1218
  } catch {
1219
+ if (isLarge)
1220
+ return generateCommitGroupsInBatches(files, diffs, model, convention);
1131
1221
  throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
1132
1222
  }
1133
1223
  const groups = parsed;
1134
1224
  if (!Array.isArray(groups) || groups.length === 0) {
1225
+ if (isLarge)
1226
+ return generateCommitGroupsInBatches(files, diffs, model, convention);
1135
1227
  throw new Error("AI response was not a valid JSON array of commit groups");
1136
1228
  }
1137
1229
  for (const group of groups) {
@@ -1141,7 +1233,51 @@ ${diffs.slice(0, 6000)}`;
1141
1233
  }
1142
1234
  return groups;
1143
1235
  }
1236
+ async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
1237
+ const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
1238
+ const allGroups = [];
1239
+ const diffSections = parseDiffByFile(diffs);
1240
+ for (let i = 0;i < files.length; i += batchSize) {
1241
+ const batchFiles = files.slice(i, i + batchSize);
1242
+ const batchDiff = batchFiles.map((f) => diffSections.get(f) ?? "").filter(Boolean).join(`
1243
+ `);
1244
+ const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
1245
+ const batchNum = Math.floor(i / batchSize) + 1;
1246
+ const totalBatches = Math.ceil(files.length / batchSize);
1247
+ const userMessage = `Group these changed files into logical atomic commits:
1248
+
1249
+ Files:
1250
+ ${batchFiles.join(`
1251
+ `)}
1252
+
1253
+ Diffs:
1254
+ ${batchDiffContent}
1255
+
1256
+ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group only the files listed above.`;
1257
+ try {
1258
+ const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
1259
+ if (!result)
1260
+ continue;
1261
+ const cleaned = extractJson(result);
1262
+ const parsed = JSON.parse(cleaned);
1263
+ if (Array.isArray(parsed)) {
1264
+ for (const group of parsed) {
1265
+ if (Array.isArray(group.files) && typeof group.message === "string") {
1266
+ allGroups.push(group);
1267
+ }
1268
+ }
1269
+ }
1270
+ } catch {}
1271
+ }
1272
+ if (allGroups.length === 0) {
1273
+ throw new Error("AI could not group any files even with batch processing");
1274
+ }
1275
+ return allGroups;
1276
+ }
1144
1277
  async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
1278
+ const totalFiles = groups.reduce((sum, g) => sum + g.files.length, 0);
1279
+ const isLarge = totalFiles >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1280
+ const diffContent = isLarge ? createCompactDiff(groups.flatMap((g) => g.files), diffs) : diffs.slice(0, 6000);
1145
1281
  const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
1146
1282
  `);
1147
1283
  const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
@@ -1149,8 +1285,8 @@ async function regenerateAllGroupMessages(groups, diffs, model, convention = "cl
1149
1285
  Groups:
1150
1286
  ${groupSummary}
1151
1287
 
1152
- Diffs (truncated):
1153
- ${diffs.slice(0, 6000)}`;
1288
+ Diffs:
1289
+ ${diffContent}`;
1154
1290
  const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
1155
1291
  if (!result)
1156
1292
  return groups;
@@ -1169,12 +1305,14 @@ ${diffs.slice(0, 6000)}`;
1169
1305
  }
1170
1306
  async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
1171
1307
  try {
1308
+ const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1309
+ const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 4000);
1172
1310
  const userMessage = `Generate a single commit message for these files:
1173
1311
 
1174
1312
  Files: ${files.join(", ")}
1175
1313
 
1176
1314
  Diff:
1177
- ${diffs.slice(0, 4000)}`;
1315
+ ${diffContent}`;
1178
1316
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
1179
1317
  return result?.trim() ?? null;
1180
1318
  } catch {
@@ -1732,7 +1870,8 @@ ${pc6.bold("Changed files:")}`);
1732
1870
  warn(`AI unavailable: ${copilotError}`);
1733
1871
  warn("Falling back to manual commit message entry.");
1734
1872
  } else {
1735
- const spinner = createSpinner("Generating commit message with AI...");
1873
+ const spinnerMsg = stagedFiles.length >= 15 ? `Generating commit message with AI (${stagedFiles.length} files — using optimized batching)...` : "Generating commit message with AI...";
1874
+ const spinner = createSpinner(spinnerMsg);
1736
1875
  commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
1737
1876
  if (commitMessage) {
1738
1877
  spinner.success("AI commit message generated.");
@@ -1823,7 +1962,7 @@ ${pc6.bold("Changed files:")}`);
1823
1962
  for (const f of changedFiles) {
1824
1963
  console.log(` ${pc6.dim("•")} ${f}`);
1825
1964
  }
1826
- const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
1965
+ const spinner = createSpinner(changedFiles.length >= 15 ? `Asking AI to group ${changedFiles.length} file(s) into logical commits (using optimized batching)...` : `Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
1827
1966
  const diffs = await getFullDiffForFiles(changedFiles);
1828
1967
  if (!diffs.trim()) {
1829
1968
  spinner.stop();
@@ -2001,7 +2140,7 @@ import pc7 from "picocolors";
2001
2140
  // package.json
2002
2141
  var package_default = {
2003
2142
  name: "contribute-now",
2004
- version: "0.4.0-dev.c791252",
2143
+ version: "0.4.0-dev.d48d9e6",
2005
2144
  description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
2006
2145
  type: "module",
2007
2146
  bin: {
@@ -2660,6 +2799,40 @@ function colorizeSubject(subject) {
2660
2799
  // src/commands/setup.ts
2661
2800
  import { defineCommand as defineCommand7 } from "citty";
2662
2801
  import pc10 from "picocolors";
2802
+ async function shouldContinueSetupWithExistingConfig(options) {
2803
+ const {
2804
+ existingConfig,
2805
+ hasConfigFile,
2806
+ confirm: confirm2,
2807
+ ensureIgnored,
2808
+ onInfo,
2809
+ onWarn,
2810
+ onSuccess,
2811
+ summary
2812
+ } = options;
2813
+ if (existingConfig) {
2814
+ onInfo("Existing .contributerc.json detected:");
2815
+ summary(existingConfig);
2816
+ const shouldContinue = await confirm2("Continue setup and overwrite existing config?");
2817
+ if (!shouldContinue) {
2818
+ if (ensureIgnored()) {
2819
+ onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
2820
+ }
2821
+ onSuccess("Keeping existing setup.");
2822
+ return false;
2823
+ }
2824
+ return true;
2825
+ }
2826
+ if (hasConfigFile) {
2827
+ onWarn("Found .contributerc.json but it appears invalid.");
2828
+ const shouldContinue = await confirm2("Continue setup and overwrite invalid config?");
2829
+ if (!shouldContinue) {
2830
+ onInfo("Keeping existing file. Run setup again when ready to repair it.");
2831
+ return false;
2832
+ }
2833
+ }
2834
+ return true;
2835
+ }
2663
2836
  var setup_default = defineCommand7({
2664
2837
  meta: {
2665
2838
  name: "setup",
@@ -2671,6 +2844,20 @@ var setup_default = defineCommand7({
2671
2844
  process.exit(1);
2672
2845
  }
2673
2846
  heading("\uD83D\uDD27 contribute-now setup");
2847
+ const existingConfig = readConfig();
2848
+ const shouldContinue = await shouldContinueSetupWithExistingConfig({
2849
+ existingConfig,
2850
+ hasConfigFile: configExists(),
2851
+ confirm: confirmPrompt,
2852
+ ensureIgnored: ensureGitignored,
2853
+ onInfo: info,
2854
+ onWarn: warn,
2855
+ onSuccess: success,
2856
+ summary: logConfigSummary
2857
+ });
2858
+ if (!shouldContinue) {
2859
+ return;
2860
+ }
2674
2861
  const workflowChoice = await selectPrompt("Which git workflow does this project use?", [
2675
2862
  "Clean Flow — main + dev, squash features into dev, merge dev into main (recommended)",
2676
2863
  "GitHub Flow — main + feature branches, squash/merge into main",
@@ -2700,31 +2887,42 @@ var setup_default = defineCommand7({
2700
2887
  info(`Found remotes: ${remotes.join(", ")}`);
2701
2888
  let detectedRole = null;
2702
2889
  let detectionSource = "";
2703
- const ghInstalled = await checkGhInstalled();
2704
- if (ghInstalled && await checkGhAuth()) {
2705
- const isFork = await isRepoFork();
2706
- if (isFork === true) {
2707
- detectedRole = "contributor";
2708
- detectionSource = "gh CLI (fork detected)";
2709
- } else if (isFork === false) {
2710
- const repoInfo = await getCurrentRepoInfo();
2711
- if (repoInfo) {
2712
- const perms = await checkRepoPermissions(repoInfo.owner, repoInfo.repo);
2713
- if (perms?.admin || perms?.push) {
2714
- detectedRole = "maintainer";
2715
- detectionSource = "gh CLI (admin/push permissions)";
2890
+ const roleSpinner = createSpinner("Detecting your role...");
2891
+ try {
2892
+ roleSpinner.update("Checking GitHub CLI and auth...");
2893
+ const ghInstalled = await checkGhInstalled();
2894
+ if (ghInstalled && await checkGhAuth()) {
2895
+ roleSpinner.update("Inspecting repository relationship (fork/permissions)...");
2896
+ const isFork = await isRepoFork();
2897
+ if (isFork === true) {
2898
+ detectedRole = "contributor";
2899
+ detectionSource = "gh CLI (fork detected)";
2900
+ } else if (isFork === false) {
2901
+ const repoInfo = await getCurrentRepoInfo();
2902
+ if (repoInfo) {
2903
+ const perms = await checkRepoPermissions(repoInfo.owner, repoInfo.repo);
2904
+ if (perms?.admin || perms?.push) {
2905
+ detectedRole = "maintainer";
2906
+ detectionSource = "gh CLI (admin/push permissions)";
2907
+ }
2716
2908
  }
2717
2909
  }
2718
2910
  }
2719
- }
2720
- if (detectedRole === null) {
2721
- if (remotes.includes("upstream")) {
2722
- detectedRole = "contributor";
2723
- detectionSource = "heuristic (upstream remote exists)";
2724
- } else if (remotes.includes("origin") && remotes.length === 1) {
2725
- detectedRole = "maintainer";
2726
- detectionSource = "heuristic (only origin remote)";
2911
+ if (detectedRole === null) {
2912
+ roleSpinner.update("Analyzing git remotes...");
2913
+ if (remotes.includes("upstream")) {
2914
+ detectedRole = "contributor";
2915
+ detectionSource = "heuristic (upstream remote exists)";
2916
+ } else if (remotes.includes("origin") && remotes.length === 1) {
2917
+ detectedRole = "maintainer";
2918
+ detectionSource = "heuristic (only origin remote)";
2919
+ }
2727
2920
  }
2921
+ roleSpinner.success("Role detection complete.");
2922
+ } catch {
2923
+ roleSpinner.fail("Role detection failed; falling back to manual selection.");
2924
+ detectedRole = null;
2925
+ detectionSource = "";
2728
2926
  }
2729
2927
  if (detectedRole === null) {
2730
2928
  const roleChoice = await selectPrompt("What is your role in this project?", [
@@ -2742,16 +2940,20 @@ var setup_default = defineCommand7({
2742
2940
  }
2743
2941
  }
2744
2942
  const defaultConfig = getDefaultConfig();
2745
- const mainBranch = await inputPrompt("Main branch name", defaultConfig.mainBranch);
2943
+ info(pc10.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
2944
+ const mainBranchDefault = defaultConfig.mainBranch;
2945
+ const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
2746
2946
  let devBranch;
2747
2947
  if (hasDevBranch(workflow)) {
2748
2948
  const defaultDev = workflow === "git-flow" ? "develop" : "dev";
2749
- devBranch = await inputPrompt("Dev/develop branch name", defaultDev);
2949
+ devBranch = await inputPrompt(`Dev/develop branch name (default: ${defaultDev} — press Enter to keep)`, defaultDev);
2750
2950
  }
2751
- const originRemote = await inputPrompt("Origin remote name", defaultConfig.origin);
2951
+ const originRemoteDefault = defaultConfig.origin;
2952
+ const originRemote = await inputPrompt(`Origin remote name (default: ${originRemoteDefault} — press Enter to keep)`, originRemoteDefault);
2752
2953
  let upstreamRemote = defaultConfig.upstream;
2753
2954
  if (detectedRole === "contributor") {
2754
- upstreamRemote = await inputPrompt("Upstream remote name", defaultConfig.upstream);
2955
+ const upstreamRemoteDefault = defaultConfig.upstream;
2956
+ upstreamRemote = await inputPrompt(`Upstream remote name (default: ${upstreamRemoteDefault} — press Enter to keep)`, upstreamRemoteDefault);
2755
2957
  if (!remotes.includes(upstreamRemote)) {
2756
2958
  warn(`Remote "${upstreamRemote}" not found.`);
2757
2959
  const originUrl = await getRemoteUrl(originRemote);
@@ -2799,9 +3001,8 @@ var setup_default = defineCommand7({
2799
3001
  warn("Config was saved — verify the branch name and re-run setup if needed.");
2800
3002
  }
2801
3003
  }
2802
- if (!isGitignored()) {
2803
- warn(".contributerc.json is not in .gitignore. Add it to avoid committing personal config.");
2804
- warn(' echo ".contributerc.json" >> .gitignore');
3004
+ if (ensureGitignored()) {
3005
+ info("Added .contributerc.json to .gitignore to avoid committing personal config.");
2805
3006
  }
2806
3007
  console.log();
2807
3008
  info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
@@ -2815,6 +3016,17 @@ var setup_default = defineCommand7({
2815
3016
  info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
2816
3017
  }
2817
3018
  });
3019
+ function logConfigSummary(config) {
3020
+ info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
3021
+ info(`Convention: ${pc10.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
3022
+ info(`Role: ${pc10.bold(config.role)}`);
3023
+ if (config.devBranch) {
3024
+ info(`Main: ${pc10.bold(config.mainBranch)} | Dev: ${pc10.bold(config.devBranch)}`);
3025
+ } else {
3026
+ info(`Main: ${pc10.bold(config.mainBranch)}`);
3027
+ }
3028
+ info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
3029
+ }
2818
3030
 
2819
3031
  // src/commands/start.ts
2820
3032
  import { defineCommand as defineCommand8 } from "citty";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contribute-now",
3
- "version": "0.4.0-dev.c791252",
3
+ "version": "0.4.0-dev.d48d9e6",
4
4
  "description": "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
5
5
  "type": "module",
6
6
  "bin": {