@dacsar/prview 1.0.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.
@@ -0,0 +1,7 @@
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 {};
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1 @@
1
+ export declare function Header(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ isFilterMode: boolean;
3
+ };
4
+ export declare function HelpBar({ isFilterMode }: Props): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,8 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ error: string | null;
3
+ };
4
+ export declare function Loading({ error }: Props): import("react/jsx-runtime").JSX.Element | null;
5
+ export {};
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1,7 @@
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 {};
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,7 @@
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 {};
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1,7 @@
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 {};
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,8 @@
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 {};
@@ -0,0 +1,6 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { PullRequest, SortState } from '../types.js';
2
+ export declare function useFilterSort(prs: PullRequest[], filter: string, sort: SortState): PullRequest[];
@@ -0,0 +1,24 @@
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
+ }
@@ -0,0 +1,10 @@
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 {};
@@ -0,0 +1,38 @@
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
+ }
@@ -0,0 +1,32 @@
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 ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
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[]>;
@@ -0,0 +1,127 @@
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
+ }
@@ -0,0 +1,3 @@
1
+ export type TimeColor = 'red' | 'yellow' | 'green';
2
+ export declare function formatElapsedTime(createdAt: string): string;
3
+ export declare function getTimeColor(createdAt: string): TimeColor;
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1 @@
1
+ export declare function openUrl(url: string): void;
@@ -0,0 +1,6 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@dacsar/prview",
3
+ "version": "1.0.0",
4
+ "description": "TUI tool to check PRs across repositories",
5
+ "type": "module",
6
+ "bin": {
7
+ "pv": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "node build.mjs",
14
+ "dev": "node build.mjs --watch",
15
+ "start": "node dist/cli.js",
16
+ "check": "biome check src/",
17
+ "check:fix": "biome check --write src/",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@inkjs/ui": "^2.0.0",
22
+ "ink": "^6.0.0",
23
+ "meow": "^13.0.0",
24
+ "react": "^19.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@biomejs/biome": "2.4.6",
28
+ "@sindresorhus/tsconfig": "^3.0.1",
29
+ "@types/node": "^25.3.2",
30
+ "@types/react": "^19.0.0",
31
+ "esbuild": "^0.27.3",
32
+ "typescript": "^5.7.0"
33
+ }
34
+ }