@gtrabanco/pi-web-github-pr-status 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project are documented here. The project follows [strict semantic versioning](./RELEASE-POLICY.md); the kind of the next release is registered in `package.json` (`nextRelease`) before `bun run publish` is executed.
4
4
 
5
+ ## 0.1.1 — 2026-09-08 (Patch release)
6
+
7
+ - See commit history for details.
8
+
5
9
  ## 0.1.0 — initial development version
6
10
 
7
11
  - GitHub PR workspace label: PR number link, CI ball (green/orange/red/none), dirty-worktree marker, push/pull arrows.
package/dist/index.js CHANGED
@@ -299,12 +299,15 @@ async function readAllProbeFiles(readFile) {
299
299
  var DEFAULT_SETTINGS = Object.freeze({
300
300
  showCI: true,
301
301
  refreshSeconds: 90,
302
+ adaptiveRefresh: true,
302
303
  merge: Object.freeze({
303
304
  enabled: true,
304
305
  method: "merge",
305
306
  requireCleanWorktree: true,
306
307
  requireCI: true,
307
- deleteBranch: false
308
+ deleteBranch: false,
309
+ checkoutTarget: true,
310
+ deleteBranchAfterMerge: true
308
311
  })
309
312
  });
310
313
  var MERGE_METHODS = ["merge", "squash", "rebase"];
@@ -341,6 +344,8 @@ function mergeBooleans(raw, defaults, warnings, prefix) {
341
344
  requireCleanWorktree: pick("requireCleanWorktree"),
342
345
  requireCI: pick("requireCI"),
343
346
  deleteBranch: pick("deleteBranch"),
347
+ checkoutTarget: pick("checkoutTarget"),
348
+ deleteBranchAfterMerge: pick("deleteBranchAfterMerge"),
344
349
  method: normalizeMethod(raw.method, defaults.method, warnings)
345
350
  };
346
351
  }
