contribute-now 0.6.2-dev.38e14f5 → 0.6.2-dev.967437a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -3
  2. package/dist/index.js +737 -615
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7059,6 +7059,9 @@ function configExists(cwd = process.cwd()) {
7059
7059
  var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
7060
7060
  var VALID_ROLES = ["maintainer", "contributor"];
7061
7061
  var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
7062
+ function isAIEnabled(config, cliNoAI = false) {
7063
+ return config.aiEnabled !== false && !cliNoAI;
7064
+ }
7062
7065
  function readConfig(cwd = process.cwd()) {
7063
7066
  const path = getConfigPath(cwd);
7064
7067
  if (!existsSync(path))
@@ -7101,7 +7104,10 @@ function readConfig(cwd = process.cwd()) {
7101
7104
  console.error("Invalid .contributerc.json: all branchPrefixes must be non-empty strings.");
7102
7105
  return null;
7103
7106
  }
7104
- return parsed;
7107
+ return {
7108
+ ...parsed,
7109
+ aiEnabled: parsed.aiEnabled !== false
7110
+ };
7105
7111
  } catch {
7106
7112
  return null;
7107
7113
  }
@@ -7149,7 +7155,8 @@ function getDefaultConfig() {
7149
7155
  upstream: "upstream",
7150
7156
  origin: "origin",
7151
7157
  branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"],
7152
- commitConvention: "clean-commit"
7158
+ commitConvention: "clean-commit",
7159
+ aiEnabled: true
7153
7160
  };
7154
7161
  }
7155
7162
 
@@ -8756,6 +8763,7 @@ var LogEngine = {
8756
8763
 
8757
8764
  // src/utils/logger.ts
8758
8765
  var import_picocolors = __toESM(require_picocolors(), 1);
8766
+ var PROJECT_DISPLAY_NAME = "Contribute Now";
8759
8767
  LogEngine.configure({
8760
8768
  mode: LogMode.INFO,
8761
8769
  format: {
@@ -8776,9 +8784,10 @@ function warn(msg, emoji = "⚠️") {
8776
8784
  function info(msg, emoji = "ℹ️") {
8777
8785
  LogEngine.info(msg, undefined, { emoji });
8778
8786
  }
8779
- function heading(msg) {
8787
+ function projectHeading(command, emoji) {
8788
+ const prefix = emoji ? `${import_picocolors.default.bold(emoji)} ` : "";
8780
8789
  console.log(`
8781
- ${import_picocolors.default.bold(msg)}`);
8790
+ ${prefix}${import_picocolors.default.bold(import_picocolors.default.cyan(PROJECT_DISPLAY_NAME))} ${import_picocolors.default.dim("—")} ${import_picocolors.default.bold(command)}`);
8782
8791
  }
8783
8792
 
8784
8793
  // src/utils/workflow.ts
@@ -8871,7 +8880,7 @@ var branch_default = defineCommand({
8871
8880
  const currentBranch = await getCurrentBranch();
8872
8881
  const showRemoteOnly = args.remote;
8873
8882
  const showAll = args.all;
8874
- heading("\uD83C\uDF3F branches");
8883
+ projectHeading("branch", "\uD83C\uDF3F");
8875
8884
  console.log();
8876
8885
  if (!showRemoteOnly) {
8877
8886
  const localBranches = await getLocalBranches();
@@ -8981,6 +8990,9 @@ function groupByRemote(branches) {
8981
8990
  }
8982
8991
 
8983
8992
  // src/commands/clean.ts
8993
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
8994
+
8995
+ // src/utils/branchPrompt.ts
8984
8996
  var import_picocolors7 = __toESM(require_picocolors(), 1);
8985
8997
 
8986
8998
  // src/utils/branch.ts
@@ -10103,12 +10115,12 @@ async function multiSelectPrompt(message, choices) {
10103
10115
  init_dist();
10104
10116
  var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
10105
10117
  Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
10106
- Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Return ONLY the message line.
10118
+ Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Do NOT use backticks, quotes, or markdown formatting around filenames, functions, or identifiers. Return ONLY the message line.
10107
10119
  Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
10108
10120
  var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
10109
10121
  Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
10110
10122
  Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
10111
- Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
10123
+ Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Do NOT use backticks, quotes, or markdown formatting around filenames, functions, or identifiers. Return ONLY the message line.
10112
10124
  Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
10113
10125
  WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
10114
10126
  function getGroupingSystemPrompt(convention) {
@@ -10133,6 +10145,7 @@ Rules:
10133
10145
  - Each group should represent ONE logical change
10134
10146
  - Every file must appear in exactly one group
10135
10147
  - Commit messages must follow the convention, be concise, imperative, max 72 chars
10148
+ - Do not use backticks, quotes, or markdown formatting in commit messages
10136
10149
  - Order groups so foundational changes come first (types, utils) and consumers come after
10137
10150
  - Return ONLY the JSON array, nothing else`;
10138
10151
  }
@@ -10253,7 +10266,7 @@ ${truncated}
10253
10266
  return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
10254
10267
  ...(truncated)` : result;
10255
10268
  }
10256
- async function checkCopilotAvailable() {
10269
+ async function checkCopilotAvailable2() {
10257
10270
  try {
10258
10271
  const client = await getManagedClient();
10259
10272
  try {
@@ -10351,6 +10364,9 @@ function extractJson(raw) {
10351
10364
  }
10352
10365
  return text;
10353
10366
  }
10367
+ function sanitizeGeneratedCommitMessage(message) {
10368
+ return message.replace(/`+/g, "").replace(/\s+/g, " ").trim();
10369
+ }
10354
10370
  async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
10355
10371
  try {
10356
10372
  const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
@@ -10368,7 +10384,7 @@ Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
10368
10384
  Diff:
10369
10385
  ${diffContent}${multiFileHint}${squashHint}`;
10370
10386
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
10371
- return result?.trim() ?? null;
10387
+ return result ? sanitizeGeneratedCommitMessage(result) : null;
10372
10388
  } catch {
10373
10389
  return null;
10374
10390
  }
@@ -10415,6 +10431,40 @@ ${conflictDiff.slice(0, 4000)}`;
10415
10431
  return null;
10416
10432
  }
10417
10433
  }
10434
+ function normalizeCommitGroups(changedFiles, groups) {
10435
+ const changedSet = new Set(changedFiles);
10436
+ const assignedFiles = new Set;
10437
+ const unknownFiles = new Set;
10438
+ const duplicateFiles = new Set;
10439
+ const normalizedGroups = groups.map((group) => {
10440
+ const uniqueFiles = new Set;
10441
+ const files = [];
10442
+ for (const file of group.files) {
10443
+ if (!changedSet.has(file)) {
10444
+ unknownFiles.add(file);
10445
+ continue;
10446
+ }
10447
+ if (uniqueFiles.has(file) || assignedFiles.has(file)) {
10448
+ duplicateFiles.add(file);
10449
+ continue;
10450
+ }
10451
+ uniqueFiles.add(file);
10452
+ assignedFiles.add(file);
10453
+ files.push(file);
10454
+ }
10455
+ return {
10456
+ ...group,
10457
+ files
10458
+ };
10459
+ }).filter((group) => group.files.length > 0);
10460
+ const unassignedFiles = changedFiles.filter((file) => !assignedFiles.has(file));
10461
+ return {
10462
+ groups: normalizedGroups,
10463
+ unknownFiles: [...unknownFiles],
10464
+ duplicateFiles: [...duplicateFiles],
10465
+ unassignedFiles
10466
+ };
10467
+ }
10418
10468
  async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
10419
10469
  const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
10420
10470
  const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
@@ -10455,7 +10505,10 @@ ${diffContent}${largeHint}`;
10455
10505
  throw new Error("AI returned groups with invalid structure (missing files or message)");
10456
10506
  }
10457
10507
  }
10458
- return groups;
10508
+ return groups.map((group) => ({
10509
+ ...group,
10510
+ message: sanitizeGeneratedCommitMessage(group.message)
10511
+ }));
10459
10512
  }
10460
10513
  async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
10461
10514
  const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
@@ -10490,7 +10543,11 @@ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group o
10490
10543
  const batchFileSet = new Set(batchFiles);
10491
10544
  const filteredFiles = group.files.filter((f3) => batchFileSet.has(f3));
10492
10545
  if (filteredFiles.length > 0) {
10493
- allGroups.push({ ...group, files: filteredFiles });
10546
+ allGroups.push({
10547
+ ...group,
10548
+ files: filteredFiles,
10549
+ message: sanitizeGeneratedCommitMessage(group.message)
10550
+ });
10494
10551
  }
10495
10552
  }
10496
10553
  }
@@ -10533,7 +10590,7 @@ ${diffContent}`;
10533
10590
  return groups;
10534
10591
  return groups.map((g3, i2) => ({
10535
10592
  files: g3.files,
10536
- message: typeof parsed[i2]?.message === "string" ? parsed[i2].message : g3.message
10593
+ message: typeof parsed[i2]?.message === "string" ? sanitizeGeneratedCommitMessage(parsed[i2].message) : g3.message
10537
10594
  }));
10538
10595
  } catch {
10539
10596
  return groups;
@@ -10550,12 +10607,160 @@ Files: ${files.join(", ")}
10550
10607
  Diff:
10551
10608
  ${diffContent}`;
10552
10609
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
10553
- return result?.trim() ?? null;
10610
+ return result ? sanitizeGeneratedCommitMessage(result) : null;
10554
10611
  } catch {
10555
10612
  return null;
10556
10613
  }
10557
10614
  }
10558
10615
 
10616
+ // src/utils/spinner.ts
10617
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
10618
+ var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10619
+ function createSpinner(text) {
10620
+ let frameIdx = 0;
10621
+ let currentText = text;
10622
+ let stopped = false;
10623
+ const clearLine = () => {
10624
+ process.stderr.write("\r\x1B[K");
10625
+ };
10626
+ const render = () => {
10627
+ if (stopped)
10628
+ return;
10629
+ const frame = import_picocolors6.default.cyan(FRAMES[frameIdx % FRAMES.length]);
10630
+ clearLine();
10631
+ process.stderr.write(`${frame} ${currentText}`);
10632
+ frameIdx++;
10633
+ };
10634
+ const timer = setInterval(render, 80);
10635
+ render();
10636
+ const stop = () => {
10637
+ if (stopped)
10638
+ return;
10639
+ stopped = true;
10640
+ clearInterval(timer);
10641
+ clearLine();
10642
+ };
10643
+ return {
10644
+ update(newText) {
10645
+ currentText = newText;
10646
+ },
10647
+ success(msg) {
10648
+ stop();
10649
+ process.stderr.write(`${import_picocolors6.default.green("✔")} ${msg}
10650
+ `);
10651
+ },
10652
+ fail(msg) {
10653
+ stop();
10654
+ process.stderr.write(`${import_picocolors6.default.red("✖")} ${msg}
10655
+ `);
10656
+ },
10657
+ stop() {
10658
+ stop();
10659
+ }
10660
+ };
10661
+ }
10662
+
10663
+ // src/utils/branchPrompt.ts
10664
+ async function promptForBranchName(options) {
10665
+ const promptMessage = options.promptMessage ?? "What are you going to work on?";
10666
+ let branchInput = options.initialValue?.trim() ?? "";
10667
+ while (!branchInput) {
10668
+ branchInput = (await inputPrompt(promptMessage)).trim();
10669
+ if (branchInput)
10670
+ break;
10671
+ warn("A branch name or description is required.");
10672
+ const action = await selectPrompt("What would you like to do?", ["Try again", "Cancel"]);
10673
+ if (action === "Cancel")
10674
+ return null;
10675
+ }
10676
+ let branchName = branchInput;
10677
+ const useAI = options.useAI !== false && looksLikeNaturalLanguage(branchInput);
10678
+ if (useAI) {
10679
+ const copilotError = await checkCopilotAvailable2();
10680
+ if (copilotError) {
10681
+ warn(`AI unavailable: ${copilotError}`);
10682
+ } else {
10683
+ while (true) {
10684
+ const spinner = createSpinner("Generating branch name suggestion...");
10685
+ const suggested = await suggestBranchName(branchInput, options.model);
10686
+ if (suggested) {
10687
+ spinner.success("Branch name suggestion ready.");
10688
+ console.log(`
10689
+ ${import_picocolors7.default.dim("AI suggestion:")} ${import_picocolors7.default.bold(import_picocolors7.default.cyan(suggested))}`);
10690
+ const action2 = await selectPrompt("What would you like to do with this branch name?", [
10691
+ "Use this suggestion",
10692
+ "Try again with AI",
10693
+ "Enter branch name manually",
10694
+ "Use my original description",
10695
+ "Cancel"
10696
+ ]);
10697
+ if (action2 === "Use this suggestion") {
10698
+ branchName = suggested;
10699
+ break;
10700
+ }
10701
+ if (action2 === "Try again with AI") {
10702
+ continue;
10703
+ }
10704
+ if (action2 === "Enter branch name manually") {
10705
+ branchName = (await inputPrompt("Enter branch name", branchInput)).trim();
10706
+ break;
10707
+ }
10708
+ if (action2 === "Use my original description") {
10709
+ branchName = branchInput;
10710
+ break;
10711
+ }
10712
+ return null;
10713
+ }
10714
+ spinner.fail("AI did not return a branch name suggestion.");
10715
+ const action = await selectPrompt("AI could not generate a branch name. What would you like to do?", [
10716
+ "Try again with AI",
10717
+ "Enter branch name manually",
10718
+ "Use my original description",
10719
+ "Cancel"
10720
+ ]);
10721
+ if (action === "Try again with AI") {
10722
+ continue;
10723
+ }
10724
+ if (action === "Enter branch name manually") {
10725
+ branchName = (await inputPrompt("Enter branch name", branchInput)).trim();
10726
+ break;
10727
+ }
10728
+ if (action === "Use my original description") {
10729
+ branchName = branchInput;
10730
+ break;
10731
+ }
10732
+ return null;
10733
+ }
10734
+ }
10735
+ }
10736
+ while (true) {
10737
+ if (!branchName) {
10738
+ branchName = (await inputPrompt("Enter branch name", branchInput)).trim();
10739
+ if (!branchName) {
10740
+ const action = await selectPrompt("What would you like to do?", ["Try again", "Cancel"]);
10741
+ if (action === "Cancel")
10742
+ return null;
10743
+ continue;
10744
+ }
10745
+ }
10746
+ if (!hasPrefix(branchName, options.branchPrefixes)) {
10747
+ const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors7.default.bold(branchName)}:`, options.branchPrefixes);
10748
+ branchName = formatBranchName(prefix, branchName);
10749
+ }
10750
+ if (!isValidBranchName(branchName)) {
10751
+ warn("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
10752
+ branchName = (await inputPrompt("Enter branch name", branchName)).trim();
10753
+ continue;
10754
+ }
10755
+ if (await branchExists(branchName)) {
10756
+ warn(`Branch ${import_picocolors7.default.bold(branchName)} already exists. Choose a different name.`);
10757
+ branchName = (await inputPrompt("Enter branch name", branchName)).trim();
10758
+ continue;
10759
+ }
10760
+ return branchName;
10761
+ }
10762
+ }
10763
+
10559
10764
  // src/utils/gh.ts
10560
10765
  import { execFile as execFileCb2 } from "node:child_process";
10561
10766
  function run2(args) {
@@ -10694,53 +10899,6 @@ async function getMergedPRForBranch(headBranch) {
10694
10899
  }
10695
10900
  }
10696
10901
 
10697
- // src/utils/spinner.ts
10698
- var import_picocolors6 = __toESM(require_picocolors(), 1);
10699
- var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10700
- function createSpinner(text) {
10701
- let frameIdx = 0;
10702
- let currentText = text;
10703
- let stopped = false;
10704
- const clearLine = () => {
10705
- process.stderr.write("\r\x1B[K");
10706
- };
10707
- const render = () => {
10708
- if (stopped)
10709
- return;
10710
- const frame = import_picocolors6.default.cyan(FRAMES[frameIdx % FRAMES.length]);
10711
- clearLine();
10712
- process.stderr.write(`${frame} ${currentText}`);
10713
- frameIdx++;
10714
- };
10715
- const timer = setInterval(render, 80);
10716
- render();
10717
- const stop = () => {
10718
- if (stopped)
10719
- return;
10720
- stopped = true;
10721
- clearInterval(timer);
10722
- clearLine();
10723
- };
10724
- return {
10725
- update(newText) {
10726
- currentText = newText;
10727
- },
10728
- success(msg) {
10729
- stop();
10730
- process.stderr.write(`${import_picocolors6.default.green("✔")} ${msg}
10731
- `);
10732
- },
10733
- fail(msg) {
10734
- stop();
10735
- process.stderr.write(`${import_picocolors6.default.red("✖")} ${msg}
10736
- `);
10737
- },
10738
- stop() {
10739
- stop();
10740
- }
10741
- };
10742
- }
10743
-
10744
10902
  // src/commands/clean.ts
10745
10903
  async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10746
10904
  if (!config)
@@ -10753,44 +10911,22 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10753
10911
  warn("You have uncommitted changes in your working tree.");
10754
10912
  }
