@noor.ahamed/pr-check 1.0.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/README.md +157 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +118 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +76 -0
- package/dist/github.d.ts +50 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +198 -0
- package/dist/login.d.ts +14 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +159 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +62 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# pr-check
|
|
2
|
+
|
|
3
|
+
Show your GitHub Pull Request responsibilities in one command: PRs awaiting your review, assigned to you, and your open PRs waiting on others.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
When you run `pr-check`, it:
|
|
8
|
+
|
|
9
|
+
1. **Awaiting Your Review** — Open PRs where you are requested as a reviewer
|
|
10
|
+
2. **Assigned to You** — Open PRs assigned to you
|
|
11
|
+
3. **Your Open PRs Waiting on Review** — PRs you authored that are still open
|
|
12
|
+
|
|
13
|
+
Each PR shows repo, number, title, URL, author, and how long it has been open. PRs older than a threshold are marked as STALE.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @noor.ahamed/pr-check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- **Node.js** 18 or later
|
|
24
|
+
- **GitHub CLI (`gh`)** — used for authentication and API access. You do **not** need to install it or log in beforehand: when you run `pr-check`, it checks for `gh` and your login status and guides you through setup if needed (install + sign-in).
|
|
25
|
+
|
|
26
|
+
No personal access token or manual config is required.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pr-check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To log in to your Github:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pr-check login
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
| Option | Description |
|
|
43
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
44
|
+
| `--no-color` | Disable colored output |
|
|
45
|
+
| `--json` | Output JSON for scripting |
|
|
46
|
+
| `--stale <days>` | Mark PRs older than N days as STALE (default: 3) |
|
|
47
|
+
| `--limit <n>` | Max results per category (default: 20) |
|
|
48
|
+
| `--org <org>` | Only show PRs in this GitHub organization |
|
|
49
|
+
| `--repos <csv>` | Only show PRs in these repos (e.g. `owner/repo1,owner/repo2`) |
|
|
50
|
+
| `--quiet` | Only print counts; use exit code to indicate responsibility |
|
|
51
|
+
|
|
52
|
+
### Examples
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Default: human-readable output (installs gh and runs login if needed)
|
|
56
|
+
pr-check
|
|
57
|
+
|
|
58
|
+
# Explicitly log in or install gh (no PR fetch)
|
|
59
|
+
pr-check login
|
|
60
|
+
|
|
61
|
+
# JSON output for scripts
|
|
62
|
+
pr-check --json
|
|
63
|
+
|
|
64
|
+
# Stale threshold 5 days, max 10 per section
|
|
65
|
+
pr-check --stale 5 --limit 10
|
|
66
|
+
|
|
67
|
+
# Only PRs in a specific org
|
|
68
|
+
pr-check --org mycompany
|
|
69
|
+
|
|
70
|
+
# Only PRs in specific repos
|
|
71
|
+
pr-check --repos owner1/repoA,owner2/repoB
|
|
72
|
+
|
|
73
|
+
# No colors (e.g. in CI logs)
|
|
74
|
+
pr-check --no-color
|
|
75
|
+
|
|
76
|
+
# Quiet: only counts and exit code
|
|
77
|
+
pr-check --quiet
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Example output (default)
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
👀 Awaiting Your Review (2)
|
|
84
|
+
|
|
85
|
+
[Draft] Add new API
|
|
86
|
+
acme/backend #42 · by @alice · open 1 day
|
|
87
|
+
https://github.com/acme/backend/pull/42
|
|
88
|
+
|
|
89
|
+
Fix login bug
|
|
90
|
+
acme/web #101 · by @bob · open 5 days STALE
|
|
91
|
+
https://github.com/acme/web/pull/101
|
|
92
|
+
|
|
93
|
+
📌 Assigned to You (0)
|
|
94
|
+
|
|
95
|
+
📝 Your Open PRs Waiting on Review (1)
|
|
96
|
+
|
|
97
|
+
Bump deps
|
|
98
|
+
acme/lib #7 · by @you · open 2 hours
|
|
99
|
+
https://github.com/acme/lib/pull/7
|
|
100
|
+
|
|
101
|
+
Total responsibility: 2
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Example JSON output
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"awaitingReview": [
|
|
109
|
+
{
|
|
110
|
+
"repo": "acme/backend",
|
|
111
|
+
"number": 42,
|
|
112
|
+
"title": "Add new API",
|
|
113
|
+
"url": "https://github.com/acme/backend/pull/42",
|
|
114
|
+
"authorLogin": "alice",
|
|
115
|
+
"createdAt": "2025-02-20T10:00:00Z",
|
|
116
|
+
"isDraft": true,
|
|
117
|
+
"reviewDecision": null,
|
|
118
|
+
"hasRequestedReviewers": true,
|
|
119
|
+
"category": "review",
|
|
120
|
+
"ageDays": 1,
|
|
121
|
+
"isStale": false
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"assignedToYou": [],
|
|
125
|
+
"yourOpenPRs": [],
|
|
126
|
+
"totalResponsibilityCount": 1
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Exit codes
|
|
131
|
+
|
|
132
|
+
| Code | Meaning |
|
|
133
|
+
| ---- | ---------------------------------------------------------------------------- |
|
|
134
|
+
| `0` | Success; with `--quiet`: no PRs awaiting your review or assigned to you |
|
|
135
|
+
| `1` | Error (e.g. install/login was skipped or failed, network or rate limit) |
|
|
136
|
+
| `2` | Only with `--quiet`: at least one PR awaiting your review or assigned to you |
|
|
137
|
+
|
|
138
|
+
Use `--quiet` in scripts or CI to check responsibility without parsing output:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pr-check --quiet
|
|
142
|
+
echo "Exit code: $?"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
- **pr-check says GitHub CLI is required and asks to install**
|
|
148
|
+
Answer **y** to have pr-check install `gh` (via winget/scoop/choco on Windows, brew on macOS, apt/dnf on Linux). If you said **n** or install failed, run `pr-check` again and choose **y**, or install from [cli.github.com](https://cli.github.com/) and run `pr-check` again.
|
|
149
|
+
|
|
150
|
+
- **After installing `gh`, pr-check still says it’s not installed**
|
|
151
|
+
On Windows, the PATH may not include `gh` until you open a **new terminal**. Close and reopen your terminal, then run `pr-check` again.
|
|
152
|
+
|
|
153
|
+
- **Rate limit exceeded**
|
|
154
|
+
Wait a few minutes or run `gh auth refresh`. Authenticated users have higher limits.
|
|
155
|
+
|
|
156
|
+
- **No PRs shown**
|
|
157
|
+
Ensure you have open PRs that match (review requested, assigned, or authored by you). Use `--org` or `--repos` only if you intend to filter.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
11
|
+
const github_1 = require("./github");
|
|
12
|
+
const format_1 = require("./format");
|
|
13
|
+
const login_1 = require("./login");
|
|
14
|
+
function isAuthError(msg) {
|
|
15
|
+
return msg === github_1.GH_NOT_FOUND || msg === github_1.GH_NOT_AUTHED;
|
|
16
|
+
}
|
|
17
|
+
function parseRepos(csv) {
|
|
18
|
+
if (!csv || !csv.trim())
|
|
19
|
+
return [];
|
|
20
|
+
return csv
|
|
21
|
+
.split(",")
|
|
22
|
+
.map((s) => s.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
async function runPrCheck(options) {
|
|
26
|
+
const run = async () => {
|
|
27
|
+
await (0, github_1.ensureGhAuth)();
|
|
28
|
+
const login = await (0, github_1.getCurrentUserLogin)();
|
|
29
|
+
const jsonOrQuiet = options.json || options.quiet;
|
|
30
|
+
const spinner = jsonOrQuiet ? null : (0, ora_1.default)("Fetching PRs...").start();
|
|
31
|
+
const results = await (0, github_1.fetchPrs)(login, options);
|
|
32
|
+
if (spinner)
|
|
33
|
+
spinner.succeed("Done");
|
|
34
|
+
if (options.quiet) {
|
|
35
|
+
const a = results.awaitingReview.length;
|
|
36
|
+
const b = results.assignedToYou.length;
|
|
37
|
+
const c = results.yourOpenPRs.length;
|
|
38
|
+
console.log(`review: ${a} assigned: ${b} open: ${c}`);
|
|
39
|
+
return results.totalResponsibilityCount > 0 ? 2 : 0;
|
|
40
|
+
}
|
|
41
|
+
if (options.json) {
|
|
42
|
+
console.log((0, format_1.formatJson)(results, options.staleDays));
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
console.log((0, format_1.formatHuman)(results, { staleDays: options.staleDays, noColor: options.noColor }));
|
|
46
|
+
return 0;
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
return await run();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
if (!isAuthError(msg)) {
|
|
54
|
+
console.error((0, format_1.formatError)(msg, options.noColor));
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const log = options.noColor ? (s) => console.log(s) : (s) => console.log(chalk_1.default.blue(s));
|
|
58
|
+
log("GitHub CLI (gh) is required and you need to be logged in. Running login flow...");
|
|
59
|
+
await (0, login_1.runLoginFlow)(options.noColor);
|
|
60
|
+
try {
|
|
61
|
+
return await run();
|
|
62
|
+
}
|
|
63
|
+
catch (retryErr) {
|
|
64
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
65
|
+
console.error((0, format_1.formatError)(retryMsg, options.noColor));
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function main() {
|
|
71
|
+
const program = (0, commander_1.createCommand)();
|
|
72
|
+
program
|
|
73
|
+
.name("pr-check")
|
|
74
|
+
.description("Show your GitHub Pull Request responsibilities in one command")
|
|
75
|
+
.version(package_json_1.default.version ?? "1.0.0")
|
|
76
|
+
.option("--no-color", "Disable colored output")
|
|
77
|
+
.option("--json", "Output JSON for scripting")
|
|
78
|
+
.option("--stale <days>", "Highlight PRs older than N days as STALE", "3")
|
|
79
|
+
.option("--limit <n>", "Max results per category", "20")
|
|
80
|
+
.option("--org <org>", "Only show PRs in this GitHub org")
|
|
81
|
+
.option("--repos <csv>", "Only show PRs in these repos (e.g. owner/repo1,owner/repo2)")
|
|
82
|
+
.option("--quiet", "Only print counts and exit with code 0/2/1");
|
|
83
|
+
program
|
|
84
|
+
.command("login")
|
|
85
|
+
.description("Log in to GitHub via GitHub CLI (gh). Prompts to install gh if not found.")
|
|
86
|
+
.action(async () => {
|
|
87
|
+
const parentOpts = program.opts();
|
|
88
|
+
const noColor = parentOpts.noColor ?? false;
|
|
89
|
+
if (noColor)
|
|
90
|
+
chalk_1.default.level = 0;
|
|
91
|
+
const code = await (0, login_1.runLoginFlow)(noColor);
|
|
92
|
+
process.exit(code);
|
|
93
|
+
});
|
|
94
|
+
program.action(async () => {
|
|
95
|
+
const opts = program.opts();
|
|
96
|
+
const noColor = opts.noColor ?? false;
|
|
97
|
+
if (noColor)
|
|
98
|
+
chalk_1.default.level = 0;
|
|
99
|
+
const staleDays = Math.max(0, parseInt(String(opts.stale ?? "3"), 10) || 3);
|
|
100
|
+
const limit = Math.max(1, Math.min(100, parseInt(String(opts.limit ?? "20"), 10) || 20));
|
|
101
|
+
const options = {
|
|
102
|
+
staleDays,
|
|
103
|
+
limitPerCategory: limit,
|
|
104
|
+
org: opts.org,
|
|
105
|
+
repos: parseRepos(opts.repos),
|
|
106
|
+
noColor,
|
|
107
|
+
json: opts.json ?? false,
|
|
108
|
+
quiet: opts.quiet ?? false,
|
|
109
|
+
};
|
|
110
|
+
const code = await runPrCheck(options);
|
|
111
|
+
process.exit(code);
|
|
112
|
+
});
|
|
113
|
+
await program.parseAsync();
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
main()
|
|
117
|
+
.then((code) => process.exit(code))
|
|
118
|
+
.catch(() => process.exit(1));
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PRRecord, GroupedResults } from "./types";
|
|
2
|
+
export declare function formatPrLine(pr: PRRecord, options: {
|
|
3
|
+
staleDays: number;
|
|
4
|
+
noColor: boolean;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function formatSection(title: string, emoji: string, prs: PRRecord[], options: {
|
|
7
|
+
staleDays: number;
|
|
8
|
+
noColor: boolean;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function formatHuman(results: GroupedResults, options: {
|
|
11
|
+
staleDays: number;
|
|
12
|
+
noColor: boolean;
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function formatJson(results: GroupedResults, staleDays: number): string;
|
|
15
|
+
export declare function formatError(message: string, noColor: boolean): string;
|
|
16
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAaxD,wBAAgB,YAAY,CAC1B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,CAgBR;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,QAAQ,EAAE,EACf,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,CAMR;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,CAsCR;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,MAAM,CAaR;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAErE"}
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatPrLine = formatPrLine;
|
|
7
|
+
exports.formatSection = formatSection;
|
|
8
|
+
exports.formatHuman = formatHuman;
|
|
9
|
+
exports.formatJson = formatJson;
|
|
10
|
+
exports.formatError = formatError;
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
function ageDays(createdAt) {
|
|
14
|
+
const then = new Date(createdAt).getTime();
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
return Math.floor((now - then) / (24 * 60 * 60 * 1000));
|
|
17
|
+
}
|
|
18
|
+
function isStale(createdAt, staleDays) {
|
|
19
|
+
return ageDays(createdAt) >= staleDays;
|
|
20
|
+
}
|
|
21
|
+
function formatPrLine(pr, options) {
|
|
22
|
+
const age = (0, utils_1.timeAgo)(pr.createdAt);
|
|
23
|
+
const stale = isStale(pr.createdAt, options.staleDays);
|
|
24
|
+
const dim = options.noColor ? (s) => s : chalk_1.default.dim;
|
|
25
|
+
const yellow = options.noColor ? (s) => s : chalk_1.default.yellow;
|
|
26
|
+
const gray = options.noColor ? (s) => s : chalk_1.default.gray;
|
|
27
|
+
const lines = [];
|
|
28
|
+
const title = pr.isDraft ? `[Draft] ${pr.title}` : pr.title;
|
|
29
|
+
lines.push(` ${title}`);
|
|
30
|
+
lines.push(dim(` ${pr.repo} #${pr.number} · by @${pr.authorLogin} · open ${age}`) +
|
|
31
|
+
(stale ? ` ${yellow(" STALE")}` : ""));
|
|
32
|
+
lines.push(gray(` ${pr.url}`));
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
35
|
+
function formatSection(title, emoji, prs, options) {
|
|
36
|
+
if (prs.length === 0)
|
|
37
|
+
return "";
|
|
38
|
+
const header = `${emoji} ${title} (${prs.length})`;
|
|
39
|
+
const bold = options.noColor ? (s) => s : chalk_1.default.bold;
|
|
40
|
+
const sections = [bold(header), ...prs.map((pr) => formatPrLine(pr, options))];
|
|
41
|
+
return sections.join("\n");
|
|
42
|
+
}
|
|
43
|
+
function formatHuman(results, options) {
|
|
44
|
+
const total = results.awaitingReview.length +
|
|
45
|
+
results.assignedToYou.length +
|
|
46
|
+
results.yourOpenPRs.length;
|
|
47
|
+
if (total === 0) {
|
|
48
|
+
const green = options.noColor ? (s) => s : chalk_1.default.green;
|
|
49
|
+
return green("All clear — no PR responsibilities right now.");
|
|
50
|
+
}
|
|
51
|
+
const parts = [];
|
|
52
|
+
parts.push(formatSection("Awaiting Your Review", "👀", results.awaitingReview, options));
|
|
53
|
+
parts.push(formatSection("Assigned to You", "📌", results.assignedToYou, options));
|
|
54
|
+
parts.push(formatSection("Your Open PRs Waiting on Review", "📝", results.yourOpenPRs, options));
|
|
55
|
+
const nonEmpty = parts.filter(Boolean);
|
|
56
|
+
const bold = options.noColor ? (s) => s : chalk_1.default.bold;
|
|
57
|
+
nonEmpty.push(bold(`\nTotal responsibility: ${results.totalResponsibilityCount}`));
|
|
58
|
+
return nonEmpty.join("\n\n");
|
|
59
|
+
}
|
|
60
|
+
function formatJson(results, staleDays) {
|
|
61
|
+
const serializePr = (pr) => ({
|
|
62
|
+
...pr,
|
|
63
|
+
ageDays: ageDays(pr.createdAt),
|
|
64
|
+
isStale: isStale(pr.createdAt, staleDays),
|
|
65
|
+
});
|
|
66
|
+
const out = {
|
|
67
|
+
awaitingReview: results.awaitingReview.map(serializePr),
|
|
68
|
+
assignedToYou: results.assignedToYou.map(serializePr),
|
|
69
|
+
yourOpenPRs: results.yourOpenPRs.map(serializePr),
|
|
70
|
+
totalResponsibilityCount: results.totalResponsibilityCount,
|
|
71
|
+
};
|
|
72
|
+
return JSON.stringify(out, null, 2);
|
|
73
|
+
}
|
|
74
|
+
function formatError(message, noColor) {
|
|
75
|
+
return noColor ? message : chalk_1.default.red(message);
|
|
76
|
+
}
|
package/dist/github.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GroupedResults, PrCheckOptions } from "./types";
|
|
2
|
+
export declare const GH_NOT_FOUND = "pr-check requires GitHub CLI (gh). Install: https://cli.github.com/ \u2014 then run: gh auth login.";
|
|
3
|
+
export declare const GH_NOT_AUTHED = "pr-check requires GitHub CLI (gh). If not logged in, run: gh auth login.";
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the GitHub CLI (gh) is installed and on PATH.
|
|
6
|
+
* On Windows, a missing command often does not set ENOENT, so any failure is treated as not installed.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isGhInstalled(): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Run `gh auth login` with inherited stdio so the user can interact.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runGhAuthLogin(): Promise<void>;
|
|
13
|
+
export declare function ensureGhAuth(): Promise<void>;
|
|
14
|
+
export declare function getCurrentUserLogin(): Promise<string>;
|
|
15
|
+
interface GhSearchPrItem {
|
|
16
|
+
repository?: {
|
|
17
|
+
nameWithOwner?: string;
|
|
18
|
+
owner?: {
|
|
19
|
+
login?: string;
|
|
20
|
+
};
|
|
21
|
+
name?: string;
|
|
22
|
+
};
|
|
23
|
+
number: number;
|
|
24
|
+
title: string;
|
|
25
|
+
url: string;
|
|
26
|
+
author?: {
|
|
27
|
+
login?: string;
|
|
28
|
+
} | null;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
isDraft: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate that each repo exists (404 or API error = invalid). Throws with invalid list if any.
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateRepos(repos: string[]): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Run gh search prs and return parsed PR list.
|
|
38
|
+
* Uses only CLI flags (no query string) so behaviour matches the manual:
|
|
39
|
+
* gh search prs --review-requested=@me --state=open --repo=owner/repo
|
|
40
|
+
*/
|
|
41
|
+
export declare function searchPrs(limit: number, flags: {
|
|
42
|
+
reviewRequested?: string;
|
|
43
|
+
assignee?: string;
|
|
44
|
+
author?: string;
|
|
45
|
+
repo?: string[];
|
|
46
|
+
owner?: string;
|
|
47
|
+
}): Promise<GhSearchPrItem[]>;
|
|
48
|
+
export declare function fetchPrs(_login: string, options: PrCheckOptions): Promise<GroupedResults>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../src/github.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAwB,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGpF,eAAO,MAAM,YAAY,wGACyE,CAAC;AACnG,eAAO,MAAM,aAAa,6EACkD,CAAC;AAE7E;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAOtD;AAED;;GAEG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBlD;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAO3D;AAED,UAAU,cAAc;IACtB,UAAU,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAeD;;GAEG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBlE;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE;IACL,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,cAAc,EAAE,CAAC,CA6C3B;AAiBD,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAwD/F"}
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GH_NOT_AUTHED = exports.GH_NOT_FOUND = void 0;
|
|
4
|
+
exports.isGhInstalled = isGhInstalled;
|
|
5
|
+
exports.runGhAuthLogin = runGhAuthLogin;
|
|
6
|
+
exports.ensureGhAuth = ensureGhAuth;
|
|
7
|
+
exports.getCurrentUserLogin = getCurrentUserLogin;
|
|
8
|
+
exports.validateRepos = validateRepos;
|
|
9
|
+
exports.searchPrs = searchPrs;
|
|
10
|
+
exports.fetchPrs = fetchPrs;
|
|
11
|
+
const execa_1 = require("execa");
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
exports.GH_NOT_FOUND = "pr-check requires GitHub CLI (gh). Install: https://cli.github.com/ — then run: gh auth login.";
|
|
14
|
+
exports.GH_NOT_AUTHED = "pr-check requires GitHub CLI (gh). If not logged in, run: gh auth login.";
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if the GitHub CLI (gh) is installed and on PATH.
|
|
17
|
+
* On Windows, a missing command often does not set ENOENT, so any failure is treated as not installed.
|
|
18
|
+
*/
|
|
19
|
+
async function isGhInstalled() {
|
|
20
|
+
try {
|
|
21
|
+
const result = await (0, execa_1.execa)("gh", ["--version"], { reject: false });
|
|
22
|
+
return result.exitCode === 0;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run `gh auth login` with inherited stdio so the user can interact.
|
|
30
|
+
*/
|
|
31
|
+
async function runGhAuthLogin() {
|
|
32
|
+
await (0, execa_1.execa)("gh", ["auth", "login"], { stdio: "inherit" });
|
|
33
|
+
}
|
|
34
|
+
async function ensureGhAuth() {
|
|
35
|
+
try {
|
|
36
|
+
await (0, execa_1.execa)("gh", ["auth", "status"]);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const execaErr = err;
|
|
40
|
+
if (execaErr.code === "ENOENT" || execaErr.message?.includes("not found")) {
|
|
41
|
+
throw new Error(exports.GH_NOT_FOUND);
|
|
42
|
+
}
|
|
43
|
+
if (execaErr.exitCode !== 0 ||
|
|
44
|
+
execaErr.message?.includes("not logged in") ||
|
|
45
|
+
execaErr.message?.includes("authentication")) {
|
|
46
|
+
throw new Error(exports.GH_NOT_AUTHED);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function getCurrentUserLogin() {
|
|
52
|
+
const { stdout } = await (0, execa_1.execa)("gh", ["api", "user", "--jq", ".login"]);
|
|
53
|
+
const login = (stdout ?? "").trim();
|
|
54
|
+
if (!login) {
|
|
55
|
+
throw new Error(exports.GH_NOT_AUTHED);
|
|
56
|
+
}
|
|
57
|
+
return login;
|
|
58
|
+
}
|
|
59
|
+
function repoSlug(item) {
|
|
60
|
+
const r = item.repository;
|
|
61
|
+
if (!r)
|
|
62
|
+
return "unknown/unknown";
|
|
63
|
+
if (r.nameWithOwner)
|
|
64
|
+
return r.nameWithOwner;
|
|
65
|
+
const owner = r.owner?.login ?? "unknown";
|
|
66
|
+
const name = r.name ?? "unknown";
|
|
67
|
+
return `${owner}/${name}`;
|
|
68
|
+
}
|
|
69
|
+
function authorLogin(item) {
|
|
70
|
+
return item.author?.login ?? "unknown";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Validate that each repo exists (404 or API error = invalid). Throws with invalid list if any.
|
|
74
|
+
*/
|
|
75
|
+
async function validateRepos(repos) {
|
|
76
|
+
if (repos.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
const invalid = [];
|
|
79
|
+
await Promise.all(repos.map(async (repo) => {
|
|
80
|
+
const result = await (0, execa_1.execa)("gh", ["api", `/repos/${repo}`], { reject: false });
|
|
81
|
+
if (result.exitCode !== 0 || !result.stdout?.trim()) {
|
|
82
|
+
invalid.push(repo);
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
if (invalid.length > 0) {
|
|
86
|
+
throw new Error(`Repository not found or no access: ${invalid.join(", ")}. Check spelling and that the repo exists.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run gh search prs and return parsed PR list.
|
|
91
|
+
* Uses only CLI flags (no query string) so behaviour matches the manual:
|
|
92
|
+
* gh search prs --review-requested=@me --state=open --repo=owner/repo
|
|
93
|
+
*/
|
|
94
|
+
async function searchPrs(limit, flags) {
|
|
95
|
+
const args = ["search", "prs", "--state=open", "--limit", String(limit)];
|
|
96
|
+
if (flags.reviewRequested) {
|
|
97
|
+
args.push(`--review-requested=${flags.reviewRequested}`);
|
|
98
|
+
}
|
|
99
|
+
if (flags.assignee) {
|
|
100
|
+
args.push(`--assignee=${flags.assignee}`);
|
|
101
|
+
}
|
|
102
|
+
if (flags.author) {
|
|
103
|
+
args.push(`--author=${flags.author}`);
|
|
104
|
+
}
|
|
105
|
+
if (flags.owner) {
|
|
106
|
+
args.push(`--owner=${flags.owner}`);
|
|
107
|
+
}
|
|
108
|
+
if (flags.repo?.length) {
|
|
109
|
+
for (const r of flags.repo) {
|
|
110
|
+
args.push(`--repo=${r}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
args.push("--json", "repository,number,title,url,author,createdAt,isDraft");
|
|
114
|
+
try {
|
|
115
|
+
const { stdout } = await (0, execa_1.execa)("gh", args);
|
|
116
|
+
const raw = (stdout ?? "").trim();
|
|
117
|
+
if (!raw)
|
|
118
|
+
return [];
|
|
119
|
+
const data = JSON.parse(raw);
|
|
120
|
+
if (Array.isArray(data)) {
|
|
121
|
+
return data;
|
|
122
|
+
}
|
|
123
|
+
const msg = data.message ?? "Unknown API error";
|
|
124
|
+
if (msg.includes("rate limit") || msg.includes("403")) {
|
|
125
|
+
throw new Error("GitHub rate limit exceeded. Try: gh auth refresh or wait a few minutes.");
|
|
126
|
+
}
|
|
127
|
+
throw new Error(msg);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const execaErr = err;
|
|
131
|
+
if (execaErr.code === "ENOENT") {
|
|
132
|
+
throw new Error(exports.GH_NOT_FOUND);
|
|
133
|
+
}
|
|
134
|
+
if (execaErr.stderr?.includes("rate limit")) {
|
|
135
|
+
throw new Error("GitHub rate limit exceeded. Try: gh auth refresh or wait a few minutes.");
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function mapToRecord(item, category) {
|
|
141
|
+
return {
|
|
142
|
+
repo: repoSlug(item),
|
|
143
|
+
number: item.number,
|
|
144
|
+
title: item.title,
|
|
145
|
+
url: item.url,
|
|
146
|
+
authorLogin: authorLogin(item),
|
|
147
|
+
createdAt: item.createdAt,
|
|
148
|
+
isDraft: item.isDraft ?? false,
|
|
149
|
+
reviewDecision: null,
|
|
150
|
+
hasRequestedReviewers: category === "review",
|
|
151
|
+
category,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function fetchPrs(_login, options) {
|
|
155
|
+
if (options.repos && options.repos.length > 0) {
|
|
156
|
+
await validateRepos(options.repos);
|
|
157
|
+
}
|
|
158
|
+
const limit = options.limitPerCategory;
|
|
159
|
+
const baseFlags = {
|
|
160
|
+
repo: options.repos,
|
|
161
|
+
owner: options.org,
|
|
162
|
+
};
|
|
163
|
+
const categories = ["review", "assigned", "authored"];
|
|
164
|
+
const searchFlags = [
|
|
165
|
+
{ ...baseFlags, reviewRequested: "@me" },
|
|
166
|
+
{ ...baseFlags, assignee: "@me" },
|
|
167
|
+
{ ...baseFlags, author: "@me" },
|
|
168
|
+
];
|
|
169
|
+
const results = await Promise.all(searchFlags.map((flags, i) => searchPrs(limit, flags).then((items) => items.map((item) => mapToRecord(item, categories[i])))));
|
|
170
|
+
const reviewList = results[0];
|
|
171
|
+
const assignedList = results[1];
|
|
172
|
+
const authoredList = results[2];
|
|
173
|
+
const allTagged = [...reviewList, ...assignedList, ...authoredList];
|
|
174
|
+
const deduped = (0, utils_1.dedupePrs)(allTagged);
|
|
175
|
+
const awaitingReview = [];
|
|
176
|
+
const assignedToYou = [];
|
|
177
|
+
const yourOpenPRs = [];
|
|
178
|
+
for (const pr of deduped) {
|
|
179
|
+
switch (pr.category) {
|
|
180
|
+
case "review":
|
|
181
|
+
awaitingReview.push(pr);
|
|
182
|
+
break;
|
|
183
|
+
case "assigned":
|
|
184
|
+
assignedToYou.push(pr);
|
|
185
|
+
break;
|
|
186
|
+
case "authored":
|
|
187
|
+
yourOpenPRs.push(pr);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const totalResponsibilityCount = awaitingReview.length + assignedToYou.length;
|
|
192
|
+
return {
|
|
193
|
+
awaitingReview,
|
|
194
|
+
assignedToYou,
|
|
195
|
+
yourOpenPRs,
|
|
196
|
+
totalResponsibilityCount,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask user: "GitHub CLI (gh) is required. Do you want to install it? (y/n)"
|
|
3
|
+
* Returns true for y/yes, false for n/no or empty.
|
|
4
|
+
*/
|
|
5
|
+
export declare function promptInstallGh(noColor: boolean): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* Try to install gh or open the install page. Uses platform-appropriate package managers.
|
|
8
|
+
*/
|
|
9
|
+
export declare function installOrOpenGh(noColor: boolean): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Full login flow: check gh -> if missing, prompt to install -> install or open URL -> run gh auth login.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runLoginFlow(noColor: boolean): Promise<number>;
|
|
14
|
+
//# sourceMappingURL=login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAMxE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0ErE;AASD;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAsCpE"}
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.promptInstallGh = promptInstallGh;
|
|
7
|
+
exports.installOrOpenGh = installOrOpenGh;
|
|
8
|
+
exports.runLoginFlow = runLoginFlow;
|
|
9
|
+
const readline_1 = require("readline");
|
|
10
|
+
const execa_1 = require("execa");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const github_1 = require("./github");
|
|
14
|
+
const GH_INSTALL_URL = "https://cli.github.com/";
|
|
15
|
+
function question(prompt) {
|
|
16
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(prompt, (answer) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve((answer ?? "").trim().toLowerCase());
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ask user: "GitHub CLI (gh) is required. Do you want to install it? (y/n)"
|
|
26
|
+
* Returns true for y/yes, false for n/no or empty.
|
|
27
|
+
*/
|
|
28
|
+
async function promptInstallGh(noColor) {
|
|
29
|
+
const msg = "GitHub CLI (gh) is required. Do you want to install it? (y/n): ";
|
|
30
|
+
const out = noColor ? msg : chalk_1.default.yellow(msg);
|
|
31
|
+
const answer = await question(out);
|
|
32
|
+
return answer === "y" || answer === "yes";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Try to install gh or open the install page. Uses platform-appropriate package managers.
|
|
36
|
+
*/
|
|
37
|
+
async function installOrOpenGh(noColor) {
|
|
38
|
+
const plat = (0, os_1.platform)();
|
|
39
|
+
const log = (s) => (noColor ? console.log(s) : console.log(chalk_1.default.blue(s)));
|
|
40
|
+
if (plat === "win32") {
|
|
41
|
+
const winInstallers = [
|
|
42
|
+
{
|
|
43
|
+
name: "winget",
|
|
44
|
+
args: [
|
|
45
|
+
"install",
|
|
46
|
+
"--id",
|
|
47
|
+
"GitHub.cli",
|
|
48
|
+
"-e",
|
|
49
|
+
"--silent",
|
|
50
|
+
"--accept-package-agreements",
|
|
51
|
+
"--accept-source-agreements",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{ name: "scoop", args: ["install", "gh"] },
|
|
55
|
+
{ name: "choco", args: ["install", "gh", "-y"] },
|
|
56
|
+
];
|
|
57
|
+
for (const { name, args } of winInstallers) {
|
|
58
|
+
try {
|
|
59
|
+
const cmd = name === "winget" ? "winget" : name === "scoop" ? "scoop" : "choco";
|
|
60
|
+
log(`Trying to install GitHub CLI via ${name}...`);
|
|
61
|
+
await (0, execa_1.execa)(cmd, args, { stdio: "inherit" });
|
|
62
|
+
log("Installation started or completed. If a new terminal is required, open one and run pr-check again.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
log("No package manager found (winget, scoop, or choco). Opening install page in browser...");
|
|
70
|
+
await openUrl(GH_INSTALL_URL);
|
|
71
|
+
console.log(`Install from: ${GH_INSTALL_URL}`);
|
|
72
|
+
console.log("After installing, run pr-check again.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (plat === "darwin") {
|
|
76
|
+
try {
|
|
77
|
+
log("Installing GitHub CLI via Homebrew...");
|
|
78
|
+
await (0, execa_1.execa)("brew", ["install", "gh"], { stdio: "inherit" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
log("Homebrew failed or not installed. Opening install page...");
|
|
83
|
+
await openUrl(GH_INSTALL_URL);
|
|
84
|
+
console.log(`Install from: ${GH_INSTALL_URL}`);
|
|
85
|
+
console.log("After installing, run pr-check again.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (plat === "linux") {
|
|
90
|
+
try {
|
|
91
|
+
log("Trying to install GitHub CLI via apt...");
|
|
92
|
+
await (0, execa_1.execa)("sudo", ["apt-get", "update", "-qq"], { reject: false });
|
|
93
|
+
await (0, execa_1.execa)("sudo", ["apt-get", "install", "-y", "gh"], { stdio: "inherit" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
try {
|
|
98
|
+
log("Trying to install GitHub CLI via dnf...");
|
|
99
|
+
await (0, execa_1.execa)("sudo", ["dnf", "install", "-y", "gh"], { stdio: "inherit" });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// fall through to open URL
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
log("Opening GitHub CLI install page in browser...");
|
|
108
|
+
await openUrl(GH_INSTALL_URL);
|
|
109
|
+
console.log(`Install from: ${GH_INSTALL_URL}`);
|
|
110
|
+
console.log("After installing, run pr-check again.");
|
|
111
|
+
}
|
|
112
|
+
function openUrl(url) {
|
|
113
|
+
const plat = (0, os_1.platform)();
|
|
114
|
+
const cmd = plat === "win32" ? "cmd" : plat === "darwin" ? "open" : "xdg-open";
|
|
115
|
+
const args = plat === "win32" ? ["/c", "start", "", url] : [url];
|
|
116
|
+
return (0, execa_1.execa)(cmd, args).then(() => undefined).catch(() => undefined);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Full login flow: check gh -> if missing, prompt to install -> install or open URL -> run gh auth login.
|
|
120
|
+
*/
|
|
121
|
+
async function runLoginFlow(noColor) {
|
|
122
|
+
if (await (0, github_1.isGhInstalled)()) {
|
|
123
|
+
try {
|
|
124
|
+
await (0, github_1.runGhAuthLogin)();
|
|
125
|
+
if (!noColor)
|
|
126
|
+
console.log(chalk_1.default.green("Login complete. You can now run pr-check."));
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
console.error(noColor ? msg : chalk_1.default.red(msg));
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log(noColor ? "GitHub CLI (gh) is not installed." : chalk_1.default.yellow("GitHub CLI (gh) is not installed."));
|
|
136
|
+
const wantInstall = await promptInstallGh(noColor);
|
|
137
|
+
if (!wantInstall) {
|
|
138
|
+
console.log(noColor ? "Skipped. Install gh from https://cli.github.com/ and run pr-check login." : chalk_1.default.dim("Skipped. Install gh from https://cli.github.com/ and run pr-check login."));
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
await installOrOpenGh(noColor);
|
|
142
|
+
if (await (0, github_1.isGhInstalled)()) {
|
|
143
|
+
try {
|
|
144
|
+
await (0, github_1.runGhAuthLogin)();
|
|
145
|
+
if (!noColor)
|
|
146
|
+
console.log(chalk_1.default.green("Login complete."));
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
console.error(noColor ? msg : chalk_1.default.red(msg));
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(noColor
|
|
156
|
+
? "After installing gh, run: pr-check login"
|
|
157
|
+
: chalk_1.default.dim("After installing gh, run: pr-check login"));
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type PRCategory = "review" | "assigned" | "authored";
|
|
2
|
+
export type ReviewDecision = "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
|
|
3
|
+
export interface PRRecord {
|
|
4
|
+
repo: string;
|
|
5
|
+
number: number;
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
authorLogin: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
isDraft: boolean;
|
|
11
|
+
reviewDecision: ReviewDecision;
|
|
12
|
+
hasRequestedReviewers: boolean;
|
|
13
|
+
category: PRCategory;
|
|
14
|
+
}
|
|
15
|
+
export interface PrCheckOptions {
|
|
16
|
+
staleDays: number;
|
|
17
|
+
limitPerCategory: number;
|
|
18
|
+
org?: string;
|
|
19
|
+
repos?: string[];
|
|
20
|
+
noColor: boolean;
|
|
21
|
+
json: boolean;
|
|
22
|
+
quiet: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface GroupedResults {
|
|
25
|
+
awaitingReview: PRRecord[];
|
|
26
|
+
assignedToYou: PRRecord[];
|
|
27
|
+
yourOpenPRs: PRRecord[];
|
|
28
|
+
totalResponsibilityCount: number;
|
|
29
|
+
}
|
|
30
|
+
export interface PrCheckJsonOutput {
|
|
31
|
+
awaitingReview: PRRecord[];
|
|
32
|
+
assignedToYou: PRRecord[];
|
|
33
|
+
yourOpenPRs: PRRecord[];
|
|
34
|
+
totalResponsibilityCount: number;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAE5D,MAAM,MAAM,cAAc,GACtB,UAAU,GACV,mBAAmB,GACnB,iBAAiB,GACjB,IAAI,CAAC;AAET,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,cAAc,CAAC;IAC/B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC3B,aAAa,EAAE,QAAQ,EAAE,CAAC;IAC1B,WAAW,EAAE,QAAQ,EAAE,CAAC;IACxB,wBAAwB,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC3B,aAAa,EAAE,QAAQ,EAAE,CAAC;IAC1B,WAAW,EAAE,QAAQ,EAAE,CAAC;IACxB,wBAAwB,EAAE,MAAM,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PRRecord } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Return human-readable time ago from ISO date string or unix timestamp.
|
|
4
|
+
*/
|
|
5
|
+
export declare function timeAgo(createdAt: string | number): string;
|
|
6
|
+
/**
|
|
7
|
+
* Dedupe PRs by URL; when a PR appears in multiple lists, keep the highest-priority category
|
|
8
|
+
* (review > assigned > authored).
|
|
9
|
+
*/
|
|
10
|
+
export declare function dedupePrs(prs: PRRecord[]): PRRecord[];
|
|
11
|
+
/**
|
|
12
|
+
* Build GitHub search query string: baseQuery + optional org: + optional repo: terms.
|
|
13
|
+
* Limit is applied at call site (gh search prs --limit).
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildSearchQuery(opts: {
|
|
16
|
+
baseQuery: string;
|
|
17
|
+
org?: string;
|
|
18
|
+
repos?: string[];
|
|
19
|
+
}): string;
|
|
20
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,SAAS,CAAC;AAQpD;;GAEG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAgB1D;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAYrD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,MAAM,CAWT"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.timeAgo = timeAgo;
|
|
4
|
+
exports.dedupePrs = dedupePrs;
|
|
5
|
+
exports.buildSearchQuery = buildSearchQuery;
|
|
6
|
+
const CATEGORY_PRIORITY = {
|
|
7
|
+
review: 3,
|
|
8
|
+
assigned: 2,
|
|
9
|
+
authored: 1,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Return human-readable time ago from ISO date string or unix timestamp.
|
|
13
|
+
*/
|
|
14
|
+
function timeAgo(createdAt) {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const then = typeof createdAt === "string"
|
|
17
|
+
? new Date(createdAt).getTime()
|
|
18
|
+
: createdAt * 1000;
|
|
19
|
+
const diffMs = Math.max(0, now - then);
|
|
20
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
21
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
22
|
+
const diffHours = Math.floor(diffMin / 60);
|
|
23
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
24
|
+
if (diffDays > 0)
|
|
25
|
+
return `${diffDays} day${diffDays === 1 ? "" : "s"}`;
|
|
26
|
+
if (diffHours > 0)
|
|
27
|
+
return `${diffHours} hour${diffHours === 1 ? "" : "s"}`;
|
|
28
|
+
if (diffMin >= 60)
|
|
29
|
+
return "1 hour";
|
|
30
|
+
return "< 1 hour";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dedupe PRs by URL; when a PR appears in multiple lists, keep the highest-priority category
|
|
34
|
+
* (review > assigned > authored).
|
|
35
|
+
*/
|
|
36
|
+
function dedupePrs(prs) {
|
|
37
|
+
const byUrl = new Map();
|
|
38
|
+
for (const pr of prs) {
|
|
39
|
+
const existing = byUrl.get(pr.url);
|
|
40
|
+
if (!existing ||
|
|
41
|
+
CATEGORY_PRIORITY[pr.category] > CATEGORY_PRIORITY[existing.category]) {
|
|
42
|
+
byUrl.set(pr.url, pr);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Array.from(byUrl.values());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build GitHub search query string: baseQuery + optional org: + optional repo: terms.
|
|
49
|
+
* Limit is applied at call site (gh search prs --limit).
|
|
50
|
+
*/
|
|
51
|
+
function buildSearchQuery(opts) {
|
|
52
|
+
const parts = [opts.baseQuery.trim()];
|
|
53
|
+
if (opts.org) {
|
|
54
|
+
parts.push(`org:${opts.org}`);
|
|
55
|
+
}
|
|
56
|
+
if (opts.repos && opts.repos.length > 0) {
|
|
57
|
+
for (const r of opts.repos) {
|
|
58
|
+
parts.push(`repo:${r}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return parts.join(" ");
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noor.ahamed/pr-check",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Show your GitHub Pull Request responsibilities in one command",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"types": "dist/cli.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pr-check": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"lint": "eslint src tests --ext .ts",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"postinstall": "echo \"pr-check requires GitHub CLI (gh). If not logged in, run: gh auth login.\""
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"github",
|
|
28
|
+
"pull-request",
|
|
29
|
+
"review",
|
|
30
|
+
"pr",
|
|
31
|
+
"cross-platform"
|
|
32
|
+
],
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/NooryA/pr-check.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/NooryA/pr-check#readme",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"chalk": "^4.1.2",
|
|
42
|
+
"commander": "^12.1.0",
|
|
43
|
+
"execa": "^8.0.1",
|
|
44
|
+
"ora": "^5.4.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.10.0",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
49
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
50
|
+
"eslint": "^8.0.0",
|
|
51
|
+
"prettier": "^3.0.0",
|
|
52
|
+
"typescript": "^5.3.0",
|
|
53
|
+
"vitest": "^1.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|