@rotorsoft/gent 1.15.1 → 1.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -212,6 +212,17 @@ import chalk from "chalk";
212
212
  // src/lib/ai-provider.ts
213
213
  import { spawn } from "child_process";
214
214
  import { execa } from "execa";
215
+ async function getOtherAvailableProviders(currentProvider) {
216
+ const allProviders = ["claude", "gemini", "codex"];
217
+ const others = allProviders.filter((p) => p !== currentProvider);
218
+ const available = [];
219
+ for (const p of others) {
220
+ if (await checkAIProvider(p)) {
221
+ available.push(p);
222
+ }
223
+ }
224
+ return available;
225
+ }
215
226
  async function invokeInternal(provider, options) {
216
227
  switch (provider) {
217
228
  case "claude":
@@ -620,11 +631,10 @@ async function createCommand(description, options) {
620
631
  logger.bold("Creating AI-enhanced ticket...");
621
632
  logger.newline();
622
633
  const config = loadConfig();
623
- const provider = resolveProvider(options, config);
624
- const providerName = getProviderDisplayName(provider);
634
+ let currentProvider = resolveProvider(options, config);
625
635
  const [ghAuth, aiOk] = await Promise.all([
626
636
  checkGhAuth(),
627
- checkAIProvider(provider)
637
+ checkAIProvider(currentProvider)
628
638
  ]);
629
639
  if (!ghAuth) {
630
640
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
@@ -632,7 +642,7 @@ async function createCommand(description, options) {
632
642
  }
633
643
  if (!aiOk) {
634
644
  logger.error(
635
- `${providerName} CLI not found. Please install ${provider} CLI first.`
645
+ `${getProviderDisplayName(currentProvider)} CLI not found. Please install ${currentProvider} CLI first.`
636
646
  );
637
647
  return;
638
648
  }
@@ -640,6 +650,7 @@ async function createCommand(description, options) {
640
650
  let aiOutput;
641
651
  let additionalHints = null;
642
652
  while (true) {
653
+ const providerName = getProviderDisplayName(currentProvider);
643
654
  const prompt = buildTicketPrompt(
644
655
  description,
645
656
  agentInstructions,
@@ -655,7 +666,7 @@ async function createCommand(description, options) {
655
666
  const result = await invokeAI(
656
667
  { prompt, streamOutput: true },
657
668
  config,
658
- options.provider
669
+ currentProvider
659
670
  );
660
671
  aiOutput = result.output;
661
672
  logger.newline();
@@ -666,6 +677,33 @@ async function createCommand(description, options) {
666
677
  );
667
678
  logger.newline();
668
679
  } catch (error) {
680
+ if (error && typeof error === "object" && "rateLimited" in error) {
681
+ logger.warning(`${providerName} is rate limited.`);
682
+ const others = await getOtherAvailableProviders(currentProvider);
683
+ if (others.length > 0) {
684
+ const { nextProvider } = await inquirer2.prompt([
685
+ {
686
+ type: "list",
687
+ name: "nextProvider",
688
+ message: "Would you like to try another provider?",
689
+ choices: [
690
+ ...others.map((p) => ({
691
+ name: `Switch to ${getProviderDisplayName(p)}`,
692
+ value: p
693
+ })),
694
+ { name: "Cancel", value: "cancel" }
695
+ ]
696
+ }
697
+ ]);
698
+ if (nextProvider !== "cancel") {
699
+ currentProvider = nextProvider;
700
+ logger.info(
701
+ `Switching to ${getProviderDisplayName(currentProvider)}...`
702
+ );
703
+ continue;
704
+ }
705
+ }
706
+ }
669
707
  logger.error(`${providerName} invocation failed: ${error}`);
670
708
  return;
671
709
  }
@@ -1600,15 +1638,15 @@ async function getChangedFiles(baseBranch = "main") {
1600
1638
  }
1601
1639
 
1602
1640
  // src/commands/pr.ts
1641
+ import inquirer5 from "inquirer";
1603
1642
  async function prCommand(options) {
1604
1643
  logger.bold("Creating AI-enhanced pull request...");
1605
1644
  logger.newline();
1606
1645
  const config = loadConfig();
1607
- const provider = options.provider ?? config.ai.provider;
1608
- const providerName = getProviderDisplayName(provider);
1646
+ let currentProvider = options.provider ?? config.ai.provider;
1609
1647
  const [ghAuth, aiOk] = await Promise.all([
1610
1648
  checkGhAuth(),
1611
- checkAIProvider(provider)
1649
+ checkAIProvider(currentProvider)
1612
1650
  ]);
1613
1651
  if (!ghAuth) {
1614
1652
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
@@ -1616,7 +1654,7 @@ async function prCommand(options) {
1616
1654
  }
1617
1655
  if (!aiOk) {
1618
1656
  logger.error(
1619
- `${providerName} CLI not found. Please install ${provider} CLI first.`
1657
+ `${getProviderDisplayName(currentProvider)} CLI not found. Please install ${currentProvider} CLI first.`
1620
1658
  );
1621
1659
  return;
1622
1660
  }
@@ -1692,26 +1730,59 @@ IMPORTANT: This PR contains UI changes. Use the Playwright MCP plugin to:
1692
1730
  }
1693
1731
  const prompt = buildPrPrompt(issue, commits, diffSummary) + captureVideoInstructions;
1694
1732
  let prBody;
1695
- try {
1696
- logger.info(
1697
- `Generating PR description with ${colors.provider(providerName)}...`
1698
- );
1699
- logger.newline();
1700
- const result = await invokeAI(
1701
- { prompt, streamOutput: true },
1702
- config,
1703
- options.provider
1704
- );
1705
- prBody = result.output;
1706
- logger.newline();
1707
- } catch (error) {
1708
- logger.warning(`${providerName} invocation failed: ${error}`);
1709
- prBody = generateFallbackBody(issue, commits);
1733
+ let usedProvider = currentProvider;
1734
+ while (true) {
1735
+ const providerName = getProviderDisplayName(usedProvider);
1736
+ try {
1737
+ logger.info(
1738
+ `Generating PR description with ${colors.provider(providerName)}...`
1739
+ );
1740
+ logger.newline();
1741
+ const result = await invokeAI(
1742
+ { prompt, streamOutput: true },
1743
+ config,
1744
+ usedProvider
1745
+ );
1746
+ prBody = result.output;
1747
+ logger.newline();
1748
+ break;
1749
+ } catch (error) {
1750
+ if (error && typeof error === "object" && "rateLimited" in error) {
1751
+ logger.warning(`${providerName} is rate limited.`);
1752
+ const others = await getOtherAvailableProviders(usedProvider);
1753
+ if (others.length > 0) {
1754
+ const { nextProvider } = await inquirer5.prompt([
1755
+ {
1756
+ type: "list",
1757
+ name: "nextProvider",
1758
+ message: "Would you like to try another provider?",
1759
+ choices: [
1760
+ ...others.map((p) => ({
1761
+ name: `Switch to ${getProviderDisplayName(p)}`,
1762
+ value: p
1763
+ })),
1764
+ { name: "Fall back to basic description", value: "fallback" }
1765
+ ]
1766
+ }
1767
+ ]);
1768
+ if (nextProvider !== "fallback") {
1769
+ usedProvider = nextProvider;
1770
+ logger.info(
1771
+ `Switching to ${getProviderDisplayName(usedProvider)}...`
1772
+ );
1773
+ continue;
1774
+ }
1775
+ }
1776
+ }
1777
+ logger.warning(`${providerName} invocation failed: ${error}`);
1778
+ prBody = generateFallbackBody(issue, commits);
1779
+ break;
1780
+ }
1710
1781
  }
1711
1782
  prBody += `
1712
1783
 
1713
1784
  ---
1714
- *Created with ${providerName} by [gent](https://github.com/Rotorsoft/gent)*`;
1785
+ *Created with ${getProviderDisplayName(usedProvider)} by [gent](https://github.com/Rotorsoft/gent)*`;
1715
1786
  const prTitle = issue?.title || commits[0] || currentBranch;
1716
1787
  let prUrl;
1717
1788
  try {
@@ -1793,7 +1864,7 @@ function generateFallbackBody(issue, commits) {
1793
1864
  }
1794
1865
 
1795
1866
  // src/commands/fix.ts
1796
- import inquirer5 from "inquirer";
1867
+ import inquirer6 from "inquirer";
1797
1868
 
1798
1869
  // src/lib/review-feedback.ts
1799
1870
  var ACTIONABLE_KEYWORDS = [
@@ -1990,7 +2061,7 @@ async function fixCommand(options) {
1990
2061
  const hasChanges = await hasUncommittedChanges();
1991
2062
  if (hasChanges) {
1992
2063
  logger.warning("You have uncommitted changes.");
1993
- const { proceed } = await inquirer5.prompt([
2064
+ const { proceed } = await inquirer6.prompt([
1994
2065
  {
1995
2066
  type: "confirm",
1996
2067
  name: "proceed",
@@ -2150,7 +2221,7 @@ import { homedir } from "os";
2150
2221
  // package.json
2151
2222
  var package_default = {
2152
2223
  name: "@rotorsoft/gent",
2153
- version: "1.15.1",
2224
+ version: "1.15.3",
2154
2225
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
2155
2226
  keywords: [
2156
2227
  "cli",
@@ -2700,25 +2771,20 @@ function getAvailableActions(state) {
2700
2771
  actions.push({ id: "quit", label: "quit", shortcut: "q" });
2701
2772
  return actions;
2702
2773
  }
2703
- if (state.isOnMain) {
2704
- actions.push({ id: "create", label: "new", shortcut: "n" });
2705
- actions.push({ id: "list", label: "list", shortcut: "l" });
2706
- actions.push({ id: "refresh", label: "refresh", shortcut: "f" });
2707
- actions.push({ id: "switch-provider", label: "ai", shortcut: "a" });
2708
- actions.push({ id: "quit", label: "quit", shortcut: "q" });
2709
- return actions;
2710
- }
2711
- if (state.hasUncommittedChanges) {
2712
- actions.push({ id: "commit", label: "commit", shortcut: "c" });
2713
- }
2714
- if (state.hasUnpushedCommits && state.commits.length > 0) {
2715
- actions.push({ id: "push", label: "push", shortcut: "p" });
2716
- }
2717
- if (!state.pr && state.commits.length > 0) {
2718
- actions.push({ id: "pr", label: "pr", shortcut: "r" });
2719
- }
2720
- if (state.issue && state.pr?.state !== "merged") {
2721
- actions.push({ id: "run", label: "run", shortcut: "u" });
2774
+ actions.push({ id: "create", label: "new", shortcut: "n" });
2775
+ if (!state.isOnMain) {
2776
+ if (state.hasUncommittedChanges) {
2777
+ actions.push({ id: "commit", label: "commit", shortcut: "c" });
2778
+ }
2779
+ if (state.hasUnpushedCommits && state.commits.length > 0) {
2780
+ actions.push({ id: "push", label: "push", shortcut: "p" });
2781
+ }
2782
+ if (!state.pr && state.commits.length > 0) {
2783
+ actions.push({ id: "pr", label: "pr", shortcut: "r" });
2784
+ }
2785
+ if (state.issue && state.pr?.state !== "merged") {
2786
+ actions.push({ id: "run", label: "run", shortcut: "u" });
2787
+ }
2722
2788
  }
2723
2789
  actions.push({ id: "list", label: "list", shortcut: "l" });
2724
2790
  actions.push({ id: "refresh", label: "refresh", shortcut: "f" });
@@ -2731,6 +2797,23 @@ function getAvailableActions(state) {
2731
2797
  import chalk3 from "chalk";
2732
2798
  var stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, "");
2733
2799
  var visibleLen = (str) => stripAnsi(str).length;
2800
+ function truncateAnsi(text, max) {
2801
+ if (visibleLen(text) <= max) return text;
2802
+ let visible = 0;
2803
+ let i = 0;
2804
+ while (i < text.length && visible < max - 1) {
2805
+ if (text[i] === "\x1B") {
2806
+ const end = text.indexOf("m", i);
2807
+ if (end !== -1) {
2808
+ i = end + 1;
2809
+ continue;
2810
+ }
2811
+ }
2812
+ visible++;
2813
+ i++;
2814
+ }
2815
+ return text.slice(0, i) + "\x1B[0m\u2026";
2816
+ }
2734
2817
  function termWidth() {
2735
2818
  return Math.min(process.stdout.columns || 80, 90);
2736
2819
  }
@@ -2770,8 +2853,9 @@ function botRow(w) {
2770
2853
  }
2771
2854
  function row(text, w) {
2772
2855
  const inner = w - 4;
2773
- const pad = Math.max(0, inner - visibleLen(text));
2774
- return chalk3.dim("\u2502") + " " + text + " ".repeat(pad) + " " + chalk3.dim("\u2502");
2856
+ const fitted = truncateAnsi(text, inner);
2857
+ const pad = Math.max(0, inner - visibleLen(fitted));
2858
+ return chalk3.dim("\u2502") + " " + fitted + " ".repeat(pad) + " " + chalk3.dim("\u2502");
2775
2859
  }
2776
2860
  function workflowBadge(status) {
2777
2861
  switch (status) {
@@ -2887,12 +2971,12 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2887
2971
  if (state.issue) {
2888
2972
  out(
2889
2973
  row(
2890
- chalk3.cyan(`#${state.issue.number}`) + " " + chalk3.bold(truncate(state.issue.title, descMax - 6)),
2974
+ chalk3.dim("\xB7 ") + chalk3.cyan(`#${state.issue.number}`) + " " + chalk3.bold(state.issue.title),
2891
2975
  w
2892
2976
  )
2893
2977
  );
2894
2978
  const desc = extractDescription(state.issue.body, descMax);
2895
- if (desc) out(row(chalk3.dim(desc), w));
2979
+ if (desc) out(row(" " + chalk3.dim(desc), w));
2896
2980
  const tags = [];
2897
2981
  if (state.workflowStatus !== "none")
2898
2982
  tags.push(workflowBadge(state.workflowStatus));
@@ -2900,13 +2984,13 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2900
2984
  const l = state.issue.labels.find((x) => x.startsWith(prefix));
2901
2985
  if (l) tags.push(chalk3.dim(l));
2902
2986
  }
2903
- if (tags.length) out(row(tags.join(" "), w));
2987
+ if (tags.length) out(row(" " + tags.join(" "), w));
2904
2988
  } else {
2905
- out(row(chalk3.dim("No linked issue"), w));
2989
+ out(row(chalk3.dim(" No linked issue"), w));
2906
2990
  }
2907
2991
  }
2908
2992
  section("Branch");
2909
- let branchLine = chalk3.magenta(state.branch);
2993
+ let branchLine = chalk3.dim("\xB7 ") + chalk3.magenta(state.branch);
2910
2994
  if (state.isOnMain && !state.hasUncommittedChanges) {
2911
2995
  branchLine += chalk3.dim(" \xB7 ready to start new work");
2912
2996
  }
@@ -2919,15 +3003,20 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2919
3003
  if (!state.hasUncommittedChanges && !state.hasUnpushedCommits && state.commits.length > 0) {
2920
3004
  bits.push(chalk3.green("\u25CF synced"));
2921
3005
  }
2922
- if (bits.length) out(row(bits.join(chalk3.dim(" \xB7 ")), w));
3006
+ if (bits.length) out(row(" " + bits.join(chalk3.dim(" \xB7 ")), w));
2923
3007
  if (state.pr || !state.isOnMain) {
2924
3008
  section("Pull Request");
2925
3009
  if (state.pr) {
2926
- const titleText = state.pr.title ? " " + truncate(state.pr.title, descMax - 12) : "";
2927
- out(row(chalk3.cyan(`#${state.pr.number}`) + titleText, w));
3010
+ const titleText = state.pr.title ? " " + state.pr.title : "";
3011
+ out(
3012
+ row(
3013
+ chalk3.dim("\xB7 ") + chalk3.cyan(`#${state.pr.number}`) + titleText,
3014
+ w
3015
+ )
3016
+ );
2928
3017
  out(
2929
3018
  row(
2930
- prBadge(state.pr.state, state.pr.isDraft) + reviewBadge(state.pr.reviewDecision),
3019
+ " " + prBadge(state.pr.state, state.pr.isDraft) + reviewBadge(state.pr.reviewDecision),
2931
3020
  w
2932
3021
  )
2933
3022
  );
@@ -2935,7 +3024,7 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2935
3024
  const n = state.reviewFeedback.length;
2936
3025
  out(
2937
3026
  row(
2938
- chalk3.yellow(`${n} actionable comment${n !== 1 ? "s" : ""} pending`),
3027
+ " " + chalk3.yellow(`${n} actionable comment${n !== 1 ? "s" : ""} pending`),
2939
3028
  w
2940
3029
  )
2941
3030
  );
@@ -2943,14 +3032,14 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2943
3032
  if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled && state.pr.state === "open") {
2944
3033
  out(
2945
3034
  row(
2946
- chalk3.cyan("UI changes detected") + chalk3.dim(" \xB7 video capture available"),
3035
+ " " + chalk3.cyan("UI changes detected") + chalk3.dim(" \xB7 video capture available"),
2947
3036
  w
2948
3037
  )
2949
3038
  );
2950
3039
  }
2951
- out(row(chalk3.dim(state.pr.url), w));
3040
+ out(row(" " + chalk3.dim(state.pr.url), w));
2952
3041
  } else {
2953
- out(row(chalk3.dim("No PR created"), w));
3042
+ out(row(chalk3.dim(" No PR created"), w));
2954
3043
  }
2955
3044
  }
2956
3045
  if (state.commits.length > 0 || !state.isOnMain) {
@@ -2958,13 +3047,13 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2958
3047
  if (state.commits.length > 0) {
2959
3048
  const max = 6;
2960
3049
  for (const c of state.commits.slice(0, max)) {
2961
- out(row(c.substring(0, w - 5), w));
3050
+ out(row(chalk3.dim("\xB7 ") + c, w));
2962
3051
  }
2963
3052
  if (state.commits.length > max) {
2964
- out(row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w));
3053
+ out(row(chalk3.dim(` \u2026 and ${state.commits.length - max} more`), w));
2965
3054
  }
2966
3055
  } else {
2967
- out(row(chalk3.dim("No commits"), w));
3056
+ out(row(chalk3.dim(" No commits"), w));
2968
3057
  }
2969
3058
  }
2970
3059
  if (hint) {
@@ -3005,23 +3094,6 @@ function modalDivRow(w) {
3005
3094
  function modalBotRow(w) {
3006
3095
  return chalk4.bold("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
3007
3096
  }
3008
- function truncateAnsi(text, max) {
3009
- if (visibleLen(text) <= max) return text;
3010
- let visible = 0;
3011
- let i = 0;
3012
- while (i < text.length && visible < max - 1) {
3013
- if (text[i] === "\x1B") {
3014
- const end = text.indexOf("m", i);
3015
- if (end !== -1) {
3016
- i = end + 1;
3017
- continue;
3018
- }
3019
- }
3020
- visible++;
3021
- i++;
3022
- }
3023
- return text.slice(0, i) + "\x1B[0m\u2026";
3024
- }
3025
3097
  function modalRow(text, w) {
3026
3098
  const inner = w - 4;
3027
3099
  const fitted = truncateAnsi(text, inner);
@@ -3056,9 +3128,10 @@ function buildSelectContent(items, selectedIndex, maxWidth) {
3056
3128
  } else {
3057
3129
  const isSelected = selectableIdx === selectedIndex;
3058
3130
  const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
3059
- const label = item.name.length > maxWidth - 2 ? item.name.slice(0, maxWidth - 3) + "\u2026" : item.name;
3131
+ const bullet = chalk4.dim("\xB7 ");
3132
+ const label = truncateAnsi(item.name, maxWidth - 4);
3060
3133
  lines.push(
3061
- prefix + (isSelected ? chalk4.bold(label) : label)
3134
+ prefix + bullet + (isSelected ? chalk4.bold(label) : label)
3062
3135
  );
3063
3136
  selectableIdx++;
3064
3137
  }