10755
10913
  if (localWork.unpushedCommits > 0) {
10756
- warn(`You have ${import_picocolors7.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
10914
+ warn(`You have ${import_picocolors8.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
10757
10915
  }
10758
10916
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
10759
10917
  const DISCARD = "Discard all changes and clean up";
10760
10918
  const CANCEL = "Skip this branch";
10761
- const action = await selectPrompt(`${import_picocolors7.default.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
10919
+ const action = await selectPrompt(`${import_picocolors8.default.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
10762
10920
  if (action === CANCEL)
10763
10921
  return "skipped";
10764
10922
  if (action === SAVE_NEW_BRANCH) {
10765
10923
  if (!config)
10766
10924
  return "skipped";
10767
- info(import_picocolors7.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
10768
- const description = await inputPrompt("What are you going to work on?");
10769
- let newBranchName = description;
10770
- if (looksLikeNaturalLanguage(description)) {
10771
- const spinner = createSpinner("Generating branch name suggestion...");
10772
- const suggested = await suggestBranchName(description);
10773
- if (suggested) {
10774
- spinner.success("Branch name suggestion ready.");
10775
- console.log(`
10776
- ${import_picocolors7.default.dim("AI suggestion:")} ${import_picocolors7.default.bold(import_picocolors7.default.cyan(suggested))}`);
10777
- const accepted = await confirmPrompt(`Use ${import_picocolors7.default.bold(suggested)} as your branch name?`);
10778
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
10779
- } else {
10780
- spinner.fail("AI did not return a suggestion.");
10781
- newBranchName = await inputPrompt("Enter branch name", description);
10782
- }
10783
- }
10784
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
10785
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors7.default.bold(newBranchName)}:`, config.branchPrefixes);
10786
- newBranchName = formatBranchName(prefix, newBranchName);
10787
- }
10788
- if (!isValidBranchName(newBranchName)) {
10789
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
10790
- return "skipped";
10791
- }
10792
- if (await branchExists(newBranchName)) {
10793
- error(`Branch ${import_picocolors7.default.bold(newBranchName)} already exists. Choose a different name.`);
10925
+ const newBranchName = await promptForBranchName({
10926
+ branchPrefixes: config.branchPrefixes,
10927
+ useAI: isAIEnabled(config)
10928
+ });
10929
+ if (!newBranchName) {
10794
10930
  return "skipped";
10795
10931
  }
10796
10932
  const renameResult = await renameBranch(currentBranch, newBranchName);
@@ -10798,7 +10934,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10798
10934
  error(`Failed to rename branch: ${renameResult.stderr}`);
10799
10935
  return "skipped";
10800
10936
  }
10801
- success(`Renamed ${import_picocolors7.default.bold(currentBranch)} → ${import_picocolors7.default.bold(newBranchName)}`);
10937
+ success(`Renamed ${import_picocolors8.default.bold(currentBranch)} → ${import_picocolors8.default.bold(newBranchName)}`);
10802
10938
  const syncSource2 = getSyncSource(config);
10803
10939
  await fetchRemote(syncSource2.remote);
10804
10940
  const savedUpstreamRef = await getUpstreamRef();
@@ -10806,10 +10942,10 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10806
10942
  if (rebaseResult.exitCode !== 0) {
10807
10943
  await rebaseAbort();
10808
10944
  warn("Rebase had conflicts — aborted to keep the repo in a clean state.");
10809
- info(`Your work is saved on ${import_picocolors7.default.bold(newBranchName)}. After cleanup, rebase manually:`, "");
10810
- info(` ${import_picocolors7.default.bold(`git checkout ${newBranchName} && git rebase ${syncSource2.ref}`)}`, "");
10945
+ info(`Your work is saved on ${import_picocolors8.default.bold(newBranchName)}. After cleanup, rebase manually:`, "");
10946
+ info(` ${import_picocolors8.default.bold(`git checkout ${newBranchName} && git rebase ${syncSource2.ref}`)}`, "");
10811
10947
  } else {
10812
- success(`Rebased ${import_picocolors7.default.bold(newBranchName)} onto ${import_picocolors7.default.bold(syncSource2.ref)}.`);
10948
+ success(`Rebased ${import_picocolors8.default.bold(newBranchName)} onto ${import_picocolors8.default.bold(syncSource2.ref)}.`);
10813
10949
  }
10814
10950
  const coResult2 = await checkoutBranch(baseBranch);
10815
10951
  if (coResult2.exitCode !== 0) {
@@ -10817,12 +10953,12 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10817
10953
  return "saved";
10818
10954
  }
10819
10955
  await updateLocalBranch(baseBranch, syncSource2.ref);
10820
- success(`Synced ${import_picocolors7.default.bold(baseBranch)} with ${import_picocolors7.default.bold(syncSource2.ref)}.`);
10956
+ success(`Synced ${import_picocolors8.default.bold(baseBranch)} with ${import_picocolors8.default.bold(syncSource2.ref)}.`);
10821
10957
  return "saved";
10822
10958
  }
10823
10959
  }
10824
10960
  const syncSource = getSyncSource(config);
10825
- info(`Switching to ${import_picocolors7.default.bold(baseBranch)} and syncing...`);
10961
+ info(`Switching to ${import_picocolors8.default.bold(baseBranch)} and syncing...`);
10826
10962
  await fetchRemote(syncSource.remote);
10827
10963
  await resetHard("HEAD");
10828
10964
  const coResult = await checkoutBranch(baseBranch);
@@ -10831,7 +10967,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10831
10967
  return "skipped";
10832
10968
  }
10833
10969
  await updateLocalBranch(baseBranch, syncSource.ref);
10834
- success(`Synced ${import_picocolors7.default.bold(baseBranch)} with ${import_picocolors7.default.bold(syncSource.ref)}.`);
10970
+ success(`Synced ${import_picocolors8.default.bold(baseBranch)} with ${import_picocolors8.default.bold(syncSource.ref)}.`);
10835
10971
  return "switched";
10836
10972
  }
10837
10973
  var clean_default = defineCommand({
@@ -10861,7 +10997,7 @@ var clean_default = defineCommand({
10861
10997
  const { origin } = config;
10862
10998
  const baseBranch = getBaseBranch(config);
10863
10999
  let currentBranch = await getCurrentBranch();
10864
- heading("\uD83E\uDDF9 contrib clean");
11000
+ projectHeading("clean", "\uD83E\uDDF9");
10865
11001
  info(`Pruning ${origin} remote refs...`);
10866
11002
  const pruneResult = await pruneRemote(origin);
10867
11003
  if (pruneResult.exitCode === 0) {
@@ -10881,21 +11017,21 @@ var clean_default = defineCommand({
10881
11017
  if (ghInstalled && ghAuthed) {
10882
11018
  const mergedPR = await getMergedPRForBranch(currentBranch);
10883
11019
  if (mergedPR) {
10884
- warn(`PR #${mergedPR.number} (${import_picocolors7.default.bold(mergedPR.title)}) has already been merged.`);
10885
- info(`Link: ${import_picocolors7.default.underline(mergedPR.url)}`, "");
11020
+ warn(`PR #${mergedPR.number} (${import_picocolors8.default.bold(mergedPR.title)}) has already been merged.`);
11021
+ info(`Link: ${import_picocolors8.default.underline(mergedPR.url)}`, "");
10886
11022
  goneCandidates.push(currentBranch);
10887
11023
  }
10888
11024
  }
10889
11025
  }
10890
11026
  if (mergedCandidates.length > 0) {
10891
11027
  console.log(`
10892
- ${import_picocolors7.default.bold("Merged branches to delete:")}`);
11028
+ ${import_picocolors8.default.bold("Merged branches to delete:")}`);
10893
11029
  for (const b2 of mergedCandidates) {
10894
- const marker = b2 === currentBranch ? import_picocolors7.default.yellow(" (current)") : "";
10895
- console.log(` ${import_picocolors7.default.dim("•")} ${b2}${marker}`);
11030
+ const marker = b2 === currentBranch ? import_picocolors8.default.yellow(" (current)") : "";
11031
+ console.log(` ${import_picocolors8.default.dim("•")} ${b2}${marker}`);
10896
11032
  }
10897
11033
  console.log();
10898
- const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors7.default.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
11034
+ const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors8.default.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
10899
11035
  if (ok) {
10900
11036
  for (const branch of mergedCandidates) {
10901
11037
  if (branch === currentBranch) {
@@ -10912,7 +11048,7 @@ ${import_picocolors7.default.bold("Merged branches to delete:")}`);
10912
11048
  }
10913
11049
  const result = await deleteBranch(branch);
10914
11050
  if (result.exitCode === 0) {
10915
- success(` Deleted ${import_picocolors7.default.bold(branch)}`);
11051
+ success(` Deleted ${import_picocolors8.default.bold(branch)}`);
10916
11052
  } else {
10917
11053
  warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
10918
11054
  }
@@ -10923,13 +11059,13 @@ ${import_picocolors7.default.bold("Merged branches to delete:")}`);
10923
11059
  }
10924
11060
  if (goneCandidates.length > 0) {
10925
11061
  console.log(`
10926
- ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash-merged):")}`);
11062
+ ${import_picocolors8.default.bold("Stale branches (remote deleted, likely squash-merged):")}`);
10927
11063
  for (const b2 of goneCandidates) {
10928
- const marker = b2 === currentBranch ? import_picocolors7.default.yellow(" (current)") : "";
10929
- console.log(` ${import_picocolors7.default.dim("•")} ${b2}${marker}`);
11064
+ const marker = b2 === currentBranch ? import_picocolors8.default.yellow(" (current)") : "";
11065
+ console.log(` ${import_picocolors8.default.dim("•")} ${b2}${marker}`);
10930
11066
  }
10931
11067
  console.log();
10932
- const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors7.default.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
11068
+ const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors8.default.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
10933
11069
  if (ok) {
10934
11070
  for (const branch of goneCandidates) {
10935
11071
  if (branch === currentBranch) {
@@ -10946,7 +11082,7 @@ ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash
10946
11082
  }
10947
11083
  const result = await forceDeleteBranch(branch);
10948
11084
  if (result.exitCode === 0) {
10949
- success(` Deleted ${import_picocolors7.default.bold(branch)}`);
11085
+ success(` Deleted ${import_picocolors8.default.bold(branch)}`);
10950
11086
  } else {
10951
11087
  warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
10952
11088
  }
@@ -10961,13 +11097,13 @@ ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash
10961
11097
  const finalBranch = await getCurrentBranch();
10962
11098
  if (finalBranch && protectedBranches.has(finalBranch)) {
10963
11099
  console.log();
10964
- info(`You're on ${import_picocolors7.default.bold(finalBranch)}. Run ${import_picocolors7.default.bold("contrib start")} to begin a new feature.`);
11100
+ info(`You're on ${import_picocolors8.default.bold(finalBranch)}. Run ${import_picocolors8.default.bold("contrib start")} to begin a new feature.`);
10965
11101
  }
10966
11102
  }
10967
11103
  });
10968
11104
 
10969
11105
  // src/commands/commit.ts
10970
- var import_picocolors8 = __toESM(require_picocolors(), 1);
11106
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
10971
11107
 
10972
11108
  // src/utils/convention.ts
10973
11109
  var CLEAN_COMMIT_PATTERN = /^(📦|🔧|🗑\uFE0F?|🔒|⚙\uFE0F?|☕|🧪|📖|🚀) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
@@ -10986,17 +11122,24 @@ var CONVENTION_FORMAT_HINTS = {
10986
11122
  conventional: [
10987
11123
  "Format: <type>[!][(<scope>)]: <description>",
10988
11124
  "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
10989
- "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
11125
+ "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README",
11126
+ "Do not use backticks or markdown formatting in the message."
10990
11127
  ],
10991
11128
  "clean-commit": [
10992
11129
  "Format: <emoji> <type>[!][(<scope>)]: <description>",
10993
11130
  "Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
10994
- "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
11131
+ "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow",
11132
+ "Do not use backticks or markdown formatting in the message."
10995
11133
  ]
10996
11134
  };
11135
+ function hasUnsupportedCommitMessageChars(message) {
11136
+ return message.includes("`");
11137
+ }
10997
11138
  function validateCommitMessage(message, convention) {
10998
11139
  if (convention === "none")
10999
11140
  return true;
11141
+ if (hasUnsupportedCommitMessageChars(message))
11142
+ return false;
11000
11143
  if (convention === "clean-commit")
11001
11144
  return CLEAN_COMMIT_PATTERN.test(message);
11002
11145
  if (convention === "conventional")
@@ -11008,6 +11151,7 @@ function getValidationError(convention) {
11008
11151
  return [];
11009
11152
  return [
11010
11153
  `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
11154
+ "Do not use backticks or markdown formatting in commit messages.",
11011
11155
  ...CONVENTION_FORMAT_HINTS[convention]
11012
11156
  ];
11013
11157
  }
@@ -11045,8 +11189,13 @@ var commit_default = defineCommand({
11045
11189
  error("No .contributerc.json found. Run `contrib setup` first.");
11046
11190
  process.exit(1);
11047
11191
  }
11048
- heading("\uD83D\uDCBE contrib commit");
11192
+ projectHeading("commit", "\uD83D\uDCBE");
11193
+ const aiEnabled = isAIEnabled(config, args["no-ai"]);
11049
11194
  if (args.group) {
11195
+ if (!aiEnabled) {
11196
+ error("AI group commit is unavailable because AI is disabled. Re-run without --group or enable AI in .contributerc.json.");
11197
+ process.exit(1);
11198
+ }
11050
11199
  await runGroupCommit(args.model, config);
11051
11200
  return;
11052
11201
  }
@@ -11058,9 +11207,9 @@ var commit_default = defineCommand({
11058
11207
  process.exit(1);
11059
11208
  }
11060
11209
  console.log(`
11061
- ${import_picocolors8.default.bold("Changed files:")}`);
11210
+ ${import_picocolors9.default.bold("Changed files:")}`);
11062
11211
  for (const f3 of changedFiles) {
11063
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11212
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11064
11213
  }
11065
11214
  const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
11066
11215
  "Stage all changes",
@@ -11102,8 +11251,8 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11102
11251
  const dirs = new Set(stagedFiles.map((f3) => f3.split("/")[0]));
11103
11252
  if (dirs.size > 1) {
11104
11253
  console.log();
11105
- warn(`You're staging ${import_picocolors8.default.bold(String(stagedFiles.length))} files across ${import_picocolors8.default.bold(String(dirs.size))} directories in a single commit.`);
11106
- info(import_picocolors8.default.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
11254
+ warn(`You're staging ${import_picocolors9.default.bold(String(stagedFiles.length))} files across ${import_picocolors9.default.bold(String(dirs.size))} directories in a single commit.`);
11255
+ info(import_picocolors9.default.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
11107
11256
  const choice = await selectPrompt("How would you like to proceed?", [
11108
11257
  "Continue as single commit",
11109
11258
  "Switch to group mode (AI splits into atomic commits)",
@@ -11119,9 +11268,9 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11119
11268
  }
11120
11269
  }
11121
11270
  let commitMessage = null;
11122
- const useAI = !args["no-ai"];
11271
+ const useAI = aiEnabled;
11123
11272
  if (useAI) {
11124
- const [copilotError, diff] = await Promise.all([checkCopilotAvailable(), getStagedDiff()]);
11273
+ const [copilotError, diff] = await Promise.all([checkCopilotAvailable2(), getStagedDiff()]);
11125
11274
  if (copilotError) {
11126
11275
  warn(`AI unavailable: ${copilotError}`);
11127
11276
  warn("Falling back to manual commit message entry.");
@@ -11132,7 +11281,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11132
11281
  if (commitMessage) {
11133
11282
  spinner.success("AI commit message generated.");
11134
11283
  console.log(`
11135
- ${import_picocolors8.default.dim("AI suggestion:")} ${import_picocolors8.default.bold(import_picocolors8.default.cyan(commitMessage))}`);
11284
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(commitMessage))}`);
11136
11285
  } else {
11137
11286
  spinner.fail("AI did not return a commit message.");
11138
11287
  warn("Falling back to manual entry.");
@@ -11158,7 +11307,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11158
11307
  if (regen) {
11159
11308
  spinner.success("Commit message regenerated.");
11160
11309
  console.log(`
11161
- ${import_picocolors8.default.dim("AI suggestion:")} ${import_picocolors8.default.bold(import_picocolors8.default.cyan(regen))}`);
11310
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(regen))}`);
11162
11311
  const ok = await confirmPrompt("Use this message?");
11163
11312
  finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
11164
11313
  } else {
@@ -11173,7 +11322,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11173
11322
  if (convention2 !== "none") {
11174
11323
  console.log();
11175
11324
  for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
11176
- console.log(import_picocolors8.default.dim(hint));
11325
+ console.log(import_picocolors9.default.dim(hint));
11177
11326
  }
11178
11327
  console.log();
11179
11328
  }
@@ -11197,12 +11346,21 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11197
11346
  error(`Failed to commit: ${result.stderr}`);
11198
11347
  process.exit(1);
11199
11348
  }
11200
- success(`Committed: ${import_picocolors8.default.bold(finalMessage)}`);
11349
+ success(`Committed: ${import_picocolors9.default.bold(finalMessage)}`);
11201
11350
  }
11202
11351
  });
