@dacsar/prview 1.0.0 → 1.1.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/README.md CHANGED
@@ -62,6 +62,10 @@ make setup
62
62
  npm run dev
63
63
  ```
64
64
 
65
- ## License
65
+ ### ローカルで動作確認
66
66
 
67
- MIT
67
+ ビルドして実行を一発で行えます:
68
+
69
+ ```sh
70
+ make pv
71
+ ```
package/dist/cli.js CHANGED
@@ -48719,6 +48719,7 @@ function Loading({ error }) {
48719
48719
 
48720
48720
  // src/constants.ts
48721
48721
  var COL = {
48722
+ icon: 4,
48722
48723
  repo: 18,
48723
48724
  status: 12,
48724
48725
  diff: 12,
@@ -48726,6 +48727,49 @@ var COL = {
48726
48727
  reviewee: 12,
48727
48728
  reviewer: 12
48728
48729
  };
48730
+ var BASE_WIDTH = 7;
48731
+ var OPTIONAL_WIDTH = {
48732
+ repo: COL.repo + 1,
48733
+ // 18 + separator
48734
+ status: COL.status + 2,
48735
+ // 12 + separator + marginLeft
48736
+ diff: COL.diff + 2,
48737
+ elapsed: COL.elapsed + 2,
48738
+ author: COL.reviewee + 2,
48739
+ reviewer: COL.reviewer + 2
48740
+ };
48741
+ var DROP_ORDER = [
48742
+ "reviewer",
48743
+ "author",
48744
+ "repo",
48745
+ "diff",
48746
+ "elapsed",
48747
+ "status"
48748
+ ];
48749
+ var MIN_TITLE_WIDTH = 20;
48750
+ function computeColumnLayout(columns) {
48751
+ const visible = new Set(
48752
+ Object.keys(OPTIONAL_WIDTH)
48753
+ );
48754
+ const fixed = () => {
48755
+ let width = BASE_WIDTH;
48756
+ for (const col of visible) width += OPTIONAL_WIDTH[col];
48757
+ return width;
48758
+ };
48759
+ for (const col of DROP_ORDER) {
48760
+ if (columns - fixed() >= MIN_TITLE_WIDTH) break;
48761
+ visible.delete(col);
48762
+ }
48763
+ return {
48764
+ showRepo: visible.has("repo"),
48765
+ showStatus: visible.has("status"),
48766
+ showDiff: visible.has("diff"),
48767
+ showElapsed: visible.has("elapsed"),
48768
+ showAuthor: visible.has("author"),
48769
+ showReviewer: visible.has("reviewer"),
48770
+ titleWidth: Math.max(10, columns - fixed())
48771
+ };
48772
+ }
48729
48773
 
48730
48774
  // src/utils/format-time.ts
48731
48775
  function formatElapsedTime(createdAt) {
@@ -48745,9 +48789,8 @@ function formatElapsedTime(createdAt) {
48745
48789
  }
48746
48790
  function getTimeIcon(createdAt) {
48747
48791
  const diffHours = (Date.now() - new Date(createdAt).getTime()) / (1e3 * 60 * 60);
48748
- if (diffHours < 12) return "\u{1F195}";
48749
- if (diffHours < 24) return "\u26A0\uFE0F";
48750
- return "\u{1F525}";
48792
+ if (diffHours >= 24) return "\u{1F525}";
48793
+ return " ";
48751
48794
  }
48752
48795
  function getTimeColor(createdAt) {
48753
48796
  const now = Date.now();
@@ -48787,6 +48830,17 @@ function StatusBadge({ decision, isDraft }) {
48787
48830
  // src/components/pr-row.tsx
48788
48831
  var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
48789
48832
  function PrRow({ pr, isSelected }) {
48833
+ const { stdout } = use_stdout_default();
48834
+ const columns = stdout?.columns ?? 120;
48835
+ const {
48836
+ showRepo,
48837
+ showStatus,
48838
+ showDiff,
48839
+ showElapsed,
48840
+ showAuthor,
48841
+ showReviewer,
48842
+ titleWidth
48843
+ } = computeColumnLayout(columns);
48790
48844
  const timeColor = getTimeColor(pr.createdAt);
48791
48845
  const elapsed = formatElapsedTime(pr.createdAt);
48792
48846
  const icon = getTimeIcon(pr.createdAt);
@@ -48799,42 +48853,50 @@ function PrRow({ pr, isSelected }) {
48799
48853
  const extraCount = filteredReviewers.length - 1;
48800
48854
  const reviewerText = firstReviewer ? `@${firstReviewer.login}${extraCount > 0 ? ` +${extraCount}` : ""}` : "";
48801
48855
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { backgroundColor: isSelected ? "#333333" : void 0, children: [
48802
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: isSelected ? "red" : void 0, children: isSelected ? "\u2502" : " " }),
48803
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: timeColor, children: [
48804
- " ",
48805
- icon,
48806
- " "
48856
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: isSelected ? "red" : void 0, children: isSelected ? ">" : " " }),
48857
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.icon, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: timeColor, wrap: "truncate", children: ` ${icon} ` }) }),
48858
+ showRepo && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
48859
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.repo, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: !isSelected, color: selColor, wrap: "truncate", children: [
48860
+ repoShort,
48861
+ "#",
48862
+ pr.number
48863
+ ] }) }),
48864
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "|" })
48807
48865
  ] }),
48808
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.repo, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: !isSelected, color: selColor, wrap: "truncate", children: [
48809
- repoShort,
48810
- "#",
48811
- pr.number
48812
- ] }) }),
48813
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "\u2502" }),
48814
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { flexGrow: 1, marginLeft: 1, marginRight: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { bold: isSelected, color: selColor, wrap: "truncate", children: pr.title }) }),
48815
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "\u2502" }),
48816
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.status, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { decision: pr.reviewDecision, isDraft: pr.isDraft }) }),
48817
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "\u2502" }),
48818
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { width: COL.diff, marginLeft: 1, justifyContent: "flex-end", children: [
48819
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: "green", children: [
48820
- "+",
48821
- pr.additions
48822
- ] }),
48823
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { children: " " }),
48824
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: "red", children: [
48825
- "-",
48826
- pr.deletions
48827
- ] })
48866
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: titleWidth, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { bold: isSelected, color: selColor, wrap: "truncate", children: pr.title }) }),
48867
+ showStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
48868
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "|" }),
48869
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.status, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { decision: pr.reviewDecision, isDraft: pr.isDraft }) })
48828
48870
  ] }),
48829
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "\u2502" }),
48830
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.elapsed, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: timeColor, children: elapsed }) }),
48831
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "\u2502" }),
48832
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.reviewee, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: true, wrap: "truncate", children: [
48833
- "@",
48834
- pr.author
48835
- ] }) }),
48836
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "\u2502" }),
48837
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.reviewer, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, wrap: "truncate", children: reviewerText }) })
48871
+ showDiff && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
48872
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "|" }),
48873
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.diff, marginLeft: 1, justifyContent: "flex-end", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { wrap: "truncate", children: [
48874
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: "green", children: [
48875
+ "+",
48876
+ pr.additions
48877
+ ] }),
48878
+ " ",
48879
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: "red", children: [
48880
+ "-",
48881
+ pr.deletions
48882
+ ] })
48883
+ ] }) })
48884
+ ] }),
48885
+ showElapsed && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
48886
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "|" }),
48887
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.elapsed, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: timeColor, wrap: "truncate", children: elapsed }) })
48888
+ ] }),
48889
+ showAuthor && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
48890
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "|" }),
48891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.reviewee, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: true, wrap: "truncate", children: [
48892
+ "@",
48893
+ pr.author
48894
+ ] }) })
48895
+ ] }),
48896
+ showReviewer && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
48897
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: "|" }),
48898
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { width: COL.reviewer, marginLeft: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, wrap: "truncate", children: reviewerText }) })
48899
+ ] })
48838
48900
  ] });
48839
48901
  }
48840
48902
 
@@ -48865,6 +48927,28 @@ function PrTable({ prs, selectedIndex, maxRows }) {
48865
48927
 
48866
48928
  // src/components/status-bar.tsx
48867
48929
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
48930
+ var HINTS = [
48931
+ { key: "Tab", label: "switch", keep: 6 },
48932
+ { key: "j/k", label: "move", keep: 3 },
48933
+ { key: "Enter", label: "open", keep: 2 },
48934
+ { key: "y", label: "copy", keep: 4 },
48935
+ { key: "/", label: "filter", keep: 5 },
48936
+ { key: "r", label: "refresh", keep: 7 },
48937
+ { key: "q", label: "quit", keep: 1 }
48938
+ ];
48939
+ var hintWidth = (h) => h.key.length + 1 + h.label.length;
48940
+ function visibleHints(budget) {
48941
+ const byPriority = [...HINTS].sort((a, b) => a.keep - b.keep);
48942
+ const shown = /* @__PURE__ */ new Set();
48943
+ let used = 0;
48944
+ for (const h of byPriority) {
48945
+ const width = hintWidth(h) + (shown.size > 0 ? 1 : 0);
48946
+ if (used + width > budget) break;
48947
+ used += width;
48948
+ shown.add(h.key);
48949
+ }
48950
+ return HINTS.filter((h) => shown.has(h.key));
48951
+ }
48868
48952
  function StatusBar({
48869
48953
  isFilterActive,
48870
48954
  filter,
@@ -48873,10 +48957,15 @@ function StatusBar({
48873
48957
  reviewCount,
48874
48958
  myCount,
48875
48959
  isFilterMode,
48876
- loading
48960
+ loading,
48961
+ copiedMessage
48877
48962
  }) {
48963
+ const { stdout } = use_stdout_default();
48964
+ const columns = stdout?.columns ?? 80;
48878
48965
  const isReview = activeTab === "review-requested";
48879
48966
  const tabLabel = isReview ? `To Review (${reviewCount})` : `My PRs (${myCount})`;
48967
+ const reserved = 2 + tabLabel.length + (loading ? 13 : 0) + 2;
48968
+ const hints = visibleHints(columns - reserved);
48880
48969
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
48881
48970
  Box_default,
48882
48971
  {
@@ -48889,7 +48978,10 @@ function StatusBar({
48889
48978
  paddingX: 1,
48890
48979
  justifyContent: "space-between",
48891
48980
  children: [
48892
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { children: isFilterActive || filter ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
48981
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { children: copiedMessage ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { color: "green", children: [
48982
+ "\u2713 ",
48983
+ copiedMessage
48984
+ ] }) : isFilterActive || filter ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
48893
48985
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "yellow", children: "/" }),
48894
48986
  isFilterActive ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
48895
48987
  TextInput,
@@ -48910,38 +49002,11 @@ function StatusBar({
48910
49002
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "gray", children: "Esc" }),
48911
49003
  " ",
48912
49004
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "cancel" })
48913
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
48914
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
48915
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: "Tab" }),
48916
- " ",
48917
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "switch" })
48918
- ] }),
48919
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
48920
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: "j/k" }),
48921
- " ",
48922
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "move" })
48923
- ] }),
48924
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
48925
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: "Enter" }),
48926
- " ",
48927
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "open" })
48928
- ] }),
48929
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
48930
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: "/" }),
48931
- " ",
48932
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "filter" })
48933
- ] }),
48934
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
48935
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: "r" }),
48936
- " ",
48937
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "refresh" })
48938
- ] }),
48939
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
48940
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: "q" }),
48941
- " ",
48942
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "quit" })
48943
- ] })
48944
- ] }) })
49005
+ ] }) : hints.map((h) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { children: [
49006
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { color: "white", children: h.key }),
49007
+ " ",
49008
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: h.label })
49009
+ ] }, h.key)) })
48945
49010
  ]
48946
49011
  }
48947
49012
  );
@@ -49130,10 +49195,18 @@ function usePullRequests() {
49130
49195
  return { reviewRequested, myPRs, loading, error, refresh: load };
49131
49196
  }
49132
49197
 
49133
- // src/utils/open-url.ts
49198
+ // src/utils/copy-to-clipboard.ts
49134
49199
  import { execFile as execFile2 } from "node:child_process";
49200
+ function copyToClipboard(text) {
49201
+ const child = execFile2("pbcopy", () => {
49202
+ });
49203
+ child.stdin?.end(text);
49204
+ }
49205
+
49206
+ // src/utils/open-url.ts
49207
+ import { execFile as execFile3 } from "node:child_process";
49135
49208
  function openUrl(url2) {
49136
- execFile2("open", [url2], () => {
49209
+ execFile3("open", [url2], () => {
49137
49210
  });
49138
49211
  }
49139
49212
 
@@ -49148,8 +49221,29 @@ function App2() {
49148
49221
  const [selectedIndex, setSelectedIndex] = (0, import_react62.useState)(0);
49149
49222
  const [filter, setFilter] = (0, import_react62.useState)("");
49150
49223
  const [isFilterMode, setIsFilterMode] = (0, import_react62.useState)(false);
49224
+ const [copiedMessage, setCopiedMessage] = (0, import_react62.useState)("");
49225
+ const copiedTimer = (0, import_react62.useRef)(null);
49226
+ const notifyCopied = (0, import_react62.useCallback)(() => {
49227
+ setCopiedMessage("Copied URL to clipboard");
49228
+ if (copiedTimer.current) {
49229
+ clearTimeout(copiedTimer.current);
49230
+ }
49231
+ copiedTimer.current = setTimeout(() => setCopiedMessage(""), 2e3);
49232
+ }, []);
49151
49233
  const currentPRs = activeTab === "review-requested" ? reviewRequested : myPRs;
49152
49234
  const filteredPRs = useFilterSort(currentPRs, filter);
49235
+ const [, setResizeTick] = (0, import_react62.useState)(0);
49236
+ (0, import_react62.useEffect)(() => {
49237
+ if (!stdout) return;
49238
+ const onResize = () => {
49239
+ stdout.write("\x1B[2J\x1B[H");
49240
+ setResizeTick((n) => n + 1);
49241
+ };
49242
+ stdout.on("resize", onResize);
49243
+ return () => {
49244
+ stdout.off("resize", onResize);
49245
+ };
49246
+ }, [stdout]);
49153
49247
  const clampIndex = (0, import_react62.useCallback)(
49154
49248
  (index) => Math.max(0, Math.min(index, filteredPRs.length - 1)),
49155
49249
  [filteredPRs.length]
@@ -49191,6 +49285,14 @@ function App2() {
49191
49285
  }
49192
49286
  return;
49193
49287
  }
49288
+ if (input === "y") {
49289
+ const pr = filteredPRs[selectedIndex];
49290
+ if (pr) {
49291
+ copyToClipboard(pr.url);
49292
+ notifyCopied();
49293
+ }
49294
+ return;
49295
+ }
49194
49296
  if (input === "/") {
49195
49297
  setIsFilterMode(true);
49196
49298
  return;
@@ -49241,7 +49343,8 @@ function App2() {
49241
49343
  reviewCount: reviewRequested.length,
49242
49344
  myCount: myPRs.length,
49243
49345
  isFilterMode,
49244
- loading
49346
+ loading,
49347
+ copiedMessage
49245
49348
  }
49246
49349
  )
49247
49350
  ] });
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@dacsar/prview",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TUI tool to check PRs across repositories",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/dac-sar/prview.git"
8
+ },
5
9
  "type": "module",
6
10
  "bin": {
7
11
  "pv": "./dist/cli.js"
package/dist/app.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function App(): import("react/jsx-runtime").JSX.Element;
package/dist/app.js DELETED
@@ -1,81 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback } from 'react';
3
- import { Box, Text, useApp, useInput } from 'ink';
4
- import { usePullRequests } from './hooks/use-pull-requests.js';
5
- import { useFilterSort } from './hooks/use-filter-sort.js';
6
- import { openUrl } from './utils/open-url.js';
7
- import { TabBar } from './components/tab-bar.js';
8
- import { PrTable } from './components/pr-table.js';
9
- import { FilterBar } from './components/filter-bar.js';
10
- import { HelpBar } from './components/help-bar.js';
11
- import { Loading } from './components/loading.js';
12
- export function App() {
13
- const { exit } = useApp();
14
- const { reviewRequested, myPRs, loading, error, refresh } = usePullRequests();
15
- const [activeTab, setActiveTab] = useState('review-requested');
16
- const [selectedIndex, setSelectedIndex] = useState(0);
17
- const [filter, setFilter] = useState('');
18
- const [isFilterMode, setIsFilterMode] = useState(false);
19
- const [sort, setSort] = useState({ key: 'time', direction: 'desc' });
20
- const currentPRs = activeTab === 'review-requested' ? reviewRequested : myPRs;
21
- const filteredPRs = useFilterSort(currentPRs, filter, sort);
22
- const clampIndex = useCallback((index) => Math.max(0, Math.min(index, filteredPRs.length - 1)), [filteredPRs.length]);
23
- useInput((_input, key) => {
24
- if (key.escape) {
25
- setIsFilterMode(false);
26
- return;
27
- }
28
- }, { isActive: isFilterMode });
29
- useInput((input, key) => {
30
- if (input === 'q') {
31
- exit();
32
- return;
33
- }
34
- if (key.tab) {
35
- setActiveTab(prev => prev === 'review-requested' ? 'my-prs' : 'review-requested');
36
- setSelectedIndex(0);
37
- return;
38
- }
39
- if (input === 'j' || key.downArrow) {
40
- setSelectedIndex(prev => clampIndex(prev + 1));
41
- return;
42
- }
43
- if (input === 'k' || key.upArrow) {
44
- setSelectedIndex(prev => clampIndex(prev - 1));
45
- return;
46
- }
47
- if (key.return) {
48
- const pr = filteredPRs[selectedIndex];
49
- if (pr) {
50
- openUrl(pr.url);
51
- }
52
- return;
53
- }
54
- if (input === 's') {
55
- setSort(prev => {
56
- if (prev.key === 'title') {
57
- return { key: 'time', direction: prev.direction };
58
- }
59
- if (prev.key === 'time' && prev.direction === 'desc') {
60
- return { key: 'time', direction: 'asc' };
61
- }
62
- return { key: 'title', direction: 'asc' };
63
- });
64
- return;
65
- }
66
- if (input === '/') {
67
- setIsFilterMode(true);
68
- return;
69
- }
70
- if (key.escape) {
71
- setFilter('');
72
- setIsFilterMode(false);
73
- return;
74
- }
75
- if (input === 'r') {
76
- refresh();
77
- return;
78
- }
79
- }, { isActive: !isFilterMode });
80
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TabBar, { activeTab: activeTab, reviewCount: reviewRequested.length, myCount: myPRs.length }), error && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), loading && filteredPRs.length === 0 ? (_jsx(Loading, { error: error })) : (_jsx(PrTable, { prs: filteredPRs, selectedIndex: selectedIndex })), _jsx(FilterBar, { isActive: isFilterMode, filter: filter, onFilterChange: setFilter }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { dimColor: true, children: ["Sort: ", sort.key, " (", sort.direction, ")"] }) }), _jsx(HelpBar, { isFilterMode: isFilterMode })] }));
81
- }
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env -S node --no-warnings
2
- export {};
@@ -1,7 +0,0 @@
1
- type Props = {
2
- isActive: boolean;
3
- filter: string;
4
- onFilterChange: (value: string) => void;
5
- };
6
- export declare function FilterBar({ isActive, filter, onFilterChange }: Props): import("react/jsx-runtime").JSX.Element | null;
7
- export {};
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { TextInput } from '@inkjs/ui';
4
- export function FilterBar({ isActive, filter, onFilterChange }) {
5
- if (!isActive && !filter) {
6
- return null;
7
- }
8
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "yellow", children: "/" }), isActive ? (_jsx(TextInput, { defaultValue: filter, onChange: onFilterChange, placeholder: "Filter by title, repo, author, reviewer..." })) : (_jsxs(Text, { dimColor: true, children: [" ", filter] }))] }));
9
- }
@@ -1 +0,0 @@
1
- export declare function Header(): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- export function Header() {
4
- return (_jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, justifyContent: "center", children: _jsx(Text, { bold: true, color: "cyan", children: "PR Checker" }) }));
5
- }
@@ -1,5 +0,0 @@
1
- type Props = {
2
- isFilterMode: boolean;
3
- };
4
- export declare function HelpBar({ isFilterMode }: Props): import("react/jsx-runtime").JSX.Element;
5
- export {};
@@ -1,8 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- export function HelpBar({ isFilterMode }) {
4
- if (isFilterMode) {
5
- return (_jsx(Box, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "Esc" }), " cancel filter"] }) }));
6
- }
7
- return (_jsxs(Box, { paddingX: 1, gap: 2, borderStyle: "single", borderColor: "gray", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "Tab" }), " switch"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "j/k" }), " move"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "Enter" }), " open"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "/" }), " filter"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "s" }), " sort"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "r" }), " refresh"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, children: "q" }), " quit"] })] }));
8
- }
@@ -1,5 +0,0 @@
1
- type Props = {
2
- error: string | null;
3
- };
4
- export declare function Loading({ error }: Props): import("react/jsx-runtime").JSX.Element | null;
5
- export {};
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Box } from 'ink';
3
- import { Spinner } from '@inkjs/ui';
4
- export function Loading({ error }) {
5
- if (error) {
6
- return null;
7
- }
8
- return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Spinner, { label: "Fetching pull requests..." }) }));
9
- }
@@ -1,7 +0,0 @@
1
- import type { PullRequest } from '../types.js';
2
- type Props = {
3
- pr: PullRequest;
4
- isSelected: boolean;
5
- };
6
- export declare function PrRow({ pr, isSelected }: Props): import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { formatElapsedTime, getTimeColor } from '../utils/format-time.js';
4
- import { StatusBadge } from './status-badge.js';
5
- export function PrRow({ pr, isSelected }) {
6
- const timeColor = getTimeColor(pr.createdAt);
7
- const elapsed = formatElapsedTime(pr.createdAt);
8
- const repoShort = pr.repository.split('/')[1] ?? pr.repository;
9
- return (_jsxs(Box, { children: [_jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : undefined, children: isSelected ? '▸ ' : ' ' }), _jsx(Box, { width: 20, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [repoShort, "#", pr.number] }) }), _jsx(Box, { flexGrow: 1, marginRight: 1, children: _jsx(Text, { bold: isSelected, wrap: "truncate", children: pr.title }) }), _jsx(Box, { width: 10, children: _jsx(StatusBadge, { decision: pr.reviewDecision, isDraft: pr.isDraft }) }), _jsx(Box, { width: 8, justifyContent: "flex-end", children: _jsxs(Text, { color: "green", children: ["+", pr.additions] }) }), _jsx(Box, { width: 8, justifyContent: "flex-end", children: _jsxs(Text, { color: "red", children: ["-", pr.deletions] }) }), _jsx(Box, { width: 6, justifyContent: "flex-end", marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["@", pr.author] }) }), _jsx(Box, { width: 10, justifyContent: "flex-end", marginLeft: 1, children: _jsx(Text, { color: timeColor, children: elapsed }) })] }));
10
- }
@@ -1,7 +0,0 @@
1
- import type { PullRequest } from '../types.js';
2
- type Props = {
3
- prs: PullRequest[];
4
- selectedIndex: number;
5
- };
6
- export declare function PrTable({ prs, selectedIndex }: Props): import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { PrRow } from './pr-row.js';
4
- export function PrTable({ prs, selectedIndex }) {
5
- if (prs.length === 0) {
6
- return (_jsx(Box, { paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "No pull requests found." }) }));
7
- }
8
- return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: prs.map((pr, index) => (_jsx(PrRow, { pr: pr, isSelected: index === selectedIndex }, pr.url))) }));
9
- }
@@ -1,7 +0,0 @@
1
- import type { ReviewDecision } from '../types.js';
2
- type Props = {
3
- decision: ReviewDecision;
4
- isDraft: boolean;
5
- };
6
- export declare function StatusBadge({ decision, isDraft }: Props): import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -1,21 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Text } from 'ink';
3
- export function StatusBadge({ decision, isDraft }) {
4
- if (isDraft) {
5
- return _jsx(Text, { color: "gray", children: " DRAFT " });
6
- }
7
- switch (decision) {
8
- case 'APPROVED': {
9
- return _jsx(Text, { color: "green", children: " APPROVED " });
10
- }
11
- case 'CHANGES_REQUESTED': {
12
- return _jsx(Text, { color: "red", children: " CHANGES " });
13
- }
14
- case 'REVIEW_REQUIRED': {
15
- return _jsx(Text, { color: "yellow", children: " PENDING " });
16
- }
17
- default: {
18
- return _jsx(Text, { color: "yellow", children: " PENDING " });
19
- }
20
- }
21
- }
@@ -1,8 +0,0 @@
1
- import type { Tab } from '../types.js';
2
- type Props = {
3
- activeTab: Tab;
4
- reviewCount: number;
5
- myCount: number;
6
- };
7
- export declare function TabBar({ activeTab, reviewCount, myCount }: Props): import("react/jsx-runtime").JSX.Element;
8
- export {};
@@ -1,6 +0,0 @@
1
- import { jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- export function TabBar({ activeTab, reviewCount, myCount }) {
4
- const isReview = activeTab === 'review-requested';
5
- return (_jsxs(Box, { gap: 2, paddingX: 1, children: [_jsxs(Text, { bold: isReview, color: isReview ? 'cyan' : undefined, dimColor: !isReview, children: [isReview ? '▸ ' : ' ', "Review Requested (", reviewCount, ")"] }), _jsxs(Text, { bold: !isReview, color: !isReview ? 'magenta' : undefined, dimColor: isReview, children: [!isReview ? '▸ ' : ' ', "My PRs (", myCount, ")"] })] }));
6
- }
@@ -1,2 +0,0 @@
1
- import type { PullRequest, SortState } from '../types.js';
2
- export declare function useFilterSort(prs: PullRequest[], filter: string, sort: SortState): PullRequest[];
@@ -1,24 +0,0 @@
1
- import { useMemo } from 'react';
2
- export function useFilterSort(prs, filter, sort) {
3
- return useMemo(() => {
4
- let result = prs;
5
- if (filter) {
6
- const lower = filter.toLowerCase();
7
- result = result.filter(pr => pr.title.toLowerCase().includes(lower)
8
- || pr.repository.toLowerCase().includes(lower)
9
- || pr.author.toLowerCase().includes(lower)
10
- || pr.reviewers.some(r => r.login.toLowerCase().includes(lower)));
11
- }
12
- const sorted = [...result].sort((a, b) => {
13
- if (sort.key === 'title') {
14
- return a.title.localeCompare(b.title);
15
- }
16
- // Sort by time (createdAt)
17
- return (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
18
- });
19
- if (sort.direction === 'desc') {
20
- sorted.reverse();
21
- }
22
- return sorted;
23
- }, [prs, filter, sort]);
24
- }
@@ -1,10 +0,0 @@
1
- import type { PullRequest } from '../types.js';
2
- type UsePullRequestsResult = {
3
- reviewRequested: PullRequest[];
4
- myPRs: PullRequest[];
5
- loading: boolean;
6
- error: string | null;
7
- refresh: () => void;
8
- };
9
- export declare function usePullRequests(): UsePullRequestsResult;
10
- export {};
@@ -1,38 +0,0 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import { fetchReviewRequested, fetchMyPRs } from '../utils/fetch-prs.js';
3
- const REFRESH_INTERVAL = 60000;
4
- export function usePullRequests() {
5
- const [reviewRequested, setReviewRequested] = useState([]);
6
- const [myPRs, setMyPRs] = useState([]);
7
- const [loading, setLoading] = useState(true);
8
- const [error, setError] = useState(null);
9
- const load = useCallback(async () => {
10
- setLoading(true);
11
- setError(null);
12
- try {
13
- const [rr, my] = await Promise.all([
14
- fetchReviewRequested(),
15
- fetchMyPRs(),
16
- ]);
17
- setReviewRequested(rr);
18
- setMyPRs(my);
19
- }
20
- catch (err) {
21
- const message = err instanceof Error ? err.message : 'Unknown error';
22
- setError(message);
23
- }
24
- finally {
25
- setLoading(false);
26
- }
27
- }, []);
28
- useEffect(() => {
29
- void load();
30
- const timer = setInterval(() => {
31
- void load();
32
- }, REFRESH_INTERVAL);
33
- return () => {
34
- clearInterval(timer);
35
- };
36
- }, [load]);
37
- return { reviewRequested, myPRs, loading, error, refresh: load };
38
- }
package/dist/types.d.ts DELETED
@@ -1,32 +0,0 @@
1
- export type ReviewDecision = 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | '';
2
- export type ReviewState = 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'PENDING' | 'DISMISSED';
3
- export type Reviewer = {
4
- login: string;
5
- state: ReviewState;
6
- };
7
- export type Label = {
8
- name: string;
9
- color: string;
10
- };
11
- export type PullRequest = {
12
- number: number;
13
- title: string;
14
- url: string;
15
- createdAt: string;
16
- repository: string;
17
- author: string;
18
- reviewers: Reviewer[];
19
- reviewDecision: ReviewDecision;
20
- isDraft: boolean;
21
- additions: number;
22
- deletions: number;
23
- labels: Label[];
24
- branch: string;
25
- };
26
- export type Tab = 'review-requested' | 'my-prs';
27
- export type SortKey = 'title' | 'time';
28
- export type SortDirection = 'asc' | 'desc';
29
- export type SortState = {
30
- key: SortKey;
31
- direction: SortDirection;
32
- };
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,4 +0,0 @@
1
- import type { PullRequest } from '../types.js';
2
- export declare function checkAuth(): Promise<boolean>;
3
- export declare function fetchReviewRequested(): Promise<PullRequest[]>;
4
- export declare function fetchMyPRs(): Promise<PullRequest[]>;
@@ -1,127 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- const QUERY = `
3
- query($q: String!) {
4
- search(query: $q, type: ISSUE, first: 50) {
5
- nodes {
6
- ... on PullRequest {
7
- number
8
- title
9
- url
10
- createdAt
11
- isDraft
12
- additions
13
- deletions
14
- headRefName
15
- reviewDecision
16
- author {
17
- login
18
- }
19
- repository {
20
- nameWithOwner
21
- }
22
- labels(first: 10) {
23
- nodes {
24
- name
25
- color
26
- }
27
- }
28
- latestReviews(first: 10) {
29
- nodes {
30
- author {
31
- login
32
- }
33
- state
34
- }
35
- }
36
- reviewRequests(first: 10) {
37
- nodes {
38
- requestedReviewer {
39
- ... on User {
40
- login
41
- }
42
- ... on Team {
43
- name
44
- }
45
- }
46
- }
47
- }
48
- }
49
- }
50
- }
51
- }
52
- `;
53
- function parseNode(node) {
54
- const reviewers = [];
55
- for (const review of node.latestReviews.nodes) {
56
- if (review.author) {
57
- reviewers.push({
58
- login: review.author.login,
59
- state: review.state,
60
- });
61
- }
62
- }
63
- for (const request of node.reviewRequests.nodes) {
64
- const login = request.requestedReviewer?.login ?? request.requestedReviewer?.name;
65
- if (login && !reviewers.some(r => r.login === login)) {
66
- reviewers.push({ login, state: 'PENDING' });
67
- }
68
- }
69
- const labels = node.labels.nodes.map(l => ({
70
- name: l.name,
71
- color: l.color,
72
- }));
73
- return {
74
- number: node.number,
75
- title: node.title,
76
- url: node.url,
77
- createdAt: node.createdAt,
78
- repository: node.repository.nameWithOwner,
79
- author: node.author?.login ?? 'unknown',
80
- reviewers,
81
- reviewDecision: node.reviewDecision ?? '',
82
- isDraft: node.isDraft,
83
- additions: node.additions,
84
- deletions: node.deletions,
85
- labels,
86
- branch: node.headRefName,
87
- };
88
- }
89
- function runGh(args, stdin) {
90
- return new Promise((resolve, reject) => {
91
- const child = execFile('gh', args, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
92
- if (error) {
93
- reject(error);
94
- return;
95
- }
96
- resolve(stdout);
97
- });
98
- if (stdin && child.stdin) {
99
- child.stdin.write(stdin);
100
- child.stdin.end();
101
- }
102
- });
103
- }
104
- function runGraphQL(searchQuery) {
105
- const body = JSON.stringify({
106
- query: QUERY,
107
- variables: { q: searchQuery },
108
- });
109
- return runGh(['api', 'graphql', '--input', '-'], body).then(result => JSON.parse(result));
110
- }
111
- export async function checkAuth() {
112
- try {
113
- await runGh(['auth', 'status']);
114
- return true;
115
- }
116
- catch {
117
- return false;
118
- }
119
- }
120
- export async function fetchReviewRequested() {
121
- const json = await runGraphQL('is:pr is:open review-requested:@me');
122
- return json.data.search.nodes.map(node => parseNode(node));
123
- }
124
- export async function fetchMyPRs() {
125
- const json = await runGraphQL('is:pr is:open author:@me');
126
- return json.data.search.nodes.map(node => parseNode(node));
127
- }
@@ -1,3 +0,0 @@
1
- export type TimeColor = 'red' | 'yellow' | 'green';
2
- export declare function formatElapsedTime(createdAt: string): string;
3
- export declare function getTimeColor(createdAt: string): TimeColor;
@@ -1,27 +0,0 @@
1
- export function formatElapsedTime(createdAt) {
2
- const now = Date.now();
3
- const created = new Date(createdAt).getTime();
4
- const diffMs = now - created;
5
- const minutes = Math.floor(diffMs / (1000 * 60));
6
- const hours = Math.floor(minutes / 60);
7
- const days = Math.floor(hours / 24);
8
- if (days > 0) {
9
- return `${String(days)}d ago`;
10
- }
11
- if (hours > 0) {
12
- return `${String(hours)}h ago`;
13
- }
14
- return `${String(minutes)}m ago`;
15
- }
16
- export function getTimeColor(createdAt) {
17
- const now = Date.now();
18
- const created = new Date(createdAt).getTime();
19
- const diffHours = (now - created) / (1000 * 60 * 60);
20
- if (diffHours > 48) {
21
- return 'red';
22
- }
23
- if (diffHours > 24) {
24
- return 'yellow';
25
- }
26
- return 'green';
27
- }
@@ -1 +0,0 @@
1
- export declare function openUrl(url: string): void;
@@ -1,6 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- export function openUrl(url) {
3
- execFile('open', [url], () => {
4
- // Silently fail — user can copy URL from the table
5
- });
6
- }