prq-cli 0.4.0 → 0.5.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 +42 -8
- package/dist/bin/prq.js +200 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Requires [GitHub CLI](https://cli.github.com/) (`gh`) to be authenticated.
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# Set up config for the current project
|
|
21
|
-
prq init
|
|
22
|
-
|
|
23
20
|
# See your review queue
|
|
24
21
|
prq
|
|
22
|
+
|
|
23
|
+
# Interactive mode — navigate with arrow keys, act with shortcuts
|
|
24
|
+
prq -i
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Commands
|
|
@@ -40,11 +40,23 @@ prq # all repos you have access to
|
|
|
40
40
|
prq status --repos org/repo1 org/repo2 # specific repos
|
|
41
41
|
prq status --stale-days 7 # custom stale threshold
|
|
42
42
|
prq status --json # machine-readable output
|
|
43
|
+
prq status -i # interactive mode
|
|
43
44
|
```
|
|
44
45
|
|
|
46
|
+
### Interactive Mode
|
|
47
|
+
|
|
48
|
+
Run `prq -i` to navigate your queue with keyboard shortcuts:
|
|
49
|
+
|
|
50
|
+
- **↑↓** navigate between PRs
|
|
51
|
+
- **r** review — open files changed tab
|
|
52
|
+
- **o** open — open PR in browser
|
|
53
|
+
- **n** nudge — post a comment
|
|
54
|
+
- **c** copy URL to clipboard
|
|
55
|
+
- **q** quit
|
|
56
|
+
|
|
45
57
|
### `prq open <identifier>`
|
|
46
58
|
|
|
47
|
-
Open a PR in the browser.
|
|
59
|
+
Open a PR in the browser.
|
|
48
60
|
|
|
49
61
|
```bash
|
|
50
62
|
prq open 482 # searches your queue for PR #482
|
|
@@ -71,14 +83,18 @@ prq nudge 482 --yes # skip confirmation
|
|
|
71
83
|
prq nudge 482 --message "Any updates?" # custom message
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
### `prq
|
|
86
|
+
### `prq run <action> <identifier>`
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
Run a custom action defined in your config.
|
|
77
89
|
|
|
78
90
|
```bash
|
|
79
|
-
prq
|
|
91
|
+
prq run checkout 482
|
|
80
92
|
```
|
|
81
93
|
|
|
94
|
+
### `prq init`
|
|
95
|
+
|
|
96
|
+
Creates a `.prqrc.json` config file in the current directory.
|
|
97
|
+
|
|
82
98
|
## Configuration
|
|
83
99
|
|
|
84
100
|
Config is loaded in this order (later overrides earlier):
|
|
@@ -92,10 +108,28 @@ Example `.prqrc.json`:
|
|
|
92
108
|
```json
|
|
93
109
|
{
|
|
94
110
|
"repos": ["org/repo1", "org/repo2"],
|
|
95
|
-
"staleDays": 5
|
|
111
|
+
"staleDays": 5,
|
|
112
|
+
"actions": {
|
|
113
|
+
"review": "claude -p '/review {url}'",
|
|
114
|
+
"checkout": "gh pr checkout {number} --repo {owner}/{repo}"
|
|
115
|
+
}
|
|
96
116
|
}
|
|
97
117
|
```
|
|
98
118
|
|
|
119
|
+
### Custom Actions
|
|
120
|
+
|
|
121
|
+
Actions are shell command templates with variables:
|
|
122
|
+
|
|
123
|
+
- `{url}` — full PR URL
|
|
124
|
+
- `{number}` — PR number
|
|
125
|
+
- `{owner}` — repo owner
|
|
126
|
+
- `{repo}` — repo name
|
|
127
|
+
- `{title}` — PR title
|
|
128
|
+
- `{author}` — PR author
|
|
129
|
+
- `{days}` — days since last activity
|
|
130
|
+
|
|
131
|
+
Default actions (`open`, `review`, `nudge`) can be overridden. Custom actions are available via `prq run <action>` and in interactive mode.
|
|
132
|
+
|
|
99
133
|
## License
|
|
100
134
|
|
|
101
135
|
MIT
|
package/dist/bin/prq.js
CHANGED
|
@@ -6583,7 +6583,7 @@ function categorize(reviewedPRs, requestedPRs, authoredPRs, staleDays) {
|
|
|
6583
6583
|
return results;
|
|
6584
6584
|
}
|
|
6585
6585
|
|
|
6586
|
-
// src/
|
|
6586
|
+
// src/interactive.ts
|
|
6587
6587
|
var CATEGORY_CONFIG = {
|
|
6588
6588
|
"needs-re-review": {
|
|
6589
6589
|
icon: "◆",
|
|
@@ -6604,6 +6604,198 @@ var CATEGORY_ORDER = [
|
|
|
6604
6604
|
"stale",
|
|
6605
6605
|
"waiting-on-others"
|
|
6606
6606
|
];
|
|
6607
|
+
function toResolvedPR(pr) {
|
|
6608
|
+
const [owner, repo] = pr.repo.split("/");
|
|
6609
|
+
return {
|
|
6610
|
+
owner,
|
|
6611
|
+
repo,
|
|
6612
|
+
number: pr.number,
|
|
6613
|
+
url: pr.url,
|
|
6614
|
+
title: pr.title,
|
|
6615
|
+
author: pr.author,
|
|
6616
|
+
updatedAt: pr.updatedAt
|
|
6617
|
+
};
|
|
6618
|
+
}
|
|
6619
|
+
function render(result, selectedIndex, message) {
|
|
6620
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
6621
|
+
const lines = [];
|
|
6622
|
+
lines.push(source_default.bold(` PRQ Status for ${result.user}`));
|
|
6623
|
+
lines.push(source_default.dim(` ${"─".repeat(50)}`));
|
|
6624
|
+
const grouped = new Map;
|
|
6625
|
+
for (const pr of result.prs) {
|
|
6626
|
+
const list = grouped.get(pr.category) ?? [];
|
|
6627
|
+
list.push(pr);
|
|
6628
|
+
grouped.set(pr.category, list);
|
|
6629
|
+
}
|
|
6630
|
+
let flatIndex = 0;
|
|
6631
|
+
for (const category of CATEGORY_ORDER) {
|
|
6632
|
+
const prs = grouped.get(category);
|
|
6633
|
+
if (!prs || prs.length === 0)
|
|
6634
|
+
continue;
|
|
6635
|
+
const config = CATEGORY_CONFIG[category];
|
|
6636
|
+
lines.push("");
|
|
6637
|
+
lines.push(` ${config.color(`${config.icon} ${config.label}`)} ${source_default.dim(`(${prs.length})`)}`);
|
|
6638
|
+
for (const pr of prs) {
|
|
6639
|
+
const isSelected = flatIndex === selectedIndex;
|
|
6640
|
+
const arrow = isSelected ? source_default.yellow("›") : " ";
|
|
6641
|
+
const draft = pr.isDraft ? source_default.dim(" [draft]") : "";
|
|
6642
|
+
const maxTitle = 50;
|
|
6643
|
+
const title = pr.title.length > maxTitle ? `${pr.title.slice(0, maxTitle - 3)}...` : pr.title;
|
|
6644
|
+
if (isSelected) {
|
|
6645
|
+
const ref = source_default.white(`#${pr.number}`);
|
|
6646
|
+
lines.push(` ${arrow} ${ref} ${source_default.white(title)}${draft}`);
|
|
6647
|
+
lines.push(` ${source_default.dim("↳")} ${source_default.dim(pr.detail)}`);
|
|
6648
|
+
} else {
|
|
6649
|
+
const ref = source_default.dim(`#${pr.number}`);
|
|
6650
|
+
lines.push(` ${arrow} ${ref} ${source_default.dim(title)}${draft}`);
|
|
6651
|
+
lines.push(` ${source_default.dim("↳")} ${source_default.dim(pr.detail)}`);
|
|
6652
|
+
}
|
|
6653
|
+
flatIndex++;
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
if (result.prs.length === 0) {
|
|
6657
|
+
lines.push("");
|
|
6658
|
+
lines.push(source_default.green(" All clear! No PRs need your attention."));
|
|
6659
|
+
}
|
|
6660
|
+
lines.push("");
|
|
6661
|
+
lines.push(source_default.dim(` ${"─".repeat(50)}`));
|
|
6662
|
+
lines.push("");
|
|
6663
|
+
lines.push(` ${source_default.dim("↑↓")} navigate ${source_default.white("r")} review ${source_default.white("o")} open ${source_default.white("n")} nudge ${source_default.white("c")} copy url ${source_default.white("q")} quit`);
|
|
6664
|
+
if (message) {
|
|
6665
|
+
lines.push("");
|
|
6666
|
+
lines.push(` ${message}`);
|
|
6667
|
+
}
|
|
6668
|
+
process.stdout.write(lines.join(`
|
|
6669
|
+
`));
|
|
6670
|
+
}
|
|
6671
|
+
async function interactiveMode(result, config) {
|
|
6672
|
+
if (result.prs.length === 0) {
|
|
6673
|
+
console.log(source_default.green(`
|
|
6674
|
+
All clear! No PRs need your attention.
|
|
6675
|
+
`));
|
|
6676
|
+
return;
|
|
6677
|
+
}
|
|
6678
|
+
let selectedIndex = 0;
|
|
6679
|
+
let message = "";
|
|
6680
|
+
const total = result.prs.length;
|
|
6681
|
+
if (!process.stdin.isTTY) {
|
|
6682
|
+
process.stdout.write(`Interactive mode requires a terminal. Use prq status instead.
|
|
6683
|
+
`);
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
6686
|
+
process.stdin.setRawMode(true);
|
|
6687
|
+
process.stdin.resume();
|
|
6688
|
+
process.stdin.setEncoding("utf8");
|
|
6689
|
+
process.stdout.write("\x1B[?25l");
|
|
6690
|
+
render(result, selectedIndex, message);
|
|
6691
|
+
return new Promise((resolve) => {
|
|
6692
|
+
const cleanup = () => {
|
|
6693
|
+
process.stdin.setRawMode(false);
|
|
6694
|
+
process.stdin.pause();
|
|
6695
|
+
process.stdin.removeAllListeners("data");
|
|
6696
|
+
process.stdout.write("\x1B[?25h\x1B[2J\x1B[H");
|
|
6697
|
+
};
|
|
6698
|
+
process.stdin.on("data", async (key) => {
|
|
6699
|
+
const pr = result.prs[selectedIndex];
|
|
6700
|
+
switch (key) {
|
|
6701
|
+
case "q":
|
|
6702
|
+
case "\x03":
|
|
6703
|
+
cleanup();
|
|
6704
|
+
resolve();
|
|
6705
|
+
return;
|
|
6706
|
+
case "\x1B[A":
|
|
6707
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
6708
|
+
message = "";
|
|
6709
|
+
break;
|
|
6710
|
+
case "\x1B[B":
|
|
6711
|
+
selectedIndex = Math.min(total - 1, selectedIndex + 1);
|
|
6712
|
+
message = "";
|
|
6713
|
+
break;
|
|
6714
|
+
case "o": {
|
|
6715
|
+
const template = getAction("open", config);
|
|
6716
|
+
if (template) {
|
|
6717
|
+
const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
|
|
6718
|
+
message = source_default.dim(`opening ${pr.repo}#${pr.number}...`);
|
|
6719
|
+
render(result, selectedIndex, message);
|
|
6720
|
+
try {
|
|
6721
|
+
await executeCommand(cmd);
|
|
6722
|
+
message = source_default.green(`opened ${pr.repo}#${pr.number}`);
|
|
6723
|
+
} catch {
|
|
6724
|
+
message = source_default.red("failed to open");
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
break;
|
|
6728
|
+
}
|
|
6729
|
+
case "r": {
|
|
6730
|
+
const template = getAction("review", config);
|
|
6731
|
+
if (template) {
|
|
6732
|
+
const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
|
|
6733
|
+
message = source_default.dim(`opening review for ${pr.repo}#${pr.number}...`);
|
|
6734
|
+
render(result, selectedIndex, message);
|
|
6735
|
+
try {
|
|
6736
|
+
await executeCommand(cmd);
|
|
6737
|
+
message = source_default.green(`opened review for ${pr.repo}#${pr.number}`);
|
|
6738
|
+
} catch {
|
|
6739
|
+
message = source_default.red("failed to open review");
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
break;
|
|
6743
|
+
}
|
|
6744
|
+
case "n": {
|
|
6745
|
+
const template = getAction("nudge", config);
|
|
6746
|
+
if (template) {
|
|
6747
|
+
const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
|
|
6748
|
+
message = source_default.dim(`nudging ${pr.repo}#${pr.number}...`);
|
|
6749
|
+
render(result, selectedIndex, message);
|
|
6750
|
+
try {
|
|
6751
|
+
await executeCommand(cmd);
|
|
6752
|
+
message = source_default.green(`nudged ${pr.repo}#${pr.number}`);
|
|
6753
|
+
} catch {
|
|
6754
|
+
message = source_default.red("failed to nudge");
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
break;
|
|
6758
|
+
}
|
|
6759
|
+
case "c": {
|
|
6760
|
+
const url = pr.url;
|
|
6761
|
+
try {
|
|
6762
|
+
const proc = process.platform === "darwin" ? "pbcopy" : process.platform === "linux" ? "xclip -selection clipboard" : "clip";
|
|
6763
|
+
await executeCommand(`echo "${url}" | ${proc}`);
|
|
6764
|
+
message = source_default.green("url copied");
|
|
6765
|
+
} catch {
|
|
6766
|
+
message = source_default.dim(url);
|
|
6767
|
+
}
|
|
6768
|
+
break;
|
|
6769
|
+
}
|
|
6770
|
+
default:
|
|
6771
|
+
break;
|
|
6772
|
+
}
|
|
6773
|
+
render(result, selectedIndex, message);
|
|
6774
|
+
});
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
6777
|
+
|
|
6778
|
+
// src/output.ts
|
|
6779
|
+
var CATEGORY_CONFIG2 = {
|
|
6780
|
+
"needs-re-review": {
|
|
6781
|
+
icon: "◆",
|
|
6782
|
+
label: "Needs Re-review",
|
|
6783
|
+
color: source_default.yellow
|
|
6784
|
+
},
|
|
6785
|
+
requested: { icon: "●", label: "Requested Reviews", color: source_default.green },
|
|
6786
|
+
stale: { icon: "○", label: "Stale", color: source_default.red },
|
|
6787
|
+
"waiting-on-others": {
|
|
6788
|
+
icon: "◇",
|
|
6789
|
+
label: "Your PRs Waiting",
|
|
6790
|
+
color: source_default.dim
|
|
6791
|
+
}
|
|
6792
|
+
};
|
|
6793
|
+
var CATEGORY_ORDER2 = [
|
|
6794
|
+
"needs-re-review",
|
|
6795
|
+
"requested",
|
|
6796
|
+
"stale",
|
|
6797
|
+
"waiting-on-others"
|
|
6798
|
+
];
|
|
6607
6799
|
function formatPR(pr) {
|
|
6608
6800
|
const draft = pr.isDraft ? source_default.dim(" [draft]") : "";
|
|
6609
6801
|
const prRef = source_default.cyan(`${pr.repo}#${pr.number}`);
|
|
@@ -6627,12 +6819,12 @@ function formatStatus(result) {
|
|
|
6627
6819
|
grouped.set(pr.category, list);
|
|
6628
6820
|
}
|
|
6629
6821
|
let hasContent = false;
|
|
6630
|
-
for (const category of
|
|
6822
|
+
for (const category of CATEGORY_ORDER2) {
|
|
6631
6823
|
const prs = grouped.get(category);
|
|
6632
6824
|
if (!prs || prs.length === 0)
|
|
6633
6825
|
continue;
|
|
6634
6826
|
hasContent = true;
|
|
6635
|
-
const config =
|
|
6827
|
+
const config = CATEGORY_CONFIG2[category];
|
|
6636
6828
|
lines.push("");
|
|
6637
6829
|
lines.push(` ${config.color(`${config.icon} ${config.label}`)} ${source_default.dim(`(${prs.length})`)}`);
|
|
6638
6830
|
for (const pr of prs) {
|
|
@@ -6656,7 +6848,7 @@ function formatStatus(result) {
|
|
|
6656
6848
|
}
|
|
6657
6849
|
|
|
6658
6850
|
// src/commands/status.ts
|
|
6659
|
-
async function statusCommand(config, json) {
|
|
6851
|
+
async function statusCommand(config, json, interactive) {
|
|
6660
6852
|
const user = config.user ?? await getAuthenticatedUser();
|
|
6661
6853
|
process.stderr.write(`Fetching PRs for ${user}...
|
|
6662
6854
|
`);
|
|
@@ -6677,6 +6869,8 @@ async function statusCommand(config, json) {
|
|
|
6677
6869
|
if (json) {
|
|
6678
6870
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6679
6871
|
`);
|
|
6872
|
+
} else if (interactive && process.stdin.isTTY) {
|
|
6873
|
+
await interactiveMode(result, config);
|
|
6680
6874
|
} else {
|
|
6681
6875
|
process.stdout.write(formatStatus(result));
|
|
6682
6876
|
}
|
|
@@ -10699,12 +10893,12 @@ function getVersion() {
|
|
|
10699
10893
|
function createCLI() {
|
|
10700
10894
|
const program2 = new Command;
|
|
10701
10895
|
program2.name("prq").description("PR Queue — see what code reviews need your attention").version(getVersion());
|
|
10702
|
-
program2.command("status", { isDefault: true }).description("Show PRs needing your attention").option("-r, --repos <repos...>", "Filter to specific repos (owner/name)").option("-s, --stale-days <days>", "Days of inactivity to consider stale", "3").option("--json", "Output as JSON").action(async (opts) => {
|
|
10896
|
+
program2.command("status", { isDefault: true }).description("Show PRs needing your attention").option("-r, --repos <repos...>", "Filter to specific repos (owner/name)").option("-s, --stale-days <days>", "Days of inactivity to consider stale", "3").option("--json", "Output as JSON").option("-i, --interactive", "Interactive mode with keyboard shortcuts").action(async (opts) => {
|
|
10703
10897
|
const config = loadConfig({
|
|
10704
10898
|
repos: opts.repos,
|
|
10705
10899
|
staleDays: opts.staleDays ? parseInt(opts.staleDays, 10) : undefined
|
|
10706
10900
|
});
|
|
10707
|
-
await statusCommand(config, opts.json ?? false);
|
|
10901
|
+
await statusCommand(config, opts.json ?? false, opts.interactive ?? false);
|
|
10708
10902
|
});
|
|
10709
10903
|
program2.command("open <identifier>").description("Open a PR in the browser").action(async (identifier) => {
|
|
10710
10904
|
const config = loadConfig({});
|