11352
+ function getFallbackGroupMessage(convention) {
11353
+ if (convention === "conventional") {
11354
+ return "chore: commit remaining changes";
11355
+ }
11356
+ if (convention === "clean-commit") {
11357
+ return "☕ chore: commit remaining changes";
11358
+ }
11359
+ return "commit remaining changes";
11360
+ }
11203
11361
  async function runGroupCommit(model, config) {
11204
11362
  const [copilotError, changedFiles] = await Promise.all([
11205
- checkCopilotAvailable(),
11363
+ checkCopilotAvailable2(),
11206
11364
  getChangedFiles()
11207
11365
  ]);
11208
11366
  if (copilotError) {
@@ -11214,9 +11372,9 @@ async function runGroupCommit(model, config) {
11214
11372
  process.exit(1);
11215
11373
  }
11216
11374
  console.log(`
11217
- ${import_picocolors8.default.bold("Changed files:")}`);
11375
+ ${import_picocolors9.default.bold("Changed files:")}`);
11218
11376
  for (const f3 of changedFiles) {
11219
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11377
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11220
11378
  }
11221
11379
  const spinner = createSpinner(changedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? `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...`);
11222
11380
  const diffs = await getFullDiffForFiles(changedFiles);
@@ -11237,15 +11395,25 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11237
11395
  error("AI could not produce commit groups. Try committing files manually.");
11238
11396
  process.exit(1);
11239
11397
  }
11240
- const changedSet = new Set(changedFiles);
11241
- for (const group of groups) {
11242
- const invalid = group.files.filter((f3) => !changedSet.has(f3));
11243
- if (invalid.length > 0) {
11244
- warn(`AI suggested unknown file(s): ${invalid.join(", ")} — removed from group.`);
11245
- }
11246
- group.files = group.files.filter((f3) => changedSet.has(f3));
11398
+ const normalized = normalizeCommitGroups(changedFiles, groups);
11399
+ if (normalized.unknownFiles.length > 0) {
11400
+ warn(`AI suggested unknown file(s): ${normalized.unknownFiles.join(", ")} removed from groups.`);
11401
+ }
11402
+ if (normalized.duplicateFiles.length > 0) {
11403
+ warn(`AI assigned duplicate file(s) across groups: ${normalized.duplicateFiles.join(", ")} — keeping the first assignment only.`);
11404
+ }
11405
+ let validGroups = normalized.groups;
11406
+ if (normalized.unassignedFiles.length > 0) {
11407
+ warn(`AI left ${normalized.unassignedFiles.length} file(s) ungrouped: ${normalized.unassignedFiles.join(", ")}. Creating a fallback group.`);
11408
+ const fallbackMessage = await regenerateGroupMessage(normalized.unassignedFiles, diffs, model, config.commitConvention) ?? getFallbackGroupMessage(config.commitConvention);
11409
+ validGroups = [
11410
+ ...validGroups,
11411
+ {
11412
+ files: normalized.unassignedFiles,
11413
+ message: fallbackMessage
11414
+ }
11415
+ ];
11247
11416
  }
11248
- let validGroups = groups.filter((g3) => g3.files.length > 0);
11249
11417
  if (validGroups.length === 0) {
11250
11418
  error("No valid groups remain after validation. Try committing files manually.");
11251
11419
  process.exit(1);
@@ -11254,13 +11422,13 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11254
11422
  let commitAll = false;
11255
11423
  while (!proceedToCommit) {
11256
11424
  console.log(`
11257
- ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
11425
+ ${import_picocolors9.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
11258
11426
  `);
11259
11427
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11260
11428
  const g3 = validGroups[i2];
11261
- console.log(` ${import_picocolors8.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors8.default.bold(g3.message)}`);
11429
+ console.log(` ${import_picocolors9.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors9.default.bold(g3.message)}`);
11262
11430
  for (const f3 of g3.files) {
11263
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11431
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11264
11432
  }
11265
11433
  console.log();
11266
11434
  }
@@ -11291,7 +11459,17 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11291
11459
  if (commitAll) {
11292
11460
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11293
11461
  const group = validGroups[i2];
11294
- const stageResult = await stageFiles(group.files);
11462
+ const remainingChangedFiles = new Set(await getChangedFiles());
11463
+ const stageableFiles = group.files.filter((file) => remainingChangedFiles.has(file));
11464
+ const skippedFiles = group.files.filter((file) => !remainingChangedFiles.has(file));
11465
+ if (skippedFiles.length > 0) {
11466
+ warn(`Group ${i2 + 1} file(s) no longer have changes: ${skippedFiles.join(", ")}`);
11467
+ }
11468
+ if (stageableFiles.length === 0) {
11469
+ warn(`Skipped group ${i2 + 1}: no files remain to commit.`);
11470
+ continue;
11471
+ }
11472
+ const stageResult = await stageFiles(stageableFiles);
11295
11473
  if (stageResult.exitCode !== 0) {
11296
11474
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11297
11475
  continue;
@@ -11300,20 +11478,20 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11300
11478
  if (commitResult.exitCode !== 0) {
11301
11479
  const detail = (commitResult.stderr || commitResult.stdout).trim();
11302
11480
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11303
- await unstageFiles(group.files);
11481
+ await unstageFiles(stageableFiles);
11304
11482
  continue;
11305
11483
  }
11306
11484
  committed++;
11307
- success(`Committed group ${i2 + 1}: ${import_picocolors8.default.bold(group.message)}`);
11485
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(group.message)}`);
11308
11486
  }
11309
11487
  } else {
11310
11488
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11311
11489
  const group = validGroups[i2];
11312
- console.log(import_picocolors8.default.bold(`
11490
+ console.log(import_picocolors9.default.bold(`
11313
11491
  ── Group ${i2 + 1}/${validGroups.length} ──`));
11314
- console.log(` ${import_picocolors8.default.cyan(group.message)}`);
11492
+ console.log(` ${import_picocolors9.default.cyan(group.message)}`);
11315
11493
  for (const f3 of group.files) {
11316
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11494
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11317
11495
  }
11318
11496
  let message = group.message;
11319
11497
  let actionDone = false;
@@ -11335,7 +11513,7 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11335
11513
  if (newMsg) {
11336
11514
  message = newMsg;
11337
11515
  group.message = newMsg;
11338
- regenSpinner.success(`New message: ${import_picocolors8.default.bold(message)}`);
11516
+ regenSpinner.success(`New message: ${import_picocolors9.default.bold(message)}`);
11339
11517
  } else {
11340
11518
  regenSpinner.fail("AI could not generate a new message. Keeping current one.");
11341
11519
  }
@@ -11360,7 +11538,18 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11360
11538
  continue;
11361
11539
  }
11362
11540
  }
11363
- const stageResult = await stageFiles(group.files);
11541
+ const remainingChangedFiles = new Set(await getChangedFiles());
11542
+ const stageableFiles = group.files.filter((file) => remainingChangedFiles.has(file));
11543
+ const skippedFiles = group.files.filter((file) => !remainingChangedFiles.has(file));
11544
+ if (skippedFiles.length > 0) {
11545
+ warn(`Group ${i2 + 1} file(s) no longer have changes: ${skippedFiles.join(", ")}`);
11546
+ }
11547
+ if (stageableFiles.length === 0) {
11548
+ warn(`Skipped group ${i2 + 1}: no files remain to commit.`);
11549
+ actionDone = true;
11550
+ continue;
11551
+ }
11552
+ const stageResult = await stageFiles(stageableFiles);
11364
11553
  if (stageResult.exitCode !== 0) {
11365
11554
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11366
11555
  actionDone = true;
@@ -11370,12 +11559,12 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11370
11559
  if (commitResult.exitCode !== 0) {
11371
11560
  const detail = (commitResult.stderr || commitResult.stdout).trim();
11372
11561
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11373
- await unstageFiles(group.files);
11562
+ await unstageFiles(stageableFiles);
11374
11563
  actionDone = true;
11375
11564
  continue;
11376
11565
  }
11377
11566
  committed++;
11378
- success(`Committed group ${i2 + 1}: ${import_picocolors8.default.bold(message)}`);
11567
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(message)}`);
11379
11568
  actionDone = true;
11380
11569
  }
11381
11570
  }
@@ -11391,11 +11580,11 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11391
11580
 
11392
11581
  // src/commands/doctor.ts
11393
11582
  import { execFile as execFileCb3 } from "node:child_process";
11394
- var import_picocolors9 = __toESM(require_picocolors(), 1);
11583
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
11395
11584
  // package.json
