@rotorsoft/gent 1.15.3 → 1.17.0

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
@@ -2221,7 +2221,7 @@ import { homedir } from "os";
2221
2221
  // package.json
2222
2222
  var package_default = {
2223
2223
  name: "@rotorsoft/gent",
2224
- version: "1.15.3",
2224
+ version: "1.17.0",
2225
2225
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
2226
2226
  keywords: [
2227
2227
  "cli",
@@ -2646,6 +2646,7 @@ async function statusCommand() {
2646
2646
  import { execa as execa4 } from "execa";
2647
2647
 
2648
2648
  // src/tui/state.ts
2649
+ var envCache = null;
2649
2650
  async function aggregateState() {
2650
2651
  const isGitRepo = await checkGitRepo();
2651
2652
  if (!isGitRepo) {
@@ -2675,16 +2676,20 @@ async function aggregateState() {
2675
2676
  }
2676
2677
  const config = loadConfig();
2677
2678
  const workflowLabels = getWorkflowLabels(config);
2678
- const [
2679
- isGhAuthenticated,
2680
- isAIProviderAvailable,
2681
- branch,
2682
- isOnMain,
2683
- uncommitted,
2684
- baseBranch
2685
- ] = await Promise.all([
2686
- checkGhAuth(),
2687
- checkAIProvider(config.ai.provider),
2679
+ if (!envCache) {
2680
+ const [ghAuth, aiAvail] = await Promise.all([
2681
+ checkGhAuth(),
2682
+ checkAIProvider(config.ai.provider)
2683
+ ]);
2684
+ envCache = {
2685
+ isGhAuthenticated: ghAuth,
2686
+ isAIProviderAvailable: aiAvail,
2687
+ isPlaywrightAvailable: false
2688
+ // resolved below on first feature branch visit
2689
+ };
2690
+ }
2691
+ const { isGhAuthenticated, isAIProviderAvailable } = envCache;
2692
+ const [branch, isOnMain, uncommitted, baseBranch] = await Promise.all([
2688
2693
  getCurrentBranch(),
2689
2694
  isOnMainBranch(),
2690
2695
  hasUncommittedChanges(),
@@ -2706,16 +2711,20 @@ async function aggregateState() {
2706
2711
  let playwrightAvailable = false;
2707
2712
  if (!isOnMain) {
2708
2713
  const issueNumber = extractIssueNumber(branch);
2714
+ const needsPlaywrightCheck = !envCache.isPlaywrightAvailable;
2709
2715
  const [issueResult, prResult, changedFiles, playwrightResult] = await Promise.all([
2710
2716
  issueNumber ? getIssue(issueNumber).catch(() => null) : Promise.resolve(null),
2711
2717
  getPrStatus().catch(() => null),
2712
2718
  getChangedFiles(baseBranch),
2713
- isPlaywrightAvailable()
2719
+ needsPlaywrightCheck ? isPlaywrightAvailable() : Promise.resolve(envCache.isPlaywrightAvailable)
2714
2720
  ]);
2715
2721
  issue = issueResult;
2716
2722
  pr = prResult;
2717
2723
  uiChanges = hasUIChanges(changedFiles);
2718
2724
  playwrightAvailable = playwrightResult;
2725
+ if (needsPlaywrightCheck) {
2726
+ envCache.isPlaywrightAvailable = playwrightResult;
2727
+ }
2719
2728
  if (issue) {
2720
2729
  if (issue.labels.includes(workflowLabels.ready)) {
2721
2730
  workflowStatus = "ready";
@@ -2935,14 +2944,24 @@ function renderActionPanel(title, content) {
2935
2944
  }
2936
2945
  console.log(botRow(w));
2937
2946
  }
2938
- function renderSettingsTo(state, w, out) {
2947
+ function renderSettingsTo(state, w, out, versionCheck) {
2939
2948
  const provider = getProviderDisplayName(state.config.ai.provider);
2940
2949
  const provTag = state.isAIProviderAvailable ? chalk3.green(provider) : chalk3.red(provider);
2941
2950
  const ghTag = state.isGhAuthenticated ? chalk3.green("authenticated") : chalk3.red("not authenticated");
2942
2951
  out(row(chalk3.dim("Provider: ") + provTag, w));
2943
2952
  out(row(chalk3.dim("GitHub: ") + ghTag, w));
2953
+ if (versionCheck?.updateAvailable && versionCheck.latestVersion) {
2954
+ out(
2955
+ row(
2956
+ chalk3.yellow(
2957
+ `Update available: ${versionCheck.currentVersion} \u2192 ${versionCheck.latestVersion}`
2958
+ ) + chalk3.dim(' \u2014 run "npm install -g @rotorsoft/gent" to upgrade'),
2959
+ w
2960
+ )
2961
+ );
2962
+ }
2944
2963
  }
2945
- function buildDashboardLines(state, actions, hint, refreshing) {
2964
+ function buildDashboardLines(state, actions, hint, refreshing, versionCheck) {
2946
2965
  const lines = [];
2947
2966
  const out = (line) => lines.push(line);
2948
2967
  const w = termWidth();
@@ -2950,7 +2969,7 @@ function buildDashboardLines(state, actions, hint, refreshing) {
2950
2969
  const version2 = getVersion();
2951
2970
  const titleLabel = `gent v${version2}`;
2952
2971
  out(topRow(titleLabel, w));
2953
- renderSettingsTo(state, w, out);
2972
+ renderSettingsTo(state, w, out, versionCheck);
2954
2973
  if (!state.isGitRepo) {
2955
2974
  out(row(chalk3.red("Not a git repository"), w));
2956
2975
  out(row(chalk3.dim("Run gent init in a git repo to get started"), w));
@@ -3071,8 +3090,8 @@ function buildDashboardLines(state, actions, hint, refreshing) {
3071
3090
  out(botRow(w));
3072
3091
  return lines;
3073
3092
  }
3074
- function renderDashboard(state, actions, hint, refreshing) {
3075
- const lines = buildDashboardLines(state, actions, hint, refreshing);
3093
+ function renderDashboard(state, actions, hint, refreshing, versionCheck) {
3094
+ const lines = buildDashboardLines(state, actions, hint, refreshing, versionCheck);
3076
3095
  for (const line of lines) {
3077
3096
  console.log(line);
3078
3097
  }
@@ -3119,7 +3138,7 @@ function buildModalFrame(title, contentLines, footerText, width) {
3119
3138
  function isSeparator(entry) {
3120
3139
  return "separator" in entry;
3121
3140
  }
3122
- function buildSelectContent(items, selectedIndex, maxWidth) {
3141
+ function buildSelectContent(items, selectedIndex, maxWidth, currentIndex) {
3123
3142
  const lines = [];
3124
3143
  let selectableIdx = 0;
3125
3144
  for (const item of items) {
@@ -3127,12 +3146,12 @@ function buildSelectContent(items, selectedIndex, maxWidth) {
3127
3146
  lines.push(chalk4.dim(item.separator));
3128
3147
  } else {
3129
3148
  const isSelected = selectableIdx === selectedIndex;
3149
+ const isCurrent = currentIndex != null && selectableIdx === currentIndex;
3130
3150
  const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
3131
3151
  const bullet = chalk4.dim("\xB7 ");
3132
3152
  const label = truncateAnsi(item.name, maxWidth - 4);
3133
- lines.push(
3134
- prefix + bullet + (isSelected ? chalk4.bold(label) : label)
3135
- );
3153
+ const styledLabel = isSelected ? chalk4.bold(label) : isCurrent ? chalk4.cyan(label) : label;
3154
+ lines.push(prefix + bullet + styledLabel);
3136
3155
  selectableIdx++;
3137
3156
  }
3138
3157
  }
@@ -3301,9 +3320,9 @@ async function showSelect(opts) {
3301
3320
  const w = modalWidth();
3302
3321
  const maxItems = selectableCount(opts.items);
3303
3322
  if (maxItems === 0) return null;
3304
- let selectedIndex = 0;
3323
+ let selectedIndex = opts.initialIndex ?? 0;
3305
3324
  const render = () => {
3306
- const content = buildSelectContent(opts.items, selectedIndex, w - 6);
3325
+ const content = buildSelectContent(opts.items, selectedIndex, w - 6, opts.currentIndex);
3307
3326
  const footer = "\u2191\u2193 Navigate Enter Select Esc Cancel";
3308
3327
  const lines = buildModalFrame(opts.title, content, footer, w);
3309
3328
  renderOverlay(opts.dashboardLines, lines, w);
@@ -3706,6 +3725,8 @@ async function handleList(dashboardLines) {
3706
3725
  const currentBranch = await getCurrentBranch();
3707
3726
  const defaultBranch = await getDefaultBranch();
3708
3727
  const items = [];
3728
+ let initialIndex = 0;
3729
+ let selectableIdx = 0;
3709
3730
  const isMain = currentBranch === defaultBranch;
3710
3731
  const mainLabel = defaultBranch + (isMain ? " (current)" : "");
3711
3732
  if (dirty && !isMain) {
@@ -3716,42 +3737,42 @@ async function handleList(dashboardLines) {
3716
3737
  } else {
3717
3738
  items.push({ name: mainLabel, value: "__main__" });
3718
3739
  }
3740
+ if (isMain) initialIndex = selectableIdx;
3741
+ selectableIdx++;
3719
3742
  const inProgressChoices = choices.filter(
3720
3743
  (c) => c.category === "in-progress"
3721
3744
  );
3722
3745
  const openPrChoices = choices.filter((c) => c.category === "open-pr");
3723
3746
  const readyChoices = choices.filter((c) => c.category === "ready");
3724
- if (inProgressChoices.length > 0) {
3725
- items.push({ separator: "\u2500\u2500 In Progress \u2500\u2500" });
3726
- for (const c of inProgressChoices) {
3747
+ const addChoices = (list) => {
3748
+ for (const c of list) {
3749
+ const isCurrent = c.branch === currentBranch;
3727
3750
  items.push({
3728
3751
  name: `#${c.issueNumber} ${c.title}`,
3729
3752
  value: String(c.issueNumber)
3730
3753
  });
3754
+ if (isCurrent) initialIndex = selectableIdx;
3755
+ selectableIdx++;
3731
3756
  }
3757
+ };
3758
+ if (inProgressChoices.length > 0) {
3759
+ items.push({ separator: "\u2500\u2500 In Progress \u2500\u2500" });
3760
+ addChoices(inProgressChoices);
3732
3761
  }
3733
3762
  if (openPrChoices.length > 0) {
3734
3763
  items.push({ separator: "\u2500\u2500 Open PRs \u2500\u2500" });
3735
- for (const c of openPrChoices) {
3736
- items.push({
3737
- name: `#${c.issueNumber} ${c.title}`,
3738
- value: String(c.issueNumber)
3739
- });
3740
- }
3764
+ addChoices(openPrChoices);
3741
3765
  }
3742
3766
  if (readyChoices.length > 0) {
3743
3767
  items.push({ separator: "\u2500\u2500 Ready \u2500\u2500" });
3744
- for (const c of readyChoices) {
3745
- items.push({
3746
- name: `#${c.issueNumber} ${c.title}`,
3747
- value: String(c.issueNumber)
3748
- });
3749
- }
3768
+ addChoices(readyChoices);
3750
3769
  }
3751
3770
  const selected = await showSelect({
3752
3771
  title: "Switch Ticket",
3753
3772
  items,
3754
- dashboardLines
3773
+ dashboardLines,
3774
+ initialIndex,
3775
+ currentIndex: initialIndex
3755
3776
  });
3756
3777
  if (!selected) return false;
3757
3778
  if (selected === "__main_disabled__") {
@@ -3828,10 +3849,12 @@ async function offerCreateBranch2(issueNumber, title, dashboardLines) {
3828
3849
  await createBranch(branchName, defaultBranch);
3829
3850
  return true;
3830
3851
  }
3852
+ var VERSION_CHECK_INTERVAL_MS = 30 * 60 * 1e3;
3831
3853
  async function tuiCommand() {
3832
3854
  let running = true;
3833
3855
  let lastActions = [];
3834
3856
  let lastDashboardLines = [];
3857
+ let versionCheck = null;
3835
3858
  const config = loadConfig();
3836
3859
  let lastState = {
3837
3860
  isGitRepo: true,
@@ -3856,14 +3879,21 @@ async function tuiCommand() {
3856
3879
  isPlaywrightAvailable: false
3857
3880
  };
3858
3881
  let needsRefresh = true;
3882
+ let isFirstLoad = true;
3859
3883
  while (running) {
3860
3884
  if (needsRefresh) {
3861
3885
  clearScreen();
3862
- renderDashboard(lastState, lastActions, void 0, true);
3863
- const state = await aggregateState();
3886
+ renderDashboard(lastState, lastActions, void 0, true, versionCheck);
3887
+ const checkInterval = isFirstLoad ? 0 : VERSION_CHECK_INTERVAL_MS;
3888
+ isFirstLoad = false;
3889
+ const [state, versionResult] = await Promise.all([
3890
+ aggregateState(),
3891
+ checkForUpdates(checkInterval).catch(() => null)
3892
+ ]);
3864
3893
  const actions = getAvailableActions(state);
3865
3894
  lastState = state;
3866
3895
  lastActions = actions;
3896
+ if (versionResult) versionCheck = versionResult;
3867
3897
  }
3868
3898
  let hint;
3869
3899
  if (lastState.isOnMain) {
@@ -3873,7 +3903,7 @@ async function tuiCommand() {
3873
3903
  } else if (lastState.hasActionableFeedback) {
3874
3904
  hint = "Review feedback needs attention";
3875
3905
  }
3876
- lastDashboardLines = buildDashboardLines(lastState, lastActions, hint);
3906
+ lastDashboardLines = buildDashboardLines(lastState, lastActions, hint, false, versionCheck);
3877
3907
  clearScreen();
3878
3908
  for (const line of lastDashboardLines) {
3879
3909
  console.log(line);