@@ -369,10 +374,14 @@ function normalizeSettings(raw) {
369
374
  const showCI = coerceBoolean(source.showCI);
370
375
  if (showCI === undefined && source.showCI !== undefined)
371
376
  warnings.push("showCI: ignored invalid value");
377
+ const adaptiveRefresh = coerceBoolean(source.adaptiveRefresh);
378
+ if (adaptiveRefresh === undefined && source.adaptiveRefresh !== undefined)
379
+ warnings.push("adaptiveRefresh: ignored invalid value");
372
380
  const mergeSource = asObject(source.merge);
373
381
  const settings = {
374
382
  showCI: showCI ?? DEFAULT_SETTINGS.showCI,
375
383
  refreshSeconds: normalizeRefreshSeconds(source.refreshSeconds, DEFAULT_SETTINGS.refreshSeconds, warnings),
384
+ adaptiveRefresh: adaptiveRefresh ?? DEFAULT_SETTINGS.adaptiveRefresh,
376
385
  merge: mergeBooleans(mergeSource, DEFAULT_SETTINGS.merge, warnings, "merge.")
377
386
  };
378
387
  return { settings, warnings };
@@ -617,6 +626,20 @@ function buildCloseCommand(prNumber) {
617
626
  assertPrNumber(prNumber);
618
627
  return `gh pr close ${String(prNumber)} < /dev/null`;
619
628
  }
629
+ function buildCheckoutTargetCommand(targetBranch) {
630
+ if (typeof targetBranch !== "string" || targetBranch.trim() === "") {
631
+ throw new Error("Invalid target branch name");
632
+ }
633
+ const safe = targetBranch.replace(/[^a-zA-Z0-9_./-]/g, "");
634
+ return `git checkout ${safe} && git pull origin ${safe}`;
635
+ }
636
+ function buildDeleteLocalBranchCommand(branchName) {
637
+ if (typeof branchName !== "string" || branchName.trim() === "") {
638
+ throw new Error("Invalid branch name");
639
+ }
640
+ const safe = branchName.replace(/[^a-zA-Z0-9_./-]/g, "");
641
+ return `git branch -d ${safe}`;
642
+ }
620
643
  function explainCiState(ci) {
621
644
  switch (ci.state) {
622
645
  case "none":
@@ -653,7 +676,8 @@ class PrUiController {
653
676
  outcome: null,
654
677
  settingsOpen: false,
655
678
  draft: null,
656
- lastInvalidate: 0
679
+ lastInvalidate: 0,
680
+ postMerge: null
657
681
  };
658
682
  this.states.set(key, created);
659
683
  return created;
@@ -681,7 +705,7 @@ class PrUiController {
681
705
  if (settings.refreshSeconds <= 0)
682
706
  return;
683
707
  const entry = statusCache.get(context);
684
- const staleMs = settings.refreshSeconds * 1000;
708
+ const staleMs = this.effectiveRefreshMs(settings, entry?.status);
685
709
  if (entry === undefined) {
686
710
  this.probe(context);
687
711
  return;
@@ -689,6 +713,17 @@ class PrUiController {
689
713
  if (Date.now() - Math.max(entry.probedAt, entry.loadedAt) >= staleMs)
690
714
  this.probe(context);
691
715
  }
716
+ effectiveRefreshMs(settings, status) {
717
+ const baseMs = settings.refreshSeconds * 1000;
718
+ if (!settings.adaptiveRefresh || status === undefined)
719
+ return baseMs;
720
+ const ci = status.ci;
721
+ if (ci.state === "running")
722
+ return Math.min(baseMs, 20000);
723
+ if (ci.state === "failed")
724
+ return Math.min(baseMs, 45000);
725
+ return baseMs;
726
+ }
692
727
  invalidate(context) {
693
728
  const state = this.stateFor(context);
694
729
  const now = Date.now();
@@ -769,6 +804,7 @@ class PrUiController {
769
804
  state.confirm = null;
770
805
  state.busy = "merge";
771
806
  state.outcome = null;
807
+ state.postMerge = null;
772
808
  this.requestRender(state);
773
809
  try {
774
810
  const run = await runGuardedCommand(context, {
@@ -778,6 +814,11 @@ class PrUiController {
778
814
  });
779
815
  if (run.exitCode === 0) {
780
816
  state.outcome = { ok: true, message: `PR #${String(pr.number)} merged (${settings.merge.method}).` };
817
+ const targetBranch = pr.baseRefName;
818
+ const mergedBranch = pr.headRefName ?? status?.branch;
819
+ if (settings.merge.checkoutTarget && targetBranch !== undefined && mergedBranch !== undefined) {
820
+ state.postMerge = { targetBranch, mergedBranch };
821
+ }
781
822
  } else {
782
823
  state.outcome = { ok: false, message: `Merge failed (exit ${String(run.exitCode ?? "?")}). Check the terminal output.`, terminalId: run.terminalId };
783
824
  }
@@ -791,6 +832,71 @@ class PrUiController {
791
832
  this.requestRender(state);
792
833
  }
793
834
  }
835
+ async postMergeCheckout(context) {
836
+ const state = this.stateFor(context);
837
+ const pm = state.postMerge;
838
+ if (pm === null)
839
+ return;
840
+ state.busy = "post-merge";
841
+ state.outcome = null;
842
+ this.requestRender(state);
843
+ try {
844
+ const run = await runGuardedCommand(context, {
845
+ title: `Checkout ${pm.targetBranch}`,
846
+ command: buildCheckoutTargetCommand(pm.targetBranch),
847
+ op: "post-merge"
848
+ });
849
+ if (run.exitCode === 0) {
850
+ state.outcome = { ok: true, message: `Switched to ${pm.targetBranch} and pulled latest.` };
851
+ state.postMerge = null;
852
+ } else {
853
+ state.outcome = { ok: false, message: `Checkout failed (exit ${String(run.exitCode ?? "?")}).`, terminalId: run.terminalId };
854
+ }
855
+ } catch (error) {
856
+ state.outcome = { ok: false, message: errorMessage(error) };
857
+ } finally {
858
+ state.busy = null;
859
+ statusCache.probe(context, { force: true }).catch(() => {
860
+ return;
861
+ });
862
+ this.requestRender(state);
863
+ }
864
+ }
865
+ async postMergeDeleteBranch(context) {
866
+ const state = this.stateFor(context);
867
+ const pm = state.postMerge;
868
+ if (pm === null)
869
+ return;
870
+ state.busy = "post-merge";
871
+ state.outcome = null;
872
+ this.requestRender(state);
873
+ try {
874
+ const run = await runGuardedCommand(context, {
875
+ title: `Delete branch ${pm.mergedBranch}`,
876
+ command: buildDeleteLocalBranchCommand(pm.mergedBranch),
877
+ op: "post-merge"
878
+ });
879
+ if (run.exitCode === 0) {
880
+ state.outcome = { ok: true, message: `Branch ${pm.mergedBranch} deleted.` };
881
+ state.postMerge = null;
882
+ } else {
883
+ state.outcome = { ok: false, message: `Delete failed (exit ${String(run.exitCode ?? "?")}).`, terminalId: run.terminalId };
884
+ }
885
+ } catch (error) {
886
+ state.outcome = { ok: false, message: errorMessage(error) };
887
+ } finally {
888
+ state.busy = null;
889
+ statusCache.probe(context, { force: true }).catch(() => {
890
+ return;
891
+ });
892
+ this.requestRender(state);
893
+ }
894
+ }
895
+ dismissPostMerge(context) {
896
+ const state = this.stateFor(context);
897
+ state.postMerge = null;
898
+ this.requestRender(state);
899
+ }
794
900
  async runClose(context, status) {
795
901
  const state = this.stateFor(context);
796
902
  const pr = status?.pr;
@@ -985,7 +1091,8 @@ function renderBody(html, controller, context, state, settings, status, busy) {
985
1091
  ${settings.showCI ? renderCiSection(html, status) : html`<p class="ghpr-muted">CI display is disabled in settings.</p>`}
986
1092
  ${renderWorktreeSection(html, status)}
987
1093
  ${renderOutcome(html, state)}
988
- ${pr !== undefined && pr.state === "OPEN" ? renderActions(html, controller, context, state, mergeEvaluation, closeEvaluation, busy) : null}
1094
+ ${state.postMerge !== null ? renderPostMerge(html, controller, context, state, busy) : null}
1095
+ ${pr !== undefined && pr.state === "OPEN" && state.postMerge === null ? renderActions(html, controller, context, state, mergeEvaluation, closeEvaluation, busy) : null}
989
1096
  ${renderSettings(html, controller, context, state, settings)}
990
1097
  `;
991
1098
  }
@@ -1078,6 +1185,48 @@ function renderOutcome(html, state) {
1078
1185
  </div>
1079
1186
  `;
1080
1187
  }
1188
+ function renderPostMerge(html, controller, context, state, busy) {
1189
+ const pm = state.postMerge;
1190
+ if (pm === null)
1191
+ return null;
1192
+ return html`
1193
+ <section class="ghpr-actions ghpr-post-merge">
1194
+ <p class="ghpr-post-merge-title">Merge complete! Next steps:</p>
1195
+ <div class="ghpr-buttons">
1196
+ <button
1197
+ type="button"
1198
+ class="ghpr-primary"
1199
+ title="Checkout "${pm.targetBranch}" and pull latest changes"
1200
+ ?disabled=${busy}
1201
+ @click=${() => {
1202
+ controller.postMergeCheckout(context);
1203
+ }}
1204
+ >
1205
+ ${state.busy === "post-merge" ? "Working…" : `Checkout ${pm.targetBranch} + pull`}
1206
+ </button>
1207
+ <button
1208
+ type="button"
1209
+ title="Delete the merged branch "${pm.mergedBranch}" locally"
1210
+ ?disabled=${busy}
1211
+ @click=${() => {
1212
+ controller.postMergeDeleteBranch(context);
1213
+ }}
1214
+ >
1215
+ ${state.busy === "post-merge" ? "Working…" : `Delete branch ${pm.mergedBranch}`}
1216
+ </button>
1217
+ <button
1218
+ type="button"
1219
+ ?disabled=${busy}
1220
+ @click=${() => {
1221
+ controller.dismissPostMerge(context);
1222
+ }}
1223
+ >
1224
+ Dismiss
1225
+ </button>
1226
+ </div>
1227
+ </section>
1228
+ `;
1229
+ }
1081
1230
  function renderActions(html, controller, context, state, mergeEvaluation, closeEvaluation, busy) {
1082
1231
  const mergeTitle = mergeEvaluation.canMerge ? mergeEvaluation.confirmations.length > 0 ? "Merge with confirmation" : "Merge this pull request" : mergeEvaluation.blockers.join(" · ");
1083
1232
  const confirm = state.confirm;
@@ -1170,8 +1319,23 @@ function renderSettings(html, controller, context, state, settings) {
1170
1319
  d.merge.deleteBranch = value;
1171
1320
  });
1172
1321
  }} /> Delete
1173
- branch after merge</label
1322
+ branch after merge (remote)</label
1174
1323
  >
1324
+ <label><input type="checkbox" ?checked=${draft.merge.checkoutTarget} @change=${(event) => {
1325
+ updateCheckbox(context, controller, event, (d, value) => {
1326
+ d.merge.checkoutTarget = value;
1327
+ });
1328
+ }} /> Offer checkout to target branch after merge</label>
1329
+ <label><input type="checkbox" ?checked=${draft.merge.deleteBranchAfterMerge} @change=${(event) => {
1330
+ updateCheckbox(context, controller, event, (d, value) => {
1331
+ d.merge.deleteBranchAfterMerge = value;
1332
+ });
1333
+ }} /> Offer delete merged branch after merge</label>
1334
+ <label><input type="checkbox" ?checked=${draft.adaptiveRefresh} @change=${(event) => {
1335
+ updateCheckbox(context, controller, event, (d, value) => {
1336
+ d.adaptiveRefresh = value;
1337
+ });
1338
+ }} /> Adaptive refresh (faster when CI is running)</label>
1175
1339
  <label>
1176
1340
  Merge method
1177
1341
  <select
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-web-github-pr-status",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "nextRelease": "patch",
5
5
  "description": "PI WEB plugin that surfaces GitHub pull request status (PR number, link, CI ball, worktree state) and one-click guarded merge/close without leaving the workspace.",
6
6
  "license": "MIT",