11396
11585
  var package_default = {
11397
11586
  name: "contribute-now",
11398
- version: "0.6.2-dev.38e14f5",
11587
+ version: "0.6.2-dev.967437a",
11399
11588
  description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
11400
11589
  type: "module",
11401
11590
  bin: {
@@ -11486,16 +11675,16 @@ async function getRepoInfoFromRemote(remote = "origin") {
11486
11675
  }
11487
11676
 
11488
11677
  // src/commands/doctor.ts
11489
- var PASS = ` ${import_picocolors9.default.green("✔")} `;
11490
- var FAIL = ` ${import_picocolors9.default.red("✗")} `;
11491
- var WARN = ` ${import_picocolors9.default.yellow("⚠")} `;
11678
+ var PASS = ` ${import_picocolors10.default.green("✔")} `;
11679
+ var FAIL = ` ${import_picocolors10.default.red("✗")} `;
11680
+ var WARN = ` ${import_picocolors10.default.yellow("⚠")} `;
11492
11681
  function printReport(report) {
11493
11682
  for (const section of report.sections) {
11494
11683
  console.log(`
11495
- ${import_picocolors9.default.bold(import_picocolors9.default.underline(section.title))}`);
11684
+ ${import_picocolors10.default.bold(import_picocolors10.default.underline(section.title))}`);
11496
11685
  for (const check of section.checks) {
11497
11686
  const prefix = check.ok ? check.warning ? WARN : PASS : FAIL;
11498
- const text = check.detail ? `${check.label} ${import_picocolors9.default.dim(`— ${check.detail}`)}` : check.label;
11687
+ const text = check.detail ? `${check.label} ${import_picocolors10.default.dim(`— ${check.detail}`)}` : check.label;
11499
11688
  console.log(`${prefix}${text}`);
11500
11689
  }
11501
11690
  }
@@ -11758,20 +11947,20 @@ var doctor_default = defineCommand({
11758
11947
  console.log(toJson(report));
11759
11948
  return;
11760
11949
  }
11761
- heading("\uD83E\uDE7A contribute-now doctor");
11950
+ projectHeading("doctor", "\uD83E\uDE7A");
11762
11951
  printReport(report);
11763
11952
  const total = report.sections.flatMap((s2) => s2.checks);
11764
11953
  const failures = total.filter((c3) => !c3.ok);
11765
11954
  const warnings = total.filter((c3) => c3.ok && c3.warning);
11766
11955
  if (failures.length === 0 && warnings.length === 0) {
11767
- console.log(` ${import_picocolors9.default.green("All checks passed!")} No issues detected.
11956
+ console.log(` ${import_picocolors10.default.green("All checks passed!")} No issues detected.
11768
11957
  `);
11769
11958
  } else {
11770
11959
  if (failures.length > 0) {
11771
- console.log(` ${import_picocolors9.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
11960
+ console.log(` ${import_picocolors10.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
11772
11961
  }
11773
11962
  if (warnings.length > 0) {
11774
- console.log(` ${import_picocolors9.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
11963
+ console.log(` ${import_picocolors10.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
11775
11964
  }
11776
11965
  console.log();
11777
11966
  }
@@ -11781,7 +11970,7 @@ var doctor_default = defineCommand({
11781
11970
  // src/commands/hook.ts
11782
11971
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
11783
11972
  import { join as join4 } from "node:path";
11784
- var import_picocolors10 = __toESM(require_picocolors(), 1);
11973
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
11785
11974
  var HOOK_MARKER = "# managed by contribute-now";
11786
11975
  function getHooksDir(cwd = process.cwd()) {
11787
11976
  return join4(cwd, ".git", "hooks");
@@ -11806,13 +11995,13 @@ esac
11806
11995
 
11807
11996
  # Detect available package runner
11808
11997
  if command -v contrib >/dev/null 2>&1; then
11809
- contrib validate "$commit_msg"
11998
+ contrib validate --file "$commit_msg_file"
11810
11999
  elif command -v bunx >/dev/null 2>&1; then
11811
- bunx contrib validate "$commit_msg"
12000
+ bunx contrib validate --file "$commit_msg_file"
11812
12001
  elif command -v pnpx >/dev/null 2>&1; then
11813
- pnpx contrib validate "$commit_msg"
12002
+ pnpx contrib validate --file "$commit_msg_file"
11814
12003
  elif command -v npx >/dev/null 2>&1; then
11815
- npx contrib validate "$commit_msg"
12004
+ npx contrib validate --file "$commit_msg_file"
11816
12005
  else
11817
12006
  echo "Warning: No package runner found. Skipping commit message validation."
11818
12007
  exit 0
@@ -11849,7 +12038,7 @@ var hook_default = defineCommand({
11849
12038
  }
11850
12039
  });
11851
12040
  async function installHook() {
11852
- heading("\uD83E\uDE9D hook install");
12041
+ projectHeading("hook install", "\uD83E\uDE9D");
11853
12042
  const config = readConfig();
11854
12043
  if (!config) {
11855
12044
  error("No .contributerc.json found. Run `contrib setup` first.");
@@ -11877,12 +12066,12 @@ async function installHook() {
11877
12066
  }
11878
12067
  writeFileSync3(hookPath, generateHookScript(), { mode: 493 });
11879
12068
  success(`commit-msg hook installed.`);
11880
- info(`Convention: ${import_picocolors10.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
11881
- info(`Path: ${import_picocolors10.default.dim(hookPath)}`, "");
12069
+ info(`Convention: ${import_picocolors11.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
12070
+ info(`Path: ${import_picocolors11.default.dim(hookPath)}`, "");
11882
12071
  warn("Note: hooks can be bypassed with `git commit --no-verify`.");
11883
12072
  }
11884
12073
  async function uninstallHook() {
11885
- heading("\uD83E\uDE9D hook uninstall");
12074
+ projectHeading("hook uninstall", "\uD83E\uDE9D");
11886
12075
  const hookPath = getHookPath();
11887
12076
  if (!existsSync4(hookPath)) {
11888
12077
  info("No commit-msg hook found. Nothing to uninstall.");
@@ -11898,7 +12087,7 @@ async function uninstallHook() {
11898
12087
  }
11899
12088
 
11900
12089
  // src/commands/log.ts
11901
- var import_picocolors11 = __toESM(require_picocolors(), 1);
12090
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
11902
12091
  var log_default = defineCommand({
11903
12092
  meta: {
11904
12093
  name: "log",
@@ -11968,14 +12157,14 @@ var log_default = defineCommand({
11968
12157
  usingFallback = true;
11969
12158
  }
11970
12159
  }
11971
- heading("\uD83D\uDCDC commit log");
12160
+ projectHeading("log", "\uD83D\uDCDC");
11972
12161
  printModeHeader(mode, currentBranch, compareRef, usingFallback);
11973
12162
  if (mode === "local" || mode === "remote") {
11974
12163
  if (!compareRef) {
11975
12164
  console.log();
11976
- console.log(import_picocolors11.default.yellow(" ⚠ Could not determine a comparison branch."));
11977
- console.log(import_picocolors11.default.dim(" No upstream tracking set and no remote base branch found."));
11978
- console.log(import_picocolors11.default.dim(` Use ${import_picocolors11.default.bold("contrib log --full")} to see the full commit history instead.`));
12165
+ console.log(import_picocolors12.default.yellow(" ⚠ Could not determine a comparison branch."));
12166
+ console.log(import_picocolors12.default.dim(" No upstream tracking set and no remote base branch found."));
12167
+ console.log(import_picocolors12.default.dim(` Use ${import_picocolors12.default.bold("contrib log --full")} to see the full commit history instead.`));
11979
12168
  console.log();
11980
12169
  printGuidance();
11981
12170
  return;
@@ -12017,26 +12206,26 @@ async function resolveBaseBranchRef(config) {
12017
12206
  }
12018
12207
  function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
12019
12208
  const branch = currentBranch ?? "HEAD";
12020
- const fallbackNote = usingFallback ? import_picocolors11.default.yellow(" (no upstream — comparing against base branch)") : "";
12209
+ const fallbackNote = usingFallback ? import_picocolors12.default.yellow(" (no upstream — comparing against base branch)") : "";
12021
12210
  console.log();
12022
12211
  switch (mode) {
12023
12212
  case "local":
12024
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("local")} — unpushed commits on ${import_picocolors11.default.bold(branch)}`) + fallbackNote);
12213
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("local")} — unpushed commits on ${import_picocolors12.default.bold(branch)}`) + fallbackNote);
12025
12214
  if (compareRef) {
12026
- console.log(import_picocolors11.default.dim(` comparing: ${import_picocolors11.default.bold(compareRef)} ➜ ${import_picocolors11.default.bold("HEAD")}`));
12215
+ console.log(import_picocolors12.default.dim(` comparing: ${import_picocolors12.default.bold(compareRef)} ➜ ${import_picocolors12.default.bold("HEAD")}`));
12027
12216
  }
12028
12217
  break;
12029
12218
  case "remote":
12030
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("remote")} — commits on remote not yet pulled into ${import_picocolors11.default.bold(branch)}`) + fallbackNote);
12219
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("remote")} — commits on remote not yet pulled into ${import_picocolors12.default.bold(branch)}`) + fallbackNote);
12031
12220
  if (compareRef) {
12032
- console.log(import_picocolors11.default.dim(` comparing: ${import_picocolors11.default.bold("HEAD")} ➜ ${import_picocolors11.default.bold(compareRef)}`));
12221
+ console.log(import_picocolors12.default.dim(` comparing: ${import_picocolors12.default.bold("HEAD")} ➜ ${import_picocolors12.default.bold(compareRef)}`));
12033
12222
  }
12034
12223
  break;
12035
12224
  case "full":
12036
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("full")} — complete commit history for ${import_picocolors11.default.bold(branch)}`));
12225
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("full")} — complete commit history for ${import_picocolors12.default.bold(branch)}`));
12037
12226
  break;
12038
12227
  case "all":
12039
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("all")} — commits across all branches`));
12228
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("all")} — commits across all branches`));
12040
12229
  break;
12041
12230
  }
12042
12231
  }
@@ -12062,7 +12251,7 @@ async function renderScopedLog(options) {
12062
12251
  }
12063
12252
  console.log();
12064
12253
  for (const entry of entries) {
12065
- const hashStr = import_picocolors11.default.yellow(entry.hash);
12254
+ const hashStr = import_picocolors12.default.yellow(entry.hash);
12066
12255
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
12067
12256
  const subjectStr = colorizeSubject(entry.subject);
12068
12257
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -12073,9 +12262,9 @@ async function renderScopedLog(options) {
12073
12262
  function printEmptyState(mode) {
12074
12263
  console.log();
12075
12264
  if (mode === "local") {
12076
- console.log(import_picocolors11.default.dim(" No local unpushed commits — you're up to date with remote!"));
12265
+ console.log(import_picocolors12.default.dim(" No local unpushed commits — you're up to date with remote!"));
12077
12266
  } else {
12078
- console.log(import_picocolors11.default.dim(" No remote-only commits — your local branch is up to date!"));
12267
+ console.log(import_picocolors12.default.dim(" No remote-only commits — your local branch is up to date!"));
12079
12268
  }
12080
12269
  console.log();
12081
12270
  }
@@ -12084,7 +12273,7 @@ async function renderFullLog(options) {
12084
12273
  if (showGraph) {
12085
12274
  const lines = await getLogGraph({ count, all, branch: targetBranch });
12086
12275
  if (lines.length === 0) {
12087
- console.log(import_picocolors11.default.dim(" No commits found."));
12276
+ console.log(import_picocolors12.default.dim(" No commits found."));
12088
12277
  console.log();
12089
12278
  return false;
12090
12279
  }
@@ -12095,13 +12284,13 @@ async function renderFullLog(options) {
12095
12284
  } else {
12096
12285
  const entries = await getLogEntries({ count, all, branch: targetBranch });
12097
12286
  if (entries.length === 0) {
12098
- console.log(import_picocolors11.default.dim(" No commits found."));
12287
+ console.log(import_picocolors12.default.dim(" No commits found."));
12099
12288
  console.log();
12100
12289
  return false;
12101
12290
  }
12102
12291
  console.log();
12103
12292
  for (const entry of entries) {
12104
- const hashStr = import_picocolors11.default.yellow(entry.hash);
12293
+ const hashStr = import_picocolors12.default.yellow(entry.hash);
12105
12294
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
12106
12295
  const subjectStr = colorizeSubject(entry.subject);
12107
12296
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -12113,42 +12302,42 @@ function printFooter(mode, count, targetBranch) {
12113
12302
  console.log();
12114
12303
  switch (mode) {
12115
12304
  case "local":
12116
- console.log(import_picocolors11.default.dim(` Showing up to ${count} unpushed commits`));
12305
+ console.log(import_picocolors12.default.dim(` Showing up to ${count} unpushed commits`));
12117
12306
  break;
12118
12307
  case "remote":
12119
- console.log(import_picocolors11.default.dim(` Showing up to ${count} remote-only commits`));
12308
+ console.log(import_picocolors12.default.dim(` Showing up to ${count} remote-only commits`));
12120
12309
  break;
12121
12310
  case "full":
12122
- console.log(import_picocolors11.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
12311
+ console.log(import_picocolors12.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
12123
12312
  break;
12124
12313
  case "all":
12125
- console.log(import_picocolors11.default.dim(` Showing ${count} most recent commits (all branches)`));
12314
+ console.log(import_picocolors12.default.dim(` Showing ${count} most recent commits (all branches)`));
12126
12315
  break;
12127
12316
  }
12128
12317
  }
12129
12318
  function printGuidance() {
12130
12319
  console.log();
12131
- console.log(import_picocolors11.default.dim(" ─── quick guide ───"));
12132
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log")} local unpushed commits (default)`));
12133
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --remote")} commits on remote not yet pulled`));
12134
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --full")} full history for the current branch`));
12135
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --all")} commits across all branches`));
12136
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log -n 50")} change the commit limit (default: 20)`));
12137
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log -b dev")} view log for a specific branch`));
12138
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --no-graph")} flat list without graph lines`));
12320
+ console.log(import_picocolors12.default.dim(" ─── quick guide ───"));
12321
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log")} local unpushed commits (default)`));
12322
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log --remote")} commits on remote not yet pulled`));
12323
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log --full")} full history for the current branch`));
12324
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log --all")} commits across all branches`));
12325
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log -n 50")} change the commit limit (default: 20)`));
12326
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log -b dev")} view log for a specific branch`));
12327
+ console.log(import_picocolors12.default.dim(` ${import_picocolors12.default.bold("contrib log --no-graph")} flat list without graph lines`));
12139
12328
  console.log();
12140
12329
  }
12141
12330
  function colorizeGraphLine(line, protectedBranches, currentBranch) {
12142
12331
  const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
12143
12332
  if (!match) {
12144
- return import_picocolors11.default.cyan(line);
12333
+ return import_picocolors12.default.cyan(line);
12145
12334
  }
12146
12335
  const [, graphPart = "", hash, , refs, subject = ""] = match;
12147
12336
  const parts = [];
12148
12337
  if (graphPart) {
12149
12338
  parts.push(colorizeGraphChars(graphPart));
12150
12339
  }
12151
- parts.push(import_picocolors11.default.yellow(hash));
12340
+ parts.push(import_picocolors12.default.yellow(hash));
12152
12341
  if (refs) {
12153
12342
  parts.push(` (${colorizeRefs(refs, protectedBranches, currentBranch)})`);
12154
12343
  }
@@ -12159,15 +12348,15 @@ function colorizeGraphChars(graphPart) {
12159
12348
  return graphPart.split("").map((ch) => {
12160
12349
  switch (ch) {
12161
12350
  case "*":
12162
- return import_picocolors11.default.green(ch);
12351
+ return import_picocolors12.default.green(ch);
12163
12352
  case "|":
12164
- return import_picocolors11.default.cyan(ch);
12353
+ return import_picocolors12.default.cyan(ch);
12165
12354
  case "/":
12166
12355
  case "\\":
12167
- return import_picocolors11.default.cyan(ch);
12356
+ return import_picocolors12.default.cyan(ch);
12168
12357
  case "-":
12169
12358
  case "_":
12170
- return import_picocolors11.default.cyan(ch);
12359
+ return import_picocolors12.default.cyan(ch);
12171
12360
  default:
12172
12361
  return ch;
12173
12362
  }
@@ -12179,45 +12368,45 @@ function colorizeRefs(refs, protectedBranches, currentBranch) {
12179
12368
  if (trimmed.startsWith("HEAD ->") || trimmed === "HEAD") {
12180
12369
  const branchName = trimmed.replace("HEAD -> ", "");
12181
12370
  if (trimmed === "HEAD") {
12182
- return import_picocolors11.default.bold(import_picocolors11.default.cyan("HEAD"));
12371
+ return import_picocolors12.default.bold(import_picocolors12.default.cyan("HEAD"));
12183
12372
  }
12184
- return `${import_picocolors11.default.bold(import_picocolors11.default.cyan("HEAD"))} ${import_picocolors11.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
12373
+ return `${import_picocolors12.default.bold(import_picocolors12.default.cyan("HEAD"))} ${import_picocolors12.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
12185
12374
  }
12186
12375
  if (trimmed.startsWith("tag:")) {
12187
- return import_picocolors11.default.bold(import_picocolors11.default.magenta(trimmed));
12376
+ return import_picocolors12.default.bold(import_picocolors12.default.magenta(trimmed));
12188
12377
  }
12189
12378
  return colorizeRefName(trimmed, protectedBranches, currentBranch);
12190
- }).join(import_picocolors11.default.dim(", "));
12379
+ }).join(import_picocolors12.default.dim(", "));
12191
12380
  }
12192
12381
  function colorizeRefName(name, protectedBranches, currentBranch) {
12193
12382
  const isRemote = name.includes("/");
12194
12383
  const localName = isRemote ? name.split("/").slice(1).join("/") : name;
12195
12384
  if (protectedBranches.includes(localName)) {
12196
- return isRemote ? import_picocolors11.default.bold(import_picocolors11.default.red(name)) : import_picocolors11.default.bold(import_picocolors11.default.red(name));
12385
+ return isRemote ? import_picocolors12.default.bold(import_picocolors12.default.red(name)) : import_picocolors12.default.bold(import_picocolors12.default.red(name));
12197
12386
  }
12198
12387
  if (localName === currentBranch) {
12199
- return import_picocolors11.default.bold(import_picocolors11.default.green(name));
12388
+ return import_picocolors12.default.bold(import_picocolors12.default.green(name));
12200
12389
  }
12201
12390
  if (isRemote) {
12202
- return import_picocolors11.default.blue(name);
12391
+ return import_picocolors12.default.blue(name);
12203
12392
  }
12204
- return import_picocolors11.default.green(name);
12393
+ return import_picocolors12.default.green(name);
12205
12394
  }
12206
12395
  function colorizeSubject(subject) {
12207
12396
  const emojiMatch = subject.match(/^((?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+\s*)/u);
12208
12397
  if (emojiMatch) {
12209
12398
  const emoji = emojiMatch[1];
12210
12399
  const rest = subject.slice(emoji.length);
12211
- return `${emoji}${import_picocolors11.default.white(rest)}`;
12400
+ return `${emoji}${import_picocolors12.default.white(rest)}`;
12212
12401
  }
12213
12402
  if (subject.startsWith("Merge ")) {
12214
- return import_picocolors11.default.dim(subject);
12403
+ return import_picocolors12.default.dim(subject);
12215
12404
  }
12216
- return import_picocolors11.default.white(subject);
12405
+ return import_picocolors12.default.white(subject);
12217
12406
  }
12218
12407
 
12219
12408
  // src/commands/save.ts
12220
- var import_picocolors12 = __toESM(require_picocolors(), 1);
12409
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
12221
12410
  import { execFile as execFileCb4 } from "node:child_process";
12222
12411
  function gitRun(args) {
12223
12412
  return new Promise((resolve2) => {
@@ -12279,7 +12468,7 @@ var save_default = defineCommand({
12279
12468
  }
12280
12469
  });
12281
12470
  async function handleSave(message) {
12282
- heading("\uD83D\uDCBE contrib save");
12471
+ projectHeading("save", "\uD83D\uDCBE");
12283
12472
  const currentBranch = await getCurrentBranch();
12284
12473
  const label = message ?? `work-in-progress on ${currentBranch ?? "unknown"}`;
12285
12474
  const stashMsg = `contrib-save: ${label}`;
@@ -12292,11 +12481,11 @@ async function handleSave(message) {
12292
12481
  info("No uncommitted changes to save.");
12293
12482
  return;
12294
12483
  }
12295
- success(`Saved: ${import_picocolors12.default.dim(label)}`);
12296
- info(`Use ${import_picocolors12.default.bold("contrib save --restore")} to bring them back.`, "");
12484
+ success(`Saved: ${import_picocolors13.default.dim(label)}`);
12485
+ info(`Use ${import_picocolors13.default.bold("contrib save --restore")} to bring them back.`, "");
12297
12486
  }
12298
12487
  async function handleRestore() {
12299
- heading("\uD83D\uDCBE contrib save --restore");
12488
+ projectHeading("save --restore", "\uD83D\uDCBE");
12300
12489
  const stashes = await getStashList();
12301
12490
  if (stashes.length === 0) {
12302
12491
  info("No saved changes found.");
@@ -12309,7 +12498,7 @@ async function handleRestore() {
12309
12498
  warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
12310
12499
  process.exit(1);
12311
12500
  }
12312
- success(`Restored: ${import_picocolors12.default.dim(stashes[0].message)}`);
12501
+ success(`Restored: ${import_picocolors13.default.dim(stashes[0].message)}`);
12313
12502
  return;
12314
12503
  }
12315
12504
  const choices = stashes.map((s2) => `${s2.index} ${s2.message}`);
@@ -12322,10 +12511,10 @@ async function handleRestore() {
12322
12511
  process.exit(1);
12323
12512
  }
12324
12513
  const match = stashes.find((s2) => String(s2.index) === idx);
12325
- success(`Restored: ${import_picocolors12.default.dim(match?.message ?? "saved changes")}`);
12514
+ success(`Restored: ${import_picocolors13.default.dim(match?.message ?? "saved changes")}`);
12326
12515
  }
12327
12516
  async function handleList() {
12328
- heading("\uD83D\uDCBE contrib save --list");
12517
+ projectHeading("save --list", "\uD83D\uDCBE");
12329
12518
  const stashes = await getStashList();
12330
12519
  if (stashes.length === 0) {
12331
12520
  info("No saved changes.");
@@ -12333,16 +12522,16 @@ async function handleList() {
12333
12522
  }
12334
12523
  console.log();
12335
12524
  for (const s2 of stashes) {
12336
- const idx = import_picocolors12.default.dim(`[${s2.index}]`);
12525
+ const idx = import_picocolors13.default.dim(`[${s2.index}]`);
12337
12526
  const msg = s2.message;
12338
12527
  console.log(` ${idx} ${msg}`);
12339
12528
  }
12340
12529
  console.log();
12341
- info(`Use ${import_picocolors12.default.bold("contrib save --restore")} to bring changes back.`, "");
12342
- info(`Use ${import_picocolors12.default.bold("contrib save --drop")} to discard saved changes.`, "");
12530
+ info(`Use ${import_picocolors13.default.bold("contrib save --restore")} to bring changes back.`, "");
12531
+ info(`Use ${import_picocolors13.default.bold("contrib save --drop")} to discard saved changes.`, "");
12343
12532
  }
12344
12533
  async function handleDrop() {
12345
- heading("\uD83D\uDCBE contrib save --drop");
12534
+ projectHeading("save --drop", "\uD83D\uDCBE");
12346
12535
  const stashes = await getStashList();
12347
12536
  if (stashes.length === 0) {
12348
12537
  info("No saved changes to drop.");
@@ -12357,7 +12546,7 @@ async function handleDrop() {
12357
12546
  process.exit(1);
12358
12547
  }
12359
12548
  const match = stashes.find((s2) => String(s2.index) === idx);
12360
- success(`Dropped: ${import_picocolors12.default.dim(match?.message ?? "saved changes")}`);
12549
+ success(`Dropped: ${import_picocolors13.default.dim(match?.message ?? "saved changes")}`);
12361
12550
  }
12362
12551
  async function getStashList() {
12363
12552
  const result = await gitRun(["stash", "list"]);
@@ -12374,7 +12563,7 @@ async function getStashList() {
12374
12563
  }
12375
12564
 
12376
12565
  // src/commands/setup.ts
12377
- var import_picocolors13 = __toESM(require_picocolors(), 1);
12566
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
12378
12567
  async function shouldContinueSetupWithExistingConfig(options) {
12379
12568
  const {
12380
12569
  existingConfig,
@@ -12422,7 +12611,7 @@ var setup_default = defineCommand({
12422
12611
  error("Not inside a git repository. Run this command from within a git repo.");
12423
12612
  process.exit(1);
12424
12613
  }
12425
- heading("\uD83D\uDD27 contribute-now setup");
12614
+ projectHeading("setup", "\uD83D\uDD27");
12426
12615
  const existingConfig = readConfig();
12427
12616
  const shouldContinue = await shouldContinueSetupWithExistingConfig({
12428
12617
  existingConfig,
@@ -12447,7 +12636,7 @@ var setup_default = defineCommand({
12447
12636
  workflow = "github-flow";
12448
12637
  else if (workflowChoice.startsWith("Git Flow"))
12449
12638
  workflow = "git-flow";
12450
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
12639
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
12451
12640
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
12452
12641
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
12453
12642
  CONVENTION_DESCRIPTIONS.conventional,
@@ -12458,6 +12647,7 @@ var setup_default = defineCommand({
12458
12647
  commitConvention = "conventional";
12459
12648
  else if (conventionChoice.includes("No commit"))
12460
12649
  commitConvention = "none";
12650
+ const enableAI = await confirmPrompt("Enable AI-assisted features like commit messages, branch naming, PR text, and conflict guidance?");
12461
12651
  const remotes = await getRemotes();
12462
12652
  if (remotes.length === 0) {
12463
12653
  error("No git remotes found. Add a remote first (e.g., git remote add origin <url>).");
@@ -12511,15 +12701,15 @@ var setup_default = defineCommand({
12511
12701
  detectedRole = roleChoice;
12512
12702
  detectionSource = "user selection";
12513
12703
  } else {
12514
- info(`Detected role: ${import_picocolors13.default.bold(detectedRole)} (via ${detectionSource})`);
12515
- const confirmed = await confirmPrompt(`Role detected as ${import_picocolors13.default.bold(detectedRole)}. Is this correct?`);
12704
+ info(`Detected role: ${import_picocolors14.default.bold(detectedRole)} (via ${detectionSource})`);
12705
+ const confirmed = await confirmPrompt(`Role detected as ${import_picocolors14.default.bold(detectedRole)}. Is this correct?`);
12516
12706
  if (!confirmed) {
12517
12707
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
12518
12708
  detectedRole = roleChoice;
12519
12709
  }
12520
12710
  }
12521
12711
  const defaultConfig = getDefaultConfig();
12522
- info(import_picocolors13.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
12712
+ info(import_picocolors14.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
12523
12713
  const mainBranchDefault = defaultConfig.mainBranch;
12524
12714
  const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
12525
12715
  let devBranch;
@@ -12545,7 +12735,7 @@ var setup_default = defineCommand({
12545
12735
  error("Setup cannot continue without the upstream remote for contributors.");
12546
12736
  process.exit(1);
12547
12737
  }
12548
- success(`Added remote ${import_picocolors13.default.bold(upstreamRemote)} → ${upstreamUrl}`);
12738
+ success(`Added remote ${import_picocolors14.default.bold(upstreamRemote)} → ${upstreamUrl}`);
12549
12739
  } else {
12550
12740
  error("An upstream remote URL is required for contributors.");
12551
12741
  info("Add it manually: git remote add upstream <url>", "");
@@ -12561,22 +12751,23 @@ var setup_default = defineCommand({
12561
12751
  upstream: upstreamRemote,
12562
12752
  origin: originRemote,
12563
12753
  branchPrefixes: defaultConfig.branchPrefixes,
12564
- commitConvention
12754
+ commitConvention,
12755
+ aiEnabled: enableAI
12565
12756
  };
12566
12757
  writeConfig(config);
12567
12758
  success(`Config written to .contributerc.json`);
12568
12759
  const syncRemote = config.role === "contributor" ? config.upstream : config.origin;
12569
- info(`Fetching ${import_picocolors13.default.bold(syncRemote)} to verify branch configuration...`, "");
12760
+ info(`Fetching ${import_picocolors14.default.bold(syncRemote)} to verify branch configuration...`, "");
12570
12761
  await fetchRemote(syncRemote);
12571
12762
  const mainRef = `${syncRemote}/${config.mainBranch}`;
12572
12763
  if (!await refExists(mainRef)) {
12573
- warn(`Main branch ref ${import_picocolors13.default.bold(mainRef)} not found on remote.`);
12764
+ warn(`Main branch ref ${import_picocolors14.default.bold(mainRef)} not found on remote.`);
12574
12765
  warn("Config was saved — verify the branch name and re-run setup if needed.");
12575
12766
  }
12576
12767
  if (config.devBranch) {
12577
12768
  const devRef = `${syncRemote}/${config.devBranch}`;
12578
12769
  if (!await refExists(devRef)) {
12579
- warn(`Dev branch ref ${import_picocolors13.default.bold(devRef)} not found on remote.`);
12770
+ warn(`Dev branch ref ${import_picocolors14.default.bold(devRef)} not found on remote.`);
12580
12771
  warn("Config was saved — verify the branch name and re-run setup if needed.");
12581
12772
  }
12582
12773
  }
@@ -12584,31 +12775,33 @@ var setup_default = defineCommand({
12584
12775
  info("Added .contributerc.json to .gitignore to avoid committing personal config.");
12585
12776
  }
12586
12777
  console.log();
12587
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12588
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12589
- info(`Role: ${import_picocolors13.default.bold(config.role)}`);
12778
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12779
+ info(`Convention: ${import_picocolors14.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12780
+ info(`AI: ${import_picocolors14.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
12781
+ info(`Role: ${import_picocolors14.default.bold(config.role)}`);
12590
12782
  if (config.devBranch) {
12591
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)} | Dev: ${import_picocolors13.default.bold(config.devBranch)}`);
12783
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)} | Dev: ${import_picocolors14.default.bold(config.devBranch)}`);
12592
12784
  } else {
12593
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)}`);
12785
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)}`);
12594
12786
  }
12595
- info(`Origin: ${import_picocolors13.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors13.default.bold(config.upstream)}` : ""}`);
12787
+ info(`Origin: ${import_picocolors14.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors14.default.bold(config.upstream)}` : ""}`);
12596
12788
  }
12597
12789
  });
12598
12790
  function logConfigSummary(config) {
12599
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12600
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12601
- info(`Role: ${import_picocolors13.default.bold(config.role)}`);
12791
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12792
+ info(`Convention: ${import_picocolors14.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12793
+ info(`AI: ${import_picocolors14.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
12794
+ info(`Role: ${import_picocolors14.default.bold(config.role)}`);
12602
12795
  if (config.devBranch) {
12603
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)} | Dev: ${import_picocolors13.default.bold(config.devBranch)}`);
12796
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)} | Dev: ${import_picocolors14.default.bold(config.devBranch)}`);
12604
12797
  } else {
12605
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)}`);
12798
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)}`);
12606
12799
  }
12607
- info(`Origin: ${import_picocolors13.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors13.default.bold(config.upstream)}` : ""}`);
12800
+ info(`Origin: ${import_picocolors14.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors14.default.bold(config.upstream)}` : ""}`);
12608
12801
  }
12609
12802
 
12610
12803
  // src/commands/start.ts
12611
- var import_picocolors14 = __toESM(require_picocolors(), 1);
12804
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
12612
12805
  var start_default = defineCommand({
12613
12806
  meta: {
12614
12807
  name: "start",
@@ -12648,57 +12841,28 @@ var start_default = defineCommand({
12648
12841
  const { branchPrefixes } = config;
12649
12842
  const baseBranch = getBaseBranch(config);
12650
12843
  const syncSource = getSyncSource(config);
12651
- let branchName = args.name;
12652
- heading("\uD83C\uDF3F contrib start");
12844
+ let branchName = args.name?.trim();
12845
+ projectHeading("start", "\uD83C\uDF3F");
12846
+ branchName = await promptForBranchName({
12847
+ initialValue: branchName,
12848
+ branchPrefixes,
12849
+ useAI: isAIEnabled(config, args["no-ai"]),
12850
+ model: args.model
12851
+ });
12653
12852
  if (!branchName) {
12654
- branchName = await inputPrompt("What are you going to work on?");
12655
- if (!branchName || branchName.trim().length === 0) {
12656
- error("A branch name or description is required.");
12657
- process.exit(1);
12658
- }
12659
- branchName = branchName.trim();
12660
- }
12661
- const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
12662
- if (useAI) {
12663
- const spinner = createSpinner("Generating branch name suggestion...");
12664
- const suggested = await suggestBranchName(branchName, args.model);
12665
- if (suggested) {
12666
- spinner.success("Branch name suggestion ready.");
12667
- console.log(`
12668
- ${import_picocolors14.default.dim("AI suggestion:")} ${import_picocolors14.default.bold(import_picocolors14.default.cyan(suggested))}`);
12669
- const accepted = await confirmPrompt(`Use ${import_picocolors14.default.bold(suggested)} as your branch name?`);
12670
- if (accepted) {
12671
- branchName = suggested;
12672
- } else {
12673
- branchName = await inputPrompt("Enter branch name", branchName);
12674
- }
12675
- } else {
12676
- spinner.fail("AI did not return a branch name suggestion.");
12677
- }
12678
- }
12679
- if (!hasPrefix(branchName, branchPrefixes)) {
12680
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors14.default.bold(branchName)}:`, branchPrefixes);
12681
- branchName = formatBranchName(prefix, branchName);
12682
- }
12683
- if (!isValidBranchName(branchName)) {
12684
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
12685
- process.exit(1);
12686
- }
12687
- info(`Creating branch: ${import_picocolors14.default.bold(branchName)}`);
12688
- if (await branchExists(branchName)) {
12689
- error(`Branch ${import_picocolors14.default.bold(branchName)} already exists.`);
12690
- info(` Use ${import_picocolors14.default.bold(`git checkout ${branchName}`)} to switch to it, or choose a different name.`, "");
12691
- process.exit(1);
12853
+ warn("Start cancelled.");
12854
+ process.exit(0);
12692
12855
  }
12856
+ info(`Creating branch: ${import_picocolors15.default.bold(branchName)}`);
12693
12857
  await fetchRemote(syncSource.remote);
12694
12858
  if (!await refExists(syncSource.ref)) {
12695
- warn(`Remote ref ${import_picocolors14.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors14.default.bold(baseBranch)}.`);
12859
+ warn(`Remote ref ${import_picocolors15.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors15.default.bold(baseBranch)}.`);
12696
12860
  }
12697
12861
  const currentBranch = await getCurrentBranch();
12698
12862
  if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
12699
12863
  const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
12700
12864
  if (ahead > 0) {
12701
- warn(`You are on ${import_picocolors14.default.bold(baseBranch)} with ${import_picocolors14.default.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${import_picocolors14.default.bold(syncSource.ref)}.`);
12865
+ warn(`You are on ${import_picocolors15.default.bold(baseBranch)} with ${import_picocolors15.default.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${import_picocolors15.default.bold(syncSource.ref)}.`);
12702
12866
  info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
12703
12867
  const proceed = await confirmPrompt("Discard local commits and sync to remote?");
12704
12868
  if (!proceed) {
@@ -12715,10 +12879,10 @@ var start_default = defineCommand({
12715
12879
  error(`Failed to create branch: ${result2.stderr}`);
12716
12880
  process.exit(1);
12717
12881
  }
12718
- success(`Created ${import_picocolors14.default.bold(branchName)} from ${import_picocolors14.default.bold(syncSource.ref)}`);
12882
+ success(`Created ${import_picocolors15.default.bold(branchName)} from ${import_picocolors15.default.bold(syncSource.ref)}`);
12719
12883
  return;
12720
12884
  }
12721
- error(`Failed to update ${import_picocolors14.default.bold(baseBranch)}: ${updateResult.stderr}`);
12885
+ error(`Failed to update ${import_picocolors15.default.bold(baseBranch)}: ${updateResult.stderr}`);
12722
12886
  info("Make sure your base branch exists locally or the remote ref is available.", "");
12723
12887
  process.exit(1);
12724
12888
  }
@@ -12727,12 +12891,12 @@ var start_default = defineCommand({
12727
12891
  error(`Failed to create branch: ${result.stderr}`);
12728
12892
  process.exit(1);
12729
12893
  }
12730
- success(`Created ${import_picocolors14.default.bold(branchName)} from latest ${import_picocolors14.default.bold(baseBranch)}`);
12894
+ success(`Created ${import_picocolors15.default.bold(branchName)} from latest ${import_picocolors15.default.bold(baseBranch)}`);
12731
12895
  }
12732
12896
  });
12733
12897
 
12734
12898
  // src/commands/status.ts
12735
- var import_picocolors15 = __toESM(require_picocolors(), 1);
12899
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
12736
12900
  var status_default = defineCommand({
12737
12901
  meta: {
12738
12902
  name: "status",
@@ -12748,9 +12912,9 @@ var status_default = defineCommand({
12748
12912
  error("No .contributerc.json found. Run `contrib setup` first.");
12749
12913
  process.exit(1);
12750
12914
  }
12751
- heading("\uD83D\uDCCA contribute-now status");
12752
- console.log(` ${import_picocolors15.default.dim("Workflow:")} ${import_picocolors15.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12753
- console.log(` ${import_picocolors15.default.dim("Role:")} ${import_picocolors15.default.bold(config.role)}`);
12915
+ projectHeading("status", "\uD83D\uDCCA");
12916
+ console.log(` ${import_picocolors16.default.dim("Workflow:")} ${import_picocolors16.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12917
+ console.log(` ${import_picocolors16.default.dim("Role:")} ${import_picocolors16.default.bold(config.role)}`);
12754
12918
  console.log();
12755
12919
  await fetchAll();
12756
12920
  const currentBranch = await getCurrentBranch();
@@ -12759,7 +12923,7 @@ var status_default = defineCommand({
12759
12923
  const isContributor = config.role === "contributor";
12760
12924
  const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
12761
12925
  if (dirty) {
12762
- console.log(` ${import_picocolors15.default.yellow("⚠")} ${import_picocolors15.default.yellow("Uncommitted changes in working tree")}`);
12926
+ console.log(` ${import_picocolors16.default.yellow("⚠")} ${import_picocolors16.default.yellow("Uncommitted changes in working tree")}`);
12763
12927
  console.log();
12764
12928
  }
12765
12929
  const mainRemote = `${origin}/${mainBranch}`;
@@ -12778,16 +12942,16 @@ var status_default = defineCommand({
12778
12942
  if (isFeatureBranch) {
12779
12943
  const branchDiv = await getDivergence(currentBranch, baseBranch);
12780
12944
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
12781
- console.log(branchLine + import_picocolors15.default.dim(` (current ${import_picocolors15.default.green("*")})`));
12945
+ console.log(branchLine + import_picocolors16.default.dim(` (current ${import_picocolors16.default.green("*")})`));
12782
12946
  branchStatus = await detectBranchStatus(currentBranch, baseBranch);
12783
12947
  if (branchStatus.merged) {
12784
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.green("Branch merged")} — ${import_picocolors15.default.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
12948
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.green("Branch merged")} — ${import_picocolors16.default.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
12785
12949
  }
12786
12950
  if (branchStatus.stale) {
12787
- console.log(` ${import_picocolors15.default.yellow("⏳")} ${import_picocolors15.default.yellow("Branch is stale")} — ${import_picocolors15.default.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
12951
+ console.log(` ${import_picocolors16.default.yellow("⏳")} ${import_picocolors16.default.yellow("Branch is stale")} — ${import_picocolors16.default.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
12788
12952
  }
12789
12953
  } else if (currentBranch) {
12790
- console.log(import_picocolors15.default.dim(` (on ${import_picocolors15.default.bold(currentBranch)} branch)`));
12954
+ console.log(import_picocolors16.default.dim(` (on ${import_picocolors16.default.bold(currentBranch)} branch)`));
12791
12955
  }
12792
12956
  let branchesAligned = true;
12793
12957
  {
@@ -12817,20 +12981,20 @@ var status_default = defineCommand({
12817
12981
  }
12818
12982
  branchesAligned = groups.size === 1;
12819
12983
  console.log();
12820
- console.log(` ${import_picocolors15.default.bold("\uD83D\uDD17 Branch Alignment")}`);
12984
+ console.log(` ${import_picocolors16.default.bold("\uD83D\uDD17 Branch Alignment")}`);
12821
12985
  for (const [hash, names] of groups) {
12822
12986
  const short = hash.slice(0, 7);
12823
- const nameStr = names.map((n2) => import_picocolors15.default.bold(n2)).join(import_picocolors15.default.dim(" · "));
12824
- console.log(` ${import_picocolors15.default.yellow(short)} ${import_picocolors15.default.dim("──")} ${nameStr}`);
12987
+ const nameStr = names.map((n2) => import_picocolors16.default.bold(n2)).join(import_picocolors16.default.dim(" · "));
12988
+ console.log(` ${import_picocolors16.default.yellow(short)} ${import_picocolors16.default.dim("──")} ${nameStr}`);
12825
12989
  const subject = await getCommitSubject(hash);
12826
12990
  if (subject) {
12827
- console.log(` ${import_picocolors15.default.dim(subject)}`);
12991
+ console.log(` ${import_picocolors16.default.dim(subject)}`);
12828
12992
  }
12829
12993
  }
12830
12994
  if (branchesAligned) {
12831
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.green("All branches aligned")} ${import_picocolors15.default.dim("— ready to start")}`);
12995
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.green("All branches aligned")} ${import_picocolors16.default.dim("— ready to start")}`);
12832
12996
  } else {
12833
- console.log(` ${import_picocolors15.default.yellow("⚠")} ${import_picocolors15.default.yellow("Branches are not fully aligned")}`);
12997
+ console.log(` ${import_picocolors16.default.yellow("⚠")} ${import_picocolors16.default.yellow("Branches are not fully aligned")}`);
12834
12998
  }
12835
12999
  }
12836
13000
  }
@@ -12838,70 +13002,70 @@ var status_default = defineCommand({
12838
13002
  if (hasFiles) {
12839
13003
  console.log();
12840
13004
  if (fileStatus.staged.length > 0) {
12841
- console.log(` ${import_picocolors15.default.green("Staged for commit:")}`);
13005
+ console.log(` ${import_picocolors16.default.green("Staged for commit:")}`);
12842
13006
  for (const { file, status } of fileStatus.staged) {
12843
- console.log(` ${import_picocolors15.default.green("+")} ${import_picocolors15.default.dim(`${status}:`)} ${file}`);
13007
+ console.log(` ${import_picocolors16.default.green("+")} ${import_picocolors16.default.dim(`${status}:`)} ${file}`);
12844
13008
  }
12845
13009
  }
12846
13010
  if (fileStatus.modified.length > 0) {
12847
- console.log(` ${import_picocolors15.default.yellow("Unstaged changes:")}`);
13011
+ console.log(` ${import_picocolors16.default.yellow("Unstaged changes:")}`);
12848
13012
  for (const { file, status } of fileStatus.modified) {
12849
- console.log(` ${import_picocolors15.default.yellow("~")} ${import_picocolors15.default.dim(`${status}:`)} ${file}`);
13013
+ console.log(` ${import_picocolors16.default.yellow("~")} ${import_picocolors16.default.dim(`${status}:`)} ${file}`);
12850
13014
  }
12851
13015
  }
12852
13016
  if (fileStatus.untracked.length > 0) {
12853
- console.log(` ${import_picocolors15.default.red("Untracked files:")}`);
13017
+ console.log(` ${import_picocolors16.default.red("Untracked files:")}`);
12854
13018
  for (const file of fileStatus.untracked) {
12855
- console.log(` ${import_picocolors15.default.red("?")} ${file}`);
13019
+ console.log(` ${import_picocolors16.default.red("?")} ${file}`);
12856
13020
  }
12857
13021
  }
12858
13022
  } else if (!dirty) {
12859
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.dim("Working tree clean")}`);
13023
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.dim("Working tree clean")}`);
12860
13024
  }
12861
13025
  const tips = [];
12862
13026
  if (!branchesAligned) {
12863
- tips.push(`Run ${import_picocolors15.default.bold("contrib sync")} to align your local branches with the remote`);
13027
+ tips.push(`Run ${import_picocolors16.default.bold("contrib sync")} to align your local branches with the remote`);
12864
13028
  }
12865
13029
  if (fileStatus.staged.length > 0) {
12866
- tips.push(`Run ${import_picocolors15.default.bold("contrib commit")} to commit staged changes`);
13030
+ tips.push(`Run ${import_picocolors16.default.bold("contrib commit")} to commit staged changes`);
12867
13031
  }
12868
13032
  if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
12869
- tips.push(`Run ${import_picocolors15.default.bold("contrib commit")} to stage and commit changes`);
13033
+ tips.push(`Run ${import_picocolors16.default.bold("contrib commit")} to stage and commit changes`);
12870
13034
  }
12871
13035
  if (isFeatureBranch && branchStatus) {
12872
13036
  if (branchStatus.merged) {
12873
- tips.push(`Run ${import_picocolors15.default.bold("contrib clean")} to delete this merged branch`);
13037
+ tips.push(`Run ${import_picocolors16.default.bold("contrib clean")} to delete this merged branch`);
12874
13038
  } else if (branchStatus.stale) {
12875
- tips.push(`Run ${import_picocolors15.default.bold("contrib sync")} to rebase on latest changes, or ${import_picocolors15.default.bold("contrib clean")} if no longer needed`);
13039
+ tips.push(`Run ${import_picocolors16.default.bold("contrib sync")} to rebase on latest changes, or ${import_picocolors16.default.bold("contrib clean")} if no longer needed`);
12876
13040
  } else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
12877
13041
  const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
12878
13042
  if (branchDiv.ahead > 0) {
12879
- tips.push(`Run ${import_picocolors15.default.bold("contrib submit")} to push and create/update your PR`);
13043
+ tips.push(`Run ${import_picocolors16.default.bold("contrib submit")} to push and create/update your PR`);
12880
13044
  }
12881
13045
  }
12882
13046
  }
12883
13047
  if (tips.length > 0) {
12884
13048
  console.log();
12885
- console.log(` ${import_picocolors15.default.dim("\uD83D\uDCA1 Tip:")}`);
13049
+ console.log(` ${import_picocolors16.default.dim("\uD83D\uDCA1 Tip:")}`);
12886
13050
  for (const tip of tips) {
12887
- console.log(` ${import_picocolors15.default.dim(tip)}`);
13051
+ console.log(` ${import_picocolors16.default.dim(tip)}`);
12888
13052
  }
12889
13053
  }
12890
13054
  console.log();
12891
13055
  }
12892
13056
  });
12893
13057
  function formatStatus(branch, base, ahead, behind) {
12894
- const label = import_picocolors15.default.bold(branch.padEnd(20));
13058
+ const label = import_picocolors16.default.bold(branch.padEnd(20));
12895
13059
  if (ahead === 0 && behind === 0) {
12896
- return ` ${import_picocolors15.default.green("✓")} ${label} ${import_picocolors15.default.dim(`in sync with ${base}`)}`;
13060
+ return ` ${import_picocolors16.default.green("✓")} ${label} ${import_picocolors16.default.dim(`in sync with ${base}`)}`;
12897
13061
  }
12898
13062
  if (ahead > 0 && behind === 0) {
12899
- return ` ${import_picocolors15.default.yellow("↑")} ${label} ${import_picocolors15.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
13063
+ return ` ${import_picocolors16.default.yellow("↑")} ${label} ${import_picocolors16.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
12900
13064
  }
12901
13065
  if (behind > 0 && ahead === 0) {
12902
- return ` ${import_picocolors15.default.red("↓")} ${label} ${import_picocolors15.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
13066
+ return ` ${import_picocolors16.default.red("↓")} ${label} ${import_picocolors16.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
12903
13067
  }
12904
- return ` ${import_picocolors15.default.red("⚡")} ${label} ${import_picocolors15.default.yellow(`${ahead} ahead`)}${import_picocolors15.default.dim(", ")}${import_picocolors15.default.red(`${behind} behind`)} ${import_picocolors15.default.dim(base)}`;
13068
+ return ` ${import_picocolors16.default.red("⚡")} ${label} ${import_picocolors16.default.yellow(`${ahead} ahead`)}${import_picocolors16.default.dim(", ")}${import_picocolors16.default.red(`${behind} behind`)} ${import_picocolors16.default.dim(base)}`;
12905
13069
  }
12906
13070
  var STALE_THRESHOLD_DAYS = 14;
12907
13071
  async function detectBranchStatus(branch, baseBranch) {
@@ -12947,34 +13111,45 @@ async function detectBranchStatus(branch, baseBranch) {
12947
13111
  }
12948
13112
 
12949
13113
  // src/commands/submit.ts
12950
- var import_picocolors16 = __toESM(require_picocolors(), 1);
13114
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
12951
13115
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
12952
- info(`Checking out ${import_picocolors16.default.bold(baseBranch)}...`);
13116
+ info(`Checking out ${import_picocolors17.default.bold(baseBranch)}...`);
12953
13117
  const coResult = await checkoutBranch(baseBranch);
12954
13118
  if (coResult.exitCode !== 0) {
12955
13119
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
12956
13120
  process.exit(1);
12957
13121
  }
12958
- info(`Squash merging ${import_picocolors16.default.bold(featureBranch)} into ${import_picocolors16.default.bold(baseBranch)}...`);
13122
+ info(`Squash merging ${import_picocolors17.default.bold(featureBranch)} into ${import_picocolors17.default.bold(baseBranch)}...`);
12959
13123
  const mergeResult = await mergeSquash(featureBranch);
12960
13124
  if (mergeResult.exitCode !== 0) {
12961
13125
  error(`Squash merge failed: ${mergeResult.stderr}`);
12962
13126
  process.exit(1);
12963
13127
  }
12964
13128
  let message = options?.defaultMsg;
12965
- if (!message) {
12966
- const copilotError = await checkCopilotAvailable();
13129
+ if (!message && options?.useAI !== false) {
13130
+ const copilotError = await checkCopilotAvailable2();
12967
13131
  if (!copilotError) {
12968
- const spinner = createSpinner("Generating AI commit message for squash merge...");
12969
- const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
12970
- const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
12971
- if (aiMsg) {
12972
- message = aiMsg;
12973
- spinner.success("AI commit message generated.");
12974
- console.log(`
12975
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(message))}`);
12976
- } else {
13132
+ while (!message) {
13133
+ const spinner = createSpinner("Generating AI commit message for squash merge...");
13134
+ const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
13135
+ const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
13136
+ if (aiMsg) {
13137
+ message = aiMsg;
13138
+ spinner.success("AI commit message generated.");
13139
+ console.log(`
13140
+ ${import_picocolors17.default.dim("AI suggestion:")} ${import_picocolors17.default.bold(import_picocolors17.default.cyan(message))}`);
13141
+ break;
13142
+ }
12977
13143
  spinner.fail("AI did not return a commit message.");
13144
+ const retryAction = await selectPrompt("AI could not generate a commit message. What would you like to do?", ["Try again with AI", "Write manually", "Cancel"]);
13145
+ if (retryAction === "Try again with AI") {
13146
+ continue;
13147
+ }
13148
+ if (retryAction === "Cancel") {
13149
+ warn("Squash merge commit cancelled.");
13150
+ process.exit(0);
13151
+ }
13152
+ break;
12978
13153
  }
12979
13154
  } else {
12980
13155
  warn(`AI unavailable: ${copilotError}`);
@@ -12983,12 +13158,11 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
12983
13158
  let finalMsg = null;
12984
13159
  if (message) {
12985
13160
  while (!finalMsg) {
12986
- const action = await selectPrompt("What would you like to do?", [
12987
- "Accept this message",
12988
- "Edit this message",
12989
- "Regenerate",
12990
- "Write manually"
12991
- ]);
13161
+ const actions = ["Accept this message", "Edit this message", "Write manually"];
13162
+ if (options?.useAI !== false) {
13163
+ actions.splice(2, 0, "Regenerate");
13164
+ }
13165
+ const action = await selectPrompt("What would you like to do?", actions);
12992
13166
  if (action === "Accept this message") {
12993
13167
  finalMsg = message;
12994
13168
  } else if (action === "Edit this message") {
@@ -13001,10 +13175,10 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13001
13175
  message = regen;
13002
13176
  spinner.success("Commit message regenerated.");
13003
13177
  console.log(`
13004
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(regen))}`);
13178
+ ${import_picocolors17.default.dim("AI suggestion:")} ${import_picocolors17.default.bold(import_picocolors17.default.cyan(regen))}`);
13005
13179
  } else {
13006
13180
  spinner.fail("Regeneration failed.");
13007
- finalMsg = await inputPrompt("Enter commit message");
13181
+ continue;
13008
13182
  }
13009
13183
  } else {
13010
13184
  finalMsg = await inputPrompt("Enter commit message");
@@ -13018,13 +13192,13 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13018
13192
  error(`Commit failed: ${commitResult.stderr}`);
13019
13193
  process.exit(1);
13020
13194
  }
13021
- info(`Pushing ${import_picocolors16.default.bold(baseBranch)} to ${origin}...`);
13195
+ info(`Pushing ${import_picocolors17.default.bold(baseBranch)} to ${origin}...`);
13022
13196
  const pushResult = await pushBranch(origin, baseBranch);
13023
13197
  if (pushResult.exitCode !== 0) {
13024
13198
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
13025
13199
  process.exit(1);
13026
13200
  }
13027
- info(`Deleting local branch ${import_picocolors16.default.bold(featureBranch)}...`);
13201
+ info(`Deleting local branch ${import_picocolors17.default.bold(featureBranch)}...`);
13028
13202
  const delLocal = await forceDeleteBranch(featureBranch);
13029
13203
  if (delLocal.exitCode !== 0) {
13030
13204
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
@@ -13032,14 +13206,14 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13032
13206
  const remoteBranchRef = `${origin}/${featureBranch}`;
13033
13207
  const remoteExists = await branchExists(remoteBranchRef);
13034
13208
  if (remoteExists) {
13035
- info(`Deleting remote branch ${import_picocolors16.default.bold(featureBranch)}...`);
13209
+ info(`Deleting remote branch ${import_picocolors17.default.bold(featureBranch)}...`);
13036
13210
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
13037
13211
  if (delRemote.exitCode !== 0) {
13038
13212
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
13039
13213
  }
13040
13214
  }
13041
- success(`Squash merged ${import_picocolors16.default.bold(featureBranch)} into ${import_picocolors16.default.bold(baseBranch)} and pushed.`);
13042
- info(`Run ${import_picocolors16.default.bold("contrib start")} to begin a new feature.`, "");
13215
+ success(`Squash merged ${import_picocolors17.default.bold(featureBranch)} into ${import_picocolors17.default.bold(baseBranch)} and pushed.`);
13216
+ info(`Run ${import_picocolors17.default.bold("contrib start")} to begin a new feature.`, "");
13043
13217
  }
13044
13218
  var submit_default = defineCommand({
13045
13219
  meta: {
@@ -13052,6 +13226,18 @@ var submit_default = defineCommand({
13052
13226
  description: "Create PR as draft",
13053
13227
  default: false
13054
13228
  },
13229
+ pullrequest: {
13230
+ type: "boolean",
13231
+ alias: "pr",
13232
+ description: "Submit directly to PR flow without prompting for mode",
13233
+ default: false
13234
+ },
13235
+ local: {
13236
+ type: "boolean",
13237
+ alias: "l",
13238
+ description: "Squash merge locally without PR (maintainers only)",
13239
+ default: false
13240
+ },
13055
13241
  "no-ai": {
13056
13242
  type: "boolean",
13057
13243
  description: "Skip AI PR description generation",
@@ -13074,6 +13260,7 @@ var submit_default = defineCommand({
13074
13260
  process.exit(1);
13075
13261
  }
13076
13262
  const { origin } = config;
13263
+ const aiEnabled = isAIEnabled(config, args["no-ai"]);
13077
13264
  const baseBranch = getBaseBranch(config);
13078
13265
  const protectedBranches = getProtectedBranches(config);
13079
13266
  const currentBranch = await getCurrentBranch();
@@ -13082,8 +13269,8 @@ var submit_default = defineCommand({
13082
13269
  process.exit(1);
13083
13270
  }
13084
13271
  if (protectedBranches.includes(currentBranch)) {
13085
- heading("\uD83D\uDE80 contrib submit");
13086
- warn(`You're on ${import_picocolors16.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
13272
+ projectHeading("submit", "\uD83D\uDE80");
13273
+ warn(`You're on ${import_picocolors17.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
13087
13274
  await fetchAll();
13088
13275
  const remoteRef = `${origin}/${currentBranch}`;
13089
13276
  const localWork = await hasLocalWork(origin, currentBranch);
@@ -13092,11 +13279,11 @@ var submit_default = defineCommand({
13092
13279
  const hasAnything = hasCommits || dirty;
13093
13280
  if (!hasAnything) {
13094
13281
  error("No local changes or commits to move. Switch to a feature branch first.");
13095
- info(` Run ${import_picocolors16.default.bold("contrib start")} to create a new feature branch.`, "");
13282
+ info(` Run ${import_picocolors17.default.bold("contrib start")} to create a new feature branch.`, "");
13096
13283
  process.exit(1);
13097
13284
  }
13098
13285
  if (hasCommits) {
13099
- info(`Found ${import_picocolors16.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors16.default.bold(currentBranch)}.`);
13286
+ info(`Found ${import_picocolors17.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors17.default.bold(currentBranch)}.`);
13100
13287
  }
13101
13288
  if (dirty) {
13102
13289
  info("You also have uncommitted changes in the working tree.");
@@ -13112,58 +13299,35 @@ var submit_default = defineCommand({
13112
13299
  info("No changes made. You are still on your current branch.");
13113
13300
  return;
13114
13301
  }
13115
- info(import_picocolors16.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13116
- const description = await inputPrompt("What are you going to work on?");
13117
- let newBranchName = description;
13118
- if (looksLikeNaturalLanguage(description)) {
13119
- const copilotError = await checkCopilotAvailable();
13120
- if (!copilotError) {
13121
- const spinner = createSpinner("Generating branch name suggestion...");
13122
- const suggested = await suggestBranchName(description, args.model);
13123
- if (suggested) {
13124
- spinner.success("Branch name suggestion ready.");
13125
- console.log(`
13126
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(suggested))}`);
13127
- const accepted = await confirmPrompt(`Use ${import_picocolors16.default.bold(suggested)} as your branch name?`);
13128
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13129
- } else {
13130
- spinner.fail("AI did not return a suggestion.");
13131
- newBranchName = await inputPrompt("Enter branch name", description);
13132
- }
13133
- }
13134
- }
13135
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13136
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors16.default.bold(newBranchName)}:`, config.branchPrefixes);
13137
- newBranchName = formatBranchName(prefix, newBranchName);
13138
- }
13139
- if (!isValidBranchName(newBranchName)) {
13140
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13141
- process.exit(1);
13142
- }
13143
- if (await branchExists(newBranchName)) {
13144
- error(`Branch ${import_picocolors16.default.bold(newBranchName)} already exists. Choose a different name.`);
13145
- process.exit(1);
13302
+ const newBranchName = await promptForBranchName({
13303
+ branchPrefixes: config.branchPrefixes,
13304
+ useAI: aiEnabled,
13305
+ model: args.model
13306
+ });
13307
+ if (!newBranchName) {
13308
+ info("No changes made. You are still on your current branch.");
13309
+ return;
13146
13310
  }
13147
13311
  const branchResult = await createBranch(newBranchName);
13148
13312
  if (branchResult.exitCode !== 0) {
13149
13313
  error(`Failed to create branch: ${branchResult.stderr}`);
13150
13314
  process.exit(1);
13151
13315
  }
13152
- success(`Created ${import_picocolors16.default.bold(newBranchName)} with your changes.`);
13316
+ success(`Created ${import_picocolors17.default.bold(newBranchName)} with your changes.`);
13153
13317
  await updateLocalBranch(currentBranch, remoteRef);
13154
- info(`Reset ${import_picocolors16.default.bold(currentBranch)} back to ${import_picocolors16.default.bold(remoteRef)} — no damage done.`, "");
13318
+ info(`Reset ${import_picocolors17.default.bold(currentBranch)} back to ${import_picocolors17.default.bold(remoteRef)} — no damage done.`, "");
13155
13319
  console.log();
13156
- success(`You're now on ${import_picocolors16.default.bold(newBranchName)} with all your work intact.`);
13157
- info(`Run ${import_picocolors16.default.bold("contrib submit")} again to push and create your PR.`, "");
13320
+ success(`You're now on ${import_picocolors17.default.bold(newBranchName)} with all your work intact.`);
13321
+ info(`Run ${import_picocolors17.default.bold("contrib submit")} again to push and create your PR.`, "");
13158
13322
  return;
13159
13323
  }
13160
- heading("\uD83D\uDE80 contrib submit");
13324
+ projectHeading("submit", "\uD83D\uDE80");
13161
13325
  const ghInstalled = await checkGhInstalled();
13162
13326
  const ghAuthed = ghInstalled && await checkGhAuth();
13163
13327
  if (ghInstalled && ghAuthed) {
13164
13328
  const mergedPR = await getMergedPRForBranch(currentBranch);
13165
13329
  if (mergedPR) {
13166
- warn(`PR #${mergedPR.number} (${import_picocolors16.default.bold(mergedPR.title)}) was already merged.`);
13330
+ warn(`PR #${mergedPR.number} (${import_picocolors17.default.bold(mergedPR.title)}) was already merged.`);
13167
13331
  const localWork = await hasLocalWork(origin, currentBranch);
13168
13332
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
13169
13333
  if (hasWork) {
@@ -13171,7 +13335,7 @@ var submit_default = defineCommand({
13171
13335
  warn("You have uncommitted changes in your working tree.");
13172
13336
  }
13173
13337
  if (localWork.unpushedCommits > 0) {
13174
- warn(`You have ${import_picocolors16.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
13338
+ warn(`You have ${import_picocolors17.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
13175
13339
  }
13176
13340
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
13177
13341
  const DISCARD = "Discard all changes and clean up";
@@ -13182,46 +13346,26 @@ var submit_default = defineCommand({
13182
13346
  return;
13183
13347
  }
13184
13348
  if (action === SAVE_NEW_BRANCH) {
13185
- info(import_picocolors16.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13186
- const description = await inputPrompt("What are you going to work on?");
13187
- let newBranchName = description;
13188
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13189
- const spinner = createSpinner("Generating branch name suggestion...");
13190
- const suggested = await suggestBranchName(description, args.model);
13191
- if (suggested) {
13192
- spinner.success("Branch name suggestion ready.");
13193
- console.log(`
13194
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(suggested))}`);
13195
- const accepted = await confirmPrompt(`Use ${import_picocolors16.default.bold(suggested)} as your branch name?`);
13196
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13197
- } else {
13198
- spinner.fail("AI did not return a suggestion.");
13199
- newBranchName = await inputPrompt("Enter branch name", description);
13200
- }
13201
- }
13202
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13203
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors16.default.bold(newBranchName)}:`, config.branchPrefixes);
13204
- newBranchName = formatBranchName(prefix, newBranchName);
13205
- }
13206
- if (!isValidBranchName(newBranchName)) {
13207
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13208
- process.exit(1);
13349
+ const newBranchName = await promptForBranchName({
13350
+ branchPrefixes: config.branchPrefixes,
13351
+ useAI: aiEnabled,
13352
+ model: args.model
13353
+ });
13354
+ if (!newBranchName) {
13355
+ info("No changes made. You are still on your current branch.");
13356
+ return;
13209
13357
  }
13210
13358
  const staleUpstream = await getUpstreamRef();
13211
13359
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
13212
- if (await branchExists(newBranchName)) {
13213
- error(`Branch ${import_picocolors16.default.bold(newBranchName)} already exists. Choose a different name.`);
13214
- process.exit(1);
13215
- }
13216
13360
  const renameResult = await renameBranch(currentBranch, newBranchName);
13217
13361
  if (renameResult.exitCode !== 0) {
13218
13362
  error(`Failed to rename branch: ${renameResult.stderr}`);
13219
13363
  process.exit(1);
13220
13364
  }
13221
- success(`Renamed ${import_picocolors16.default.bold(currentBranch)} → ${import_picocolors16.default.bold(newBranchName)}`);
13365
+ success(`Renamed ${import_picocolors17.default.bold(currentBranch)} → ${import_picocolors17.default.bold(newBranchName)}`);
13222
13366
  await unsetUpstream();
13223
13367
  const syncSource2 = getSyncSource(config);
13224
- info(`Syncing ${import_picocolors16.default.bold(newBranchName)} with latest ${import_picocolors16.default.bold(baseBranch)}...`);
13368
+ info(`Syncing ${import_picocolors17.default.bold(newBranchName)} with latest ${import_picocolors17.default.bold(baseBranch)}...`);
13225
13369
  await fetchRemote(syncSource2.remote);
13226
13370
  let rebaseResult;
13227
13371
  if (staleUpstreamHash) {
@@ -13232,17 +13376,17 @@ var submit_default = defineCommand({
13232
13376
  }
13233
13377
  if (rebaseResult.exitCode !== 0) {
13234
13378
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
13235
- info(` ${import_picocolors16.default.bold("git rebase --continue")}`, "");
13379
+ info(` ${import_picocolors17.default.bold("git rebase --continue")}`, "");
13236
13380
  } else {
13237
- success(`Rebased ${import_picocolors16.default.bold(newBranchName)} onto ${import_picocolors16.default.bold(syncSource2.ref)}.`);
13381
+ success(`Rebased ${import_picocolors17.default.bold(newBranchName)} onto ${import_picocolors17.default.bold(syncSource2.ref)}.`);
13238
13382
  }
13239
- info(`All your changes are preserved. Run ${import_picocolors16.default.bold("contrib submit")} when ready to create a new PR.`, "");
13383
+ info(`All your changes are preserved. Run ${import_picocolors17.default.bold("contrib submit")} when ready to create a new PR.`, "");
13240
13384
  return;
13241
13385
  }
13242
13386
  warn("Discarding local changes...");
13243
13387
  }
13244
13388
  const syncSource = getSyncSource(config);
13245
- info(`Switching to ${import_picocolors16.default.bold(baseBranch)} and syncing...`);
13389
+ info(`Switching to ${import_picocolors17.default.bold(baseBranch)} and syncing...`);
13246
13390
  await fetchRemote(syncSource.remote);
13247
13391
  await resetHard("HEAD");
13248
13392
  const coResult = await checkoutBranch(baseBranch);
@@ -13251,23 +13395,23 @@ var submit_default = defineCommand({
13251
13395
  process.exit(1);
13252
13396
  }
13253
13397
  await updateLocalBranch(baseBranch, syncSource.ref);
13254
- success(`Synced ${import_picocolors16.default.bold(baseBranch)} with ${import_picocolors16.default.bold(syncSource.ref)}.`);
13255
- info(`Deleting stale branch ${import_picocolors16.default.bold(currentBranch)}...`);
13398
+ success(`Synced ${import_picocolors17.default.bold(baseBranch)} with ${import_picocolors17.default.bold(syncSource.ref)}.`);
13399
+ info(`Deleting stale branch ${import_picocolors17.default.bold(currentBranch)}...`);
13256
13400
  const delResult = await forceDeleteBranch(currentBranch);
13257
13401
  if (delResult.exitCode === 0) {
13258
- success(`Deleted ${import_picocolors16.default.bold(currentBranch)}.`);
13402
+ success(`Deleted ${import_picocolors17.default.bold(currentBranch)}.`);
13259
13403
  } else {
13260
13404
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
13261
13405
  }
13262
13406
  console.log();
13263
- info(`You're now on ${import_picocolors16.default.bold(baseBranch)}. Run ${import_picocolors16.default.bold("contrib start")} to begin a new feature.`);
13407
+ info(`You're now on ${import_picocolors17.default.bold(baseBranch)}. Run ${import_picocolors17.default.bold("contrib start")} to begin a new feature.`);
13264
13408
  return;
13265
13409
  }
13266
13410
  }
13267
13411
  if (ghInstalled && ghAuthed) {
13268
13412
  const existingPR = await getPRForBranch(currentBranch);
13269
13413
  if (existingPR) {
13270
- info(`Pushing ${import_picocolors16.default.bold(currentBranch)} to ${origin}...`);
13414
+ info(`Pushing ${import_picocolors17.default.bold(currentBranch)} to ${origin}...`);
13271
13415
  const pushResult2 = await pushSetUpstream(origin, currentBranch);
13272
13416
  if (pushResult2.exitCode !== 0) {
13273
13417
  error(`Failed to push: ${pushResult2.stderr}`);
@@ -13278,8 +13422,8 @@ var submit_default = defineCommand({
13278
13422
  }
13279
13423
  process.exit(1);
13280
13424
  }
13281
- success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors16.default.bold(existingPR.title)}`);
13282
- console.log(` ${import_picocolors16.default.cyan(existingPR.url)}`);
13425
+ success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors17.default.bold(existingPR.title)}`);
13426
+ console.log(` ${import_picocolors17.default.cyan(existingPR.url)}`);
13283
13427
  return;
13284
13428
  }
13285
13429
  }
@@ -13287,7 +13431,7 @@ var submit_default = defineCommand({
13287
13431
  let prBody = null;
13288
13432
  async function tryGenerateAI() {
13289
13433
  const [copilotError, commits, diff] = await Promise.all([
13290
- checkCopilotAvailable(),
13434
+ checkCopilotAvailable2(),
13291
13435
  getLog(baseBranch, "HEAD"),
13292
13436
  getLogDiff(baseBranch, "HEAD")
13293
13437
  ]);
@@ -13299,10 +13443,10 @@ var submit_default = defineCommand({
13299
13443
  prBody = result.body;
13300
13444
  spinner.success("PR description generated.");
13301
13445
  console.log(`
13302
- ${import_picocolors16.default.dim("AI title:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(prTitle))}`);
13446
+ ${import_picocolors17.default.dim("AI title:")} ${import_picocolors17.default.bold(import_picocolors17.default.cyan(prTitle))}`);
13303
13447
  console.log(`
13304
- ${import_picocolors16.default.dim("AI body preview:")}`);
13305
- console.log(import_picocolors16.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
13448
+ ${import_picocolors17.default.dim("AI body preview:")}`);
13449
+ console.log(import_picocolors17.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
13306
13450
  } else {
13307
13451
  spinner.fail("AI did not return a PR description.");
13308
13452
  }
@@ -13315,7 +13459,23 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13315
13459
  const REGENERATE = "Regenerate AI description";
13316
13460
  let submitAction = "cancel";
13317
13461
  const isMaintainer = config.role === "maintainer";
13318
- if (isMaintainer) {
13462
+ if (args.pullrequest && args.local) {
13463
+ error("Use only one submit mode flag at a time: --pullrequest/--pr/-pr or -l for local squash merge.");
13464
+ process.exit(1);
13465
+ }
13466
+ if (args.local && !isMaintainer) {
13467
+ error("The -l flag is only available for maintainers. Contributors must submit via PR.");
13468
+ process.exit(1);
13469
+ }
13470
+ if (args.local) {
13471
+ await performSquashMerge(origin, baseBranch, currentBranch, {
13472
+ model: args.model,
13473
+ convention: config.commitConvention,
13474
+ useAI: aiEnabled
13475
+ });
13476
+ return;
13477
+ }
13478
+ if (isMaintainer && !args.pullrequest) {
13319
13479
  const maintainerChoice = await selectPrompt("How would you like to submit your changes?", ["Create a PR", SQUASH_LOCAL, CANCEL]);
13320
13480
  if (maintainerChoice === CANCEL) {
13321
13481
  warn("Submit cancelled.");
@@ -13324,12 +13484,13 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13324
13484
  if (maintainerChoice === SQUASH_LOCAL) {
13325
13485
  await performSquashMerge(origin, baseBranch, currentBranch, {
13326
13486
  model: args.model,
13327
- convention: config.commitConvention
13487
+ convention: config.commitConvention,
13488
+ useAI: aiEnabled
13328
13489
  });
13329
13490
  return;
13330
13491
  }
13331
13492
  }
13332
- if (!args["no-ai"]) {
13493
+ if (aiEnabled) {
13333
13494
  await tryGenerateAI();
13334
13495
  }
13335
13496
  let actionResolved = false;
@@ -13368,7 +13529,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13368
13529
  }
13369
13530
  } else {
13370
13531
  const choices = [];
13371
- if (!args["no-ai"])
13532
+ if (aiEnabled)
13372
13533
  choices.push(REGENERATE);
13373
13534
  choices.push("Write title & body manually", "Use gh --fill (auto-fill from commits)", CANCEL);
13374
13535
  const action = await selectPrompt("How would you like to create the PR?", choices);
@@ -13392,7 +13553,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13392
13553
  warn("Submit cancelled.");
13393
13554
  return;
13394
13555
  }
13395
- info(`Pushing ${import_picocolors16.default.bold(currentBranch)} to ${origin}...`);
13556
+ info(`Pushing ${import_picocolors17.default.bold(currentBranch)} to ${origin}...`);
13396
13557
  const pushResult = await pushSetUpstream(origin, currentBranch);
13397
13558
  if (pushResult.exitCode !== 0) {
13398
13559
  error(`Failed to push: ${pushResult.stderr}`);
@@ -13411,7 +13572,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13411
13572
  const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
13412
13573
  console.log();
13413
13574
  info("Create your PR manually:", "");
13414
- console.log(` ${import_picocolors16.default.cyan(prUrl)}`);
13575
+ console.log(` ${import_picocolors17.default.cyan(prUrl)}`);
13415
13576
  } else {
13416
13577
  info("gh CLI not available. Create your PR manually on GitHub.", "");
13417
13578
  }
@@ -13445,7 +13606,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13445
13606
  });
13446
13607
 
13447
13608
  // src/commands/switch.ts
13448
- var import_picocolors17 = __toESM(require_picocolors(), 1);
13609
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
13449
13610
  var switch_default = defineCommand({
13450
13611
  meta: {
13451
13612
  name: "switch",
@@ -13466,7 +13627,7 @@ var switch_default = defineCommand({
13466
13627
  const config = readConfig();
13467
13628
  const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
13468
13629
  const currentBranch = await getCurrentBranch();
13469
- heading("\uD83D\uDD00 contrib switch");
13630
+ projectHeading("switch", "\uD83D\uDD00");
13470
13631
  let targetBranch = args.name;
13471
13632
  if (!targetBranch) {
13472
13633
  const localBranches = await getLocalBranches();
@@ -13477,11 +13638,11 @@ var switch_default = defineCommand({
13477
13638
  const choices = localBranches.filter((b2) => b2.name !== currentBranch).map((b2) => {
13478
13639
  const labels = [];
13479
13640
  if (protectedBranches.includes(b2.name))
13480
- labels.push(import_picocolors17.default.red("protected"));
13641
+ labels.push(import_picocolors18.default.red("protected"));
13481
13642
  if (b2.upstream)
13482
- labels.push(import_picocolors17.default.dim(`→ ${b2.upstream}`));
13643
+ labels.push(import_picocolors18.default.dim(`→ ${b2.upstream}`));
13483
13644
  if (b2.gone)
13484
- labels.push(import_picocolors17.default.red("remote gone"));
13645
+ labels.push(import_picocolors18.default.red("remote gone"));
13485
13646
  const suffix = labels.length > 0 ? ` ${labels.join(" · ")}` : "";
13486
13647
  return `${b2.name}${suffix}`;
13487
13648
  });
@@ -13493,7 +13654,7 @@ var switch_default = defineCommand({
13493
13654
  targetBranch = selected.split(/\s{2,}/)[0].trim();
13494
13655
  }
13495
13656
  if (targetBranch === currentBranch) {
13496
- info(`Already on ${import_picocolors17.default.bold(targetBranch)}.`);
13657
+ info(`Already on ${import_picocolors18.default.bold(targetBranch)}.`);
13497
13658
  return;
13498
13659
  }
13499
13660
  if (await hasUncommittedChanges()) {
@@ -13512,7 +13673,7 @@ var switch_default = defineCommand({
13512
13673
  const stashMsg = `contrib-save: auto-save from ${currentBranch}`;
13513
13674
  try {
13514
13675
  await exec("git", ["stash", "push", "-m", stashMsg]);
13515
- info(`Saved changes: ${import_picocolors17.default.dim(stashMsg)}`);
13676
+ info(`Saved changes: ${import_picocolors18.default.dim(stashMsg)}`);
13516
13677
  } catch {
13517
13678
  error("Failed to save changes. Please commit or save manually.");
13518
13679
  process.exit(1);
@@ -13528,9 +13689,9 @@ var switch_default = defineCommand({
13528
13689
  }
13529
13690
  process.exit(1);
13530
13691
  }
13531
- success(`Switched to ${import_picocolors17.default.bold(targetBranch)}`);
13532
- info(`Your changes from ${import_picocolors17.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
13533
- info(`Use ${import_picocolors17.default.bold("contrib save --restore")} to bring them back.`, "");
13692
+ success(`Switched to ${import_picocolors18.default.bold(targetBranch)}`);
13693
+ info(`Your changes from ${import_picocolors18.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
13694
+ info(`Use ${import_picocolors18.default.bold("contrib save --restore")} to bring them back.`, "");
13534
13695
  return;
13535
13696
  }
13536
13697
  const result = await checkoutBranch(targetBranch);
@@ -13538,12 +13699,12 @@ var switch_default = defineCommand({
13538
13699
  error(`Failed to switch to ${targetBranch}: ${result.stderr}`);
13539
13700
  process.exit(1);
13540
13701
  }
13541
- success(`Switched to ${import_picocolors17.default.bold(targetBranch)}`);
13702
+ success(`Switched to ${import_picocolors18.default.bold(targetBranch)}`);
13542
13703
  }
13543
13704
  });
13544
13705
 
13545
13706
  // src/commands/sync.ts
13546
- var import_picocolors18 = __toESM(require_picocolors(), 1);
13707
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
13547
13708
  var sync_default = defineCommand({
13548
13709
  meta: {
13549
13710
  name: "sync",
@@ -13582,7 +13743,7 @@ var sync_default = defineCommand({
13582
13743
  error("You have uncommitted changes. Please commit or stash them before syncing.");
13583
13744
  process.exit(1);
13584
13745
  }
13585
- heading(`\uD83D\uDD04 contrib sync (${workflow}, ${role})`);
13746
+ projectHeading(`sync (${workflow}, ${role})`, "\uD83D\uDD04");
13586
13747
  const baseBranch = getBaseBranch(config);
13587
13748
  const syncSource = getSyncSource(config);
13588
13749
  info(`Fetching ${syncSource.remote}...`);
@@ -13595,24 +13756,24 @@ var sync_default = defineCommand({
13595
13756
  await fetchRemote(origin);
13596
13757
  }
13597
13758
  if (!await refExists(syncSource.ref)) {
13598
- error(`Remote ref ${import_picocolors18.default.bold(syncSource.ref)} does not exist.`);
13759
+ error(`Remote ref ${import_picocolors19.default.bold(syncSource.ref)} does not exist.`);
13599
13760
  info("This can happen if the branch was renamed or deleted on the remote.", "");
13600
- info(`Check your config: the base branch may need updating via ${import_picocolors18.default.bold("contrib setup")}.`, "");
13761
+ info(`Check your config: the base branch may need updating via ${import_picocolors19.default.bold("contrib setup")}.`, "");
13601
13762
  process.exit(1);
13602
13763
  }
13603
13764
  let allowMergeCommit = false;
13604
13765
  const div = await getDivergence(baseBranch, syncSource.ref);
13605
13766
  if (div.ahead > 0 || div.behind > 0) {
13606
- info(`${import_picocolors18.default.bold(baseBranch)} is ${import_picocolors18.default.yellow(`${div.ahead} ahead`)} and ${import_picocolors18.default.red(`${div.behind} behind`)} ${syncSource.ref}`);
13767
+ info(`${import_picocolors19.default.bold(baseBranch)} is ${import_picocolors19.default.yellow(`${div.ahead} ahead`)} and ${import_picocolors19.default.red(`${div.behind} behind`)} ${syncSource.ref}`);
13607
13768
  } else {
13608
- info(`${import_picocolors18.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
13769
+ info(`${import_picocolors19.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
13609
13770
  }
13610
13771
  if (div.ahead > 0) {
13611
13772
  const currentBranch = await getCurrentBranch();
13612
13773
  const protectedBranches = getProtectedBranches(config);
13613
13774
  const isOnProtected = currentBranch && protectedBranches.includes(currentBranch);
13614
13775
  if (isOnProtected) {
13615
- warn(`You have ${import_picocolors18.default.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${import_picocolors18.default.bold(baseBranch)} that aren't on the remote.`);
13776
+ warn(`You have ${import_picocolors19.default.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${import_picocolors19.default.bold(baseBranch)} that aren't on the remote.`);
13616
13777
  info("Pulling now could create a merge commit, which breaks clean history.");
13617
13778
  console.log();
13618
13779
  const MOVE_BRANCH = "Move my commits to a new feature branch, then sync";
@@ -13628,44 +13789,21 @@ var sync_default = defineCommand({
13628
13789
  return;
13629
13790
  }
13630
13791
  if (action === MOVE_BRANCH) {
13631
- info(import_picocolors18.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13632
- const description = await inputPrompt("What are you going to work on?");
13633
- let newBranchName = description;
13634
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13635
- const copilotError = await checkCopilotAvailable();
13636
- if (!copilotError) {
13637
- const spinner = createSpinner("Generating branch name suggestion...");
13638
- const suggested = await suggestBranchName(description, args.model);
13639
- if (suggested) {
13640
- spinner.success("Branch name suggestion ready.");
13641
- console.log(`
13642
- ${import_picocolors18.default.dim("AI suggestion:")} ${import_picocolors18.default.bold(import_picocolors18.default.cyan(suggested))}`);
13643
- const accepted = await confirmPrompt(`Use ${import_picocolors18.default.bold(suggested)} as your branch name?`);
13644
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13645
- } else {
13646
- spinner.fail("AI did not return a suggestion.");
13647
- newBranchName = await inputPrompt("Enter branch name", description);
13648
- }
13649
- }
13650
- }
13651
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13652
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors18.default.bold(newBranchName)}:`, config.branchPrefixes);
13653
- newBranchName = formatBranchName(prefix, newBranchName);
13654
- }
13655
- if (!isValidBranchName(newBranchName)) {
13656
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13657
- process.exit(1);
13658
- }
13659
- if (await branchExists(newBranchName)) {
13660
- error(`Branch ${import_picocolors18.default.bold(newBranchName)} already exists. Choose a different name.`);
13661
- process.exit(1);
13792
+ const newBranchName = await promptForBranchName({
13793
+ branchPrefixes: config.branchPrefixes,
13794
+ useAI: isAIEnabled(config, args["no-ai"]),
13795
+ model: args.model
13796
+ });
13797
+ if (!newBranchName) {
13798
+ info("No changes made.");
13799
+ return;
13662
13800
  }
13663
13801
  const branchResult = await createBranch(newBranchName);
13664
13802
  if (branchResult.exitCode !== 0) {
13665
13803
  error(`Failed to create branch: ${branchResult.stderr}`);
13666
13804
  process.exit(1);
13667
13805
  }
13668
- success(`Created ${import_picocolors18.default.bold(newBranchName)} with your commits.`);
13806
+ success(`Created ${import_picocolors19.default.bold(newBranchName)} with your commits.`);
13669
13807
  const coResult2 = await checkoutBranch(baseBranch);
13670
13808
  if (coResult2.exitCode !== 0) {
13671
13809
  error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
@@ -13673,11 +13811,11 @@ var sync_default = defineCommand({
13673
13811
  }
13674
13812
  const remoteRef = syncSource.ref;
13675
13813
  await updateLocalBranch(baseBranch, remoteRef);
13676
- success(`Reset ${import_picocolors18.default.bold(baseBranch)} to ${import_picocolors18.default.bold(remoteRef)}.`);
13677
- success(`${import_picocolors18.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
13814
+ success(`Reset ${import_picocolors19.default.bold(baseBranch)} to ${import_picocolors19.default.bold(remoteRef)}.`);
13815
+ success(`${import_picocolors19.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
13678
13816
  console.log();
13679
- info(`Your commits are safe on ${import_picocolors18.default.bold(newBranchName)}.`, "");
13680
- info(`Run ${import_picocolors18.default.bold(`git checkout ${newBranchName}`)} then ${import_picocolors18.default.bold("contrib update")} to rebase onto the synced ${import_picocolors18.default.bold(baseBranch)}.`, "");
13817
+ info(`Your commits are safe on ${import_picocolors19.default.bold(newBranchName)}.`, "");
13818
+ info(`Run ${import_picocolors19.default.bold(`git checkout ${newBranchName}`)} then ${import_picocolors19.default.bold("contrib update")} to rebase onto the synced ${import_picocolors19.default.bold(baseBranch)}.`, "");
13681
13819
  return;
13682
13820
  }
13683
13821
  allowMergeCommit = true;
@@ -13685,7 +13823,7 @@ var sync_default = defineCommand({
13685
13823
  }
13686
13824
  }
13687
13825
  if (!args.yes) {
13688
- const ok = await confirmPrompt(`This will pull ${import_picocolors18.default.bold(syncSource.ref)} into local ${import_picocolors18.default.bold(baseBranch)}.`);
13826
+ const ok = await confirmPrompt(`This will pull ${import_picocolors19.default.bold(syncSource.ref)} into local ${import_picocolors19.default.bold(baseBranch)}.`);
13689
13827
  if (!ok)
13690
13828
  process.exit(0);
13691
13829
  }
@@ -13699,8 +13837,8 @@ var sync_default = defineCommand({
13699
13837
  if (allowMergeCommit) {
13700
13838
  error(`Pull failed: ${pullResult.stderr.trim()}`);
13701
13839
  } else {
13702
- error(`Fast-forward pull failed. Your local ${import_picocolors18.default.bold(baseBranch)} may have diverged.`);
13703
- info(`Use ${import_picocolors18.default.bold("contrib sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
13840
+ error(`Fast-forward pull failed. Your local ${import_picocolors19.default.bold(baseBranch)} may have diverged.`);
13841
+ info(`Use ${import_picocolors19.default.bold("contrib sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
13704
13842
  }
13705
13843
  process.exit(1);
13706
13844
  }
@@ -13708,7 +13846,7 @@ var sync_default = defineCommand({
13708
13846
  if (hasDevBranch(workflow) && role === "maintainer") {
13709
13847
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
13710
13848
  if (mainDiv.behind > 0) {
13711
- info(`Also syncing ${import_picocolors18.default.bold(config.mainBranch)}...`);
13849
+ info(`Also syncing ${import_picocolors19.default.bold(config.mainBranch)}...`);
13712
13850
  const mainCoResult = await checkoutBranch(config.mainBranch);
13713
13851
  if (mainCoResult.exitCode === 0) {
13714
13852
  const mainPullResult = await pullFastForwardOnly(origin, config.mainBranch);
@@ -13746,20 +13884,20 @@ var sync_default = defineCommand({
13746
13884
  groups.get(hash).push(name);
13747
13885
  }
13748
13886
  console.log();
13749
- console.log(` ${import_picocolors18.default.bold("\uD83D\uDD17 Branch Alignment")}`);
13887
+ console.log(` ${import_picocolors19.default.bold("\uD83D\uDD17 Branch Alignment")}`);
13750
13888
  for (const [hash, names] of groups) {
13751
13889
  const short = hash.slice(0, 7);
13752
- const nameStr = names.map((n2) => import_picocolors18.default.bold(n2)).join(import_picocolors18.default.dim(" · "));
13753
- console.log(` ${import_picocolors18.default.yellow(short)} ${import_picocolors18.default.dim("──")} ${nameStr}`);
13890
+ const nameStr = names.map((n2) => import_picocolors19.default.bold(n2)).join(import_picocolors19.default.dim(" · "));
13891
+ console.log(` ${import_picocolors19.default.yellow(short)} ${import_picocolors19.default.dim("──")} ${nameStr}`);
13754
13892
  const subject = await getCommitSubject(hash);
13755
13893
  if (subject) {
13756
- console.log(` ${import_picocolors18.default.dim(subject)}`);
13894
+ console.log(` ${import_picocolors19.default.dim(subject)}`);
13757
13895
  }
13758
13896
  }
13759
13897
  if (groups.size === 1) {
13760
- console.log(` ${import_picocolors18.default.green("✓")} ${import_picocolors18.default.green("All branches aligned")} ${import_picocolors18.default.dim("— ready to start")}`);
13898
+ console.log(` ${import_picocolors19.default.green("✓")} ${import_picocolors19.default.green("All branches aligned")} ${import_picocolors19.default.dim("— ready to start")}`);
13761
13899
  } else {
13762
- console.log(` ${import_picocolors18.default.yellow("⚠")} ${import_picocolors18.default.yellow("Branches are not fully aligned")}`);
13900
+ console.log(` ${import_picocolors19.default.yellow("⚠")} ${import_picocolors19.default.yellow("Branches are not fully aligned")}`);
13763
13901
  }
13764
13902
  }
13765
13903
  }
@@ -13768,7 +13906,7 @@ var sync_default = defineCommand({
13768
13906
 
13769
13907
  // src/commands/update.ts
13770
13908
  import { readFileSync as readFileSync4 } from "node:fs";
13771
- var import_picocolors19 = __toESM(require_picocolors(), 1);
13909
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
13772
13910
  var update_default = defineCommand({
13773
13911
  meta: {
13774
13912
  name: "update",
@@ -13805,8 +13943,8 @@ var update_default = defineCommand({
13805
13943
  process.exit(1);
13806
13944
  }
13807
13945
  if (protectedBranches.includes(currentBranch)) {
13808
- heading("\uD83D\uDD03 contrib update");
13809
- warn(`You're on ${import_picocolors19.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
13946
+ projectHeading("update", "\uD83D\uDD03");
13947
+ warn(`You're on ${import_picocolors20.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
13810
13948
  await fetchAll();
13811
13949
  const { origin } = config;
13812
13950
  const remoteRef = `${origin}/${currentBranch}`;
@@ -13815,12 +13953,12 @@ var update_default = defineCommand({
13815
13953
  const hasCommits = localWork.unpushedCommits > 0;
13816
13954
  const hasAnything = hasCommits || dirty;
13817
13955
  if (!hasAnything) {
13818
- info(`No local changes found on ${import_picocolors19.default.bold(currentBranch)}.`);
13819
- info(`Use ${import_picocolors19.default.bold("contrib sync")} to sync protected branches, or ${import_picocolors19.default.bold("contrib start")} to create a feature branch.`);
13956
+ info(`No local changes found on ${import_picocolors20.default.bold(currentBranch)}.`);
13957
+ info(`Use ${import_picocolors20.default.bold("contrib sync")} to sync protected branches, or ${import_picocolors20.default.bold("contrib start")} to create a feature branch.`);
13820
13958
  process.exit(1);
13821
13959
  }
13822
13960
  if (hasCommits) {
13823
- info(`Found ${import_picocolors19.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors19.default.bold(currentBranch)}.`);
13961
+ info(`Found ${import_picocolors20.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors20.default.bold(currentBranch)}.`);
13824
13962
  }
13825
13963
  if (dirty) {
13826
13964
  info("You also have uncommitted changes in the working tree.");
@@ -13836,56 +13974,37 @@ var update_default = defineCommand({
13836
13974
  info("No changes made. You are still on your current branch.");
13837
13975
  return;
13838
13976
  }
13839
- info(import_picocolors19.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13840
- const description = await inputPrompt("What are you going to work on?");
13841
- let newBranchName = description;
13842
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13843
- const copilotError = await checkCopilotAvailable();
13844
- if (!copilotError) {
13845
- const spinner = createSpinner("Generating branch name suggestion...");
13846
- const suggested = await suggestBranchName(description, args.model);
13847
- if (suggested) {
13848
- spinner.success("Branch name suggestion ready.");
13849
- console.log(`
13850
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(suggested))}`);
13851
- const accepted = await confirmPrompt(`Use ${import_picocolors19.default.bold(suggested)} as your branch name?`);
13852
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13853
- } else {
13854
- spinner.fail("AI did not return a suggestion.");
13855
- newBranchName = await inputPrompt("Enter branch name", description);
13856
- }
13857
- }
13858
- }
13859
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13860
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors19.default.bold(newBranchName)}:`, config.branchPrefixes);
13861
- newBranchName = formatBranchName(prefix, newBranchName);
13862
- }
13863
- if (!isValidBranchName(newBranchName)) {
13864
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13865
- process.exit(1);
13977
+ const newBranchName = await promptForBranchName({
13978
+ branchPrefixes: config.branchPrefixes,
13979
+ useAI: isAIEnabled(config, args["no-ai"]),
13980
+ model: args.model
13981
+ });
13982
+ if (!newBranchName) {
13983
+ info("No changes made. You are still on your current branch.");
13984
+ return;
13866
13985
  }
13867
13986
  const branchResult = await createBranch(newBranchName);
13868
13987
  if (branchResult.exitCode !== 0) {
13869
13988
  error(`Failed to create branch: ${branchResult.stderr}`);
13870
13989
  process.exit(1);
13871
13990
  }
13872
- success(`Created ${import_picocolors19.default.bold(newBranchName)} with your changes.`);
13991
+ success(`Created ${import_picocolors20.default.bold(newBranchName)} with your changes.`);
13873
13992
  await updateLocalBranch(currentBranch, remoteRef);
13874
- info(`Reset ${import_picocolors19.default.bold(currentBranch)} back to ${import_picocolors19.default.bold(remoteRef)} — no damage done.`, "");
13993
+ info(`Reset ${import_picocolors20.default.bold(currentBranch)} back to ${import_picocolors20.default.bold(remoteRef)} — no damage done.`, "");
13875
13994
  console.log();
13876
- success(`You're now on ${import_picocolors19.default.bold(newBranchName)} with all your work intact.`);
13877
- info(`Run ${import_picocolors19.default.bold("contrib update")} again to rebase onto latest ${import_picocolors19.default.bold(baseBranch)}.`, "");
13995
+ success(`You're now on ${import_picocolors20.default.bold(newBranchName)} with all your work intact.`);
13996
+ info(`Run ${import_picocolors20.default.bold("contrib update")} again to rebase onto latest ${import_picocolors20.default.bold(baseBranch)}.`, "");
13878
13997
  return;
13879
13998
  }
13880
13999
  if (await hasUncommittedChanges()) {
13881
14000
  error("You have uncommitted changes. Please commit or stash them first.");
13882
14001
  process.exit(1);
13883
14002
  }
13884
- heading("\uD83D\uDD03 contrib update");
14003
+ projectHeading("update", "\uD83D\uDD03");
13885
14004
  const mergedPR = await getMergedPRForBranch(currentBranch);
13886
14005
  if (mergedPR) {
13887
- warn(`PR #${mergedPR.number} (${import_picocolors19.default.bold(mergedPR.title)}) has already been merged.`);
13888
- info(`Link: ${import_picocolors19.default.underline(mergedPR.url)}`, "");
14006
+ warn(`PR #${mergedPR.number} (${import_picocolors20.default.bold(mergedPR.title)}) has already been merged.`);
14007
+ info(`Link: ${import_picocolors20.default.underline(mergedPR.url)}`, "");
13889
14008
  const localWork = await hasLocalWork(syncSource.remote, currentBranch);
13890
14009
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
13891
14010
  if (hasWork) {
@@ -13898,49 +14017,29 @@ var update_default = defineCommand({
13898
14017
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
13899
14018
  const DISCARD = "Discard all changes and clean up";
13900
14019
  const CANCEL = "Cancel";
13901
- const action = await selectPrompt(`${import_picocolors19.default.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
14020
+ const action = await selectPrompt(`${import_picocolors20.default.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
13902
14021
  if (action === CANCEL) {
13903
14022
  info("No changes made. You are still on your current branch.");
13904
14023
  return;
13905
14024
  }
13906
14025
  if (action === SAVE_NEW_BRANCH) {
13907
- info(import_picocolors19.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13908
- const description = await inputPrompt("What are you going to work on?");
13909
- let newBranchName = description;
13910
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13911
- const spinner = createSpinner("Generating branch name suggestion...");
13912
- const suggested = await suggestBranchName(description, args.model);
13913
- if (suggested) {
13914
- spinner.success("Branch name suggestion ready.");
13915
- console.log(`
13916
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(suggested))}`);
13917
- const accepted = await confirmPrompt(`Use ${import_picocolors19.default.bold(suggested)} as your branch name?`);
13918
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13919
- } else {
13920
- spinner.fail("AI did not return a suggestion.");
13921
- newBranchName = await inputPrompt("Enter branch name", description);
13922
- }
13923
- }
13924
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13925
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors19.default.bold(newBranchName)}:`, config.branchPrefixes);
13926
- newBranchName = formatBranchName(prefix, newBranchName);
13927
- }
13928
- if (!isValidBranchName(newBranchName)) {
13929
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13930
- process.exit(1);
14026
+ const newBranchName = await promptForBranchName({
14027
+ branchPrefixes: config.branchPrefixes,
14028
+ useAI: isAIEnabled(config, args["no-ai"]),
14029
+ model: args.model
14030
+ });
14031
+ if (!newBranchName) {
14032
+ info("No changes made. You are still on your current branch.");
14033
+ return;
13931
14034
  }
13932
14035
  const staleUpstream = await getUpstreamRef();
13933
14036
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
13934
- if (await branchExists(newBranchName)) {
13935
- error(`Branch ${import_picocolors19.default.bold(newBranchName)} already exists. Choose a different name.`);
13936
- process.exit(1);
13937
- }
13938
14037
  const renameResult = await renameBranch(currentBranch, newBranchName);
13939
14038
  if (renameResult.exitCode !== 0) {
13940
14039
  error(`Failed to rename branch: ${renameResult.stderr}`);
13941
14040
  process.exit(1);
13942
14041
  }
13943
- success(`Renamed ${import_picocolors19.default.bold(currentBranch)} → ${import_picocolors19.default.bold(newBranchName)}`);
14042
+ success(`Renamed ${import_picocolors20.default.bold(currentBranch)} → ${import_picocolors20.default.bold(newBranchName)}`);
13944
14043
  await unsetUpstream();
13945
14044
  await fetchRemote(syncSource.remote);
13946
14045
  let rebaseResult2;
@@ -13952,11 +14051,11 @@ var update_default = defineCommand({
13952
14051
  }
13953
14052
  if (rebaseResult2.exitCode !== 0) {
13954
14053
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
13955
- info(` ${import_picocolors19.default.bold("git rebase --continue")}`, "");
14054
+ info(` ${import_picocolors20.default.bold("git rebase --continue")}`, "");
13956
14055
  } else {
13957
- success(`Rebased ${import_picocolors19.default.bold(newBranchName)} onto ${import_picocolors19.default.bold(syncSource.ref)}.`);
14056
+ success(`Rebased ${import_picocolors20.default.bold(newBranchName)} onto ${import_picocolors20.default.bold(syncSource.ref)}.`);
13958
14057
  }
13959
- info(`All your changes are preserved. Run ${import_picocolors19.default.bold("contrib submit")} when ready to create a new PR.`, "");
14058
+ info(`All your changes are preserved. Run ${import_picocolors20.default.bold("contrib submit")} when ready to create a new PR.`, "");
13960
14059
  return;
13961
14060
  }
13962
14061
  warn("Discarding local changes...");
@@ -13969,30 +14068,30 @@ var update_default = defineCommand({
13969
14068
  process.exit(1);
13970
14069
  }
13971
14070
  await updateLocalBranch(baseBranch, syncSource.ref);
13972
- success(`Synced ${import_picocolors19.default.bold(baseBranch)} with ${import_picocolors19.default.bold(syncSource.ref)}.`);
13973
- info(`Deleting stale branch ${import_picocolors19.default.bold(currentBranch)}...`);
14071
+ success(`Synced ${import_picocolors20.default.bold(baseBranch)} with ${import_picocolors20.default.bold(syncSource.ref)}.`);
14072
+ info(`Deleting stale branch ${import_picocolors20.default.bold(currentBranch)}...`);
13974
14073
  await forceDeleteBranch(currentBranch);
13975
- success(`Deleted ${import_picocolors19.default.bold(currentBranch)}.`);
13976
- info(`Run ${import_picocolors19.default.bold("contrib start")} to begin a new feature branch.`, "");
14074
+ success(`Deleted ${import_picocolors20.default.bold(currentBranch)}.`);
14075
+ info(`Run ${import_picocolors20.default.bold("contrib start")} to begin a new feature branch.`, "");
13977
14076
  return;
13978
14077
  }
13979
- info(`Updating ${import_picocolors19.default.bold(currentBranch)} with latest ${import_picocolors19.default.bold(baseBranch)}...`);
14078
+ info(`Updating ${import_picocolors20.default.bold(currentBranch)} with latest ${import_picocolors20.default.bold(baseBranch)}...`);
13980
14079
  await fetchRemote(syncSource.remote);
13981
14080
  if (!await refExists(syncSource.ref)) {
13982
- error(`Remote ref ${import_picocolors19.default.bold(syncSource.ref)} does not exist.`);
14081
+ error(`Remote ref ${import_picocolors20.default.bold(syncSource.ref)} does not exist.`);
13983
14082
  error("Run `git fetch --all` and verify your remote configuration.");
13984
14083
  process.exit(1);
13985
14084
  }
13986
14085
  await updateLocalBranch(baseBranch, syncSource.ref);
13987
14086
  const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
13988
14087
  if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
13989
- info(import_picocolors19.default.dim(`Using --onto rebase (branch was based on a different ref)`));
14088
+ info(import_picocolors20.default.dim(`Using --onto rebase (branch was based on a different ref)`));
13990
14089
  }
13991
14090
  const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
13992
14091
  if (rebaseResult.exitCode !== 0) {
13993
14092
  warn("Rebase hit conflicts. Resolve them manually.");
13994
14093
  console.log();
13995
- if (!args["no-ai"]) {
14094
+ if (isAIEnabled(config, args["no-ai"])) {
13996
14095
  const copilotError = await checkCopilotAvailable();
13997
14096
  if (!copilotError) {
13998
14097
  info("Fetching AI conflict resolution suggestions...");
@@ -14015,10 +14114,10 @@ ${content.slice(0, 2000)}
14015
14114
  if (suggestion) {
14016
14115
  spinner.success("AI conflict guidance ready.");
14017
14116
  console.log(`
14018
- ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
14019
- console.log(import_picocolors19.default.dim("─".repeat(60)));
14117
+ ${import_picocolors20.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
14118
+ console.log(import_picocolors20.default.dim("─".repeat(60)));
14020
14119
  console.log(suggestion);
14021
- console.log(import_picocolors19.default.dim("─".repeat(60)));
14120
+ console.log(import_picocolors20.default.dim("─".repeat(60)));
14022
14121
  console.log();
14023
14122
  } else {
14024
14123
  spinner.fail("AI could not analyze the conflicts.");
@@ -14026,20 +14125,21 @@ ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance
14026
14125
  }
14027
14126
  }
14028
14127
  }
14029
- console.log(import_picocolors19.default.bold("To resolve:"));
14128
+ console.log(import_picocolors20.default.bold("To resolve:"));
14030
14129
  console.log(` 1. Fix conflicts in the affected files`);
14031
- console.log(` 2. ${import_picocolors19.default.cyan("git add <resolved-files>")}`);
14032
- console.log(` 3. ${import_picocolors19.default.cyan("git rebase --continue")}`);
14130
+ console.log(` 2. ${import_picocolors20.default.cyan("git add <resolved-files>")}`);
14131
+ console.log(` 3. ${import_picocolors20.default.cyan("git rebase --continue")}`);
14033
14132
  console.log();
14034
- console.log(` Or abort: ${import_picocolors19.default.cyan("git rebase --abort")}`);
14133
+ console.log(` Or abort: ${import_picocolors20.default.cyan("git rebase --abort")}`);
14035
14134
  process.exit(1);
14036
14135
  }
14037
- success(`${import_picocolors19.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors19.default.bold(baseBranch)}`);
14136
+ success(`${import_picocolors20.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors20.default.bold(baseBranch)}`);
14038
14137
  }
14039
14138
  });
14040
14139
 
14041
14140
  // src/commands/validate.ts
14042
- var import_picocolors20 = __toESM(require_picocolors(), 1);
14141
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
14142
+ import { readFileSync as readFileSync5 } from "node:fs";
14043
14143
  var validate_default = defineCommand({
14044
14144
  meta: {
14045
14145
  name: "validate",
@@ -14049,7 +14149,11 @@ var validate_default = defineCommand({
14049
14149
  message: {
14050
14150
  type: "positional",
14051
14151
  description: "The commit message to validate",
14052
- required: true
14152
+ required: false
14153
+ },
14154
+ file: {
14155
+ type: "string",
14156
+ description: "Path to a commit message file; only the first line is validated"
14053
14157
  }
14054
14158
  },
14055
14159
  async run({ args }) {
@@ -14063,14 +14167,18 @@ var validate_default = defineCommand({
14063
14167
  info('Commit convention is set to "none". All messages are accepted.');
14064
14168
  process.exit(0);
14065
14169
  }
14066
- const message = args.message;
14170
+ const message = args.file ? readFileSync5(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
14171
+ if (!message) {
14172
+ error("No commit message provided. Pass a message or use --file <path>.");
14173
+ process.exit(1);
14174
+ }
14067
14175
  if (validateCommitMessage(message, convention)) {
14068
14176
  success(`Valid ${CONVENTION_LABELS[convention]} message.`);
14069
14177
  process.exit(0);
14070
14178
  }
14071
14179
  const errors = getValidationError(convention);
14072
14180
  for (const line of errors) {
14073
- console.error(import_picocolors20.default.red(` ✗ ${line}`));
14181
+ console.error(import_picocolors21.default.red(` ✗ ${line}`));
14074
14182
  }
14075
14183
  process.exit(1);
14076
14184
  }
@@ -15465,7 +15573,7 @@ nodeFiglet.fontsSync = function() {
15465
15573
  };
15466
15574
 
15467
15575
  // src/ui/banner.ts
15468
- var import_picocolors21 = __toESM(require_picocolors(), 1);
15576
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
15469
15577
  var LOGO_BIG;
15470
15578
  try {
15471
15579
  LOGO_BIG = nodeFiglet.textSync(`Contribute
@@ -15487,19 +15595,33 @@ function getAuthor() {
15487
15595
  }
15488
15596
  function showBanner(variant = "small") {
15489
15597
  const logo = variant === "big" ? LOGO_BIG : LOGO_SMALL;
15490
- console.log(import_picocolors21.default.cyan(`
15598
+ console.log(import_picocolors22.default.cyan(`
15491
15599
  ${logo}`));
15492
- console.log(` ${import_picocolors21.default.dim(`v${getVersion()}`)} ${import_picocolors21.default.dim("—")} ${import_picocolors21.default.dim(`Built by ${getAuthor()}`)}`);
15600
+ console.log(` ${import_picocolors22.default.dim(`v${getVersion()}`)} ${import_picocolors22.default.dim("—")} ${import_picocolors22.default.dim(`Built by ${getAuthor()}`)}`);
15493
15601
  if (variant === "big") {
15494
15602
  console.log();
15495
- console.log(` ${import_picocolors21.default.yellow("Star")} ${import_picocolors21.default.cyan("https://github.com/warengonzaga/contribute-now")}`);
15496
- console.log(` ${import_picocolors21.default.green("Contribute")} ${import_picocolors21.default.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
15497
- console.log(` ${import_picocolors21.default.magenta("Sponsor")} ${import_picocolors21.default.cyan("https://warengonzaga.com/sponsor")}`);
15603
+ console.log(` ${import_picocolors22.default.yellow("Star")} ${import_picocolors22.default.cyan("https://github.com/warengonzaga/contribute-now")}`);
15604
+ console.log(` ${import_picocolors22.default.green("Contribute")} ${import_picocolors22.default.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
15605
+ console.log(` ${import_picocolors22.default.magenta("Sponsor")} ${import_picocolors22.default.cyan("https://warengonzaga.com/sponsor")}`);
15498
15606
  }
15499
15607
  console.log();
15500
15608
  }
15501
15609
 
15502
15610
  // src/index.ts
15611
+ function normalizeCliArgs(argv2) {
15612
+ return argv2.map((arg, index) => {
15613
+ const previous = argv2[index - 1];
15614
+ const isSubmitCommand = previous === "submit" || argv2.includes("submit");
15615
+ if (!isSubmitCommand) {
15616
+ return arg;
15617
+ }
15618
+ if (arg === "-pr" || arg === "--pr") {
15619
+ return "--pullrequest";
15620
+ }
15621
+ return arg;
15622
+ });
15623
+ }
15624
+ process.argv = normalizeCliArgs(process.argv);
15503
15625
  var isVersion = process.argv.includes("--version") || process.argv.includes("-v");
15504
15626
  if (!isVersion) {
15505
15627
  const subCommands = [