prq-cli 0.6.0 → 0.7.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 +129 -56
- package/dist/bin/prq.js +79 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
PR Queue — see what code reviews need your attention.
|
|
6
6
|
|
|
7
|
-
A CLI tool that
|
|
7
|
+
A CLI tool that shows you a categorized view of PRs that need action — then lets you act on them with whatever tools you already use. PRQ is the queue. You bring the workflow.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -17,83 +17,169 @@ Requires [GitHub CLI](https://cli.github.com/) (`gh`) to be authenticated.
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# See your review queue
|
|
20
|
+
# See your review queue (interactive by default)
|
|
21
21
|
prq
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
prq -
|
|
23
|
+
# Non-interactive / plain text
|
|
24
|
+
prq --no-interactive
|
|
25
|
+
|
|
26
|
+
# Act on a PR
|
|
27
|
+
prq review 482
|
|
28
|
+
prq open 482
|
|
29
|
+
prq nudge 482
|
|
25
30
|
```
|
|
26
31
|
|
|
27
32
|
## Commands
|
|
28
33
|
|
|
29
34
|
### `prq status` (default)
|
|
30
35
|
|
|
31
|
-
Shows PRs needing your attention
|
|
36
|
+
Shows PRs needing your attention in four categories:
|
|
32
37
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
38
|
+
- **◆ Needs Re-review** — new commits pushed after your review
|
|
39
|
+
- **● Requested** — you're a requested reviewer
|
|
40
|
+
- **○ Stale** — no activity for N days
|
|
41
|
+
- **◇ Your PRs Waiting** — waiting on someone else
|
|
37
42
|
|
|
38
43
|
```bash
|
|
39
|
-
prq #
|
|
44
|
+
prq # interactive mode (default)
|
|
40
45
|
prq status --repos org/repo1 org/repo2 # specific repos
|
|
41
|
-
prq status --stale-days 7 # custom
|
|
42
|
-
prq status --json # machine-readable
|
|
43
|
-
prq
|
|
46
|
+
prq status --stale-days 7 # custom threshold
|
|
47
|
+
prq status --json # machine-readable
|
|
48
|
+
prq --no-interactive # plain text output
|
|
44
49
|
```
|
|
45
50
|
|
|
46
|
-
### Interactive Mode
|
|
51
|
+
### Interactive Mode (default)
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
Interactive mode is the default when running in a terminal. Navigate your queue with keyboard shortcuts:
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
| Key | Action |
|
|
56
|
+
|-----|--------|
|
|
57
|
+
| ↑↓ | Navigate between PRs |
|
|
58
|
+
| r | Review — open files changed |
|
|
59
|
+
| o | Open — open PR in browser |
|
|
60
|
+
| n | Nudge — post a comment |
|
|
61
|
+
| c | Copy URL to clipboard |
|
|
62
|
+
| a | Actions — open menu with all actions |
|
|
63
|
+
| q | Quit |
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
Press **a** to open the actions menu, which lists all actions (built-in and custom from your config). Press **1-9** to run an action, or **q** to dismiss.
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
### `prq open/review/nudge <identifier>`
|
|
68
|
+
|
|
69
|
+
Act on PRs by number, `org/repo#number`, or full URL:
|
|
60
70
|
|
|
61
71
|
```bash
|
|
62
|
-
prq open 482 #
|
|
63
|
-
prq
|
|
64
|
-
prq
|
|
72
|
+
prq open 482 # open in browser
|
|
73
|
+
prq review 482 # open files changed
|
|
74
|
+
prq nudge 482 # post a comment
|
|
75
|
+
prq nudge 482 --yes --message "Update?" # skip confirmation
|
|
65
76
|
```
|
|
66
77
|
|
|
67
|
-
### `prq
|
|
78
|
+
### `prq run <action> <identifier>`
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
Run any custom action you've defined:
|
|
70
81
|
|
|
71
82
|
```bash
|
|
72
|
-
prq
|
|
73
|
-
prq review
|
|
83
|
+
prq run checkout 482
|
|
84
|
+
prq run ai-review 482
|
|
74
85
|
```
|
|
75
86
|
|
|
76
|
-
### `prq
|
|
87
|
+
### `prq skill`
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
Install the `/prq` skill for Claude Code:
|
|
79
90
|
|
|
80
91
|
```bash
|
|
81
|
-
prq
|
|
82
|
-
prq
|
|
83
|
-
prq nudge 482 --message "Any updates?" # custom message
|
|
92
|
+
prq skill # install in current project
|
|
93
|
+
prq skill --global # install globally
|
|
84
94
|
```
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
## Pluggable Actions
|
|
97
|
+
|
|
98
|
+
PRQ doesn't force a workflow. Every action is a configurable shell command template. Override the defaults or add your own in `.prqrc.json`:
|
|
99
|
+
|
|
100
|
+
### Use Claude Code for reviews
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"actions": {
|
|
105
|
+
"review": "claude -p '/review {url}'"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Now `prq review 482` dispatches to Claude Code.
|
|
111
|
+
|
|
112
|
+
### Use Codex for reviews
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"actions": {
|
|
117
|
+
"review": "codex exec --full-auto 'review the PR at {url}'"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Use gh CLI to checkout
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"actions": {
|
|
127
|
+
"checkout": "gh pr checkout {number} --repo {owner}/{repo}"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then `prq run checkout 482`.
|
|
87
133
|
|
|
88
|
-
|
|
134
|
+
### Just open in browser (default)
|
|
135
|
+
|
|
136
|
+
With no config, `prq review` opens the files changed tab and `prq open` opens the PR page. Zero setup needed.
|
|
137
|
+
|
|
138
|
+
### Template variables
|
|
139
|
+
|
|
140
|
+
| Variable | Example |
|
|
141
|
+
|----------|---------|
|
|
142
|
+
| `{url}` | `https://github.com/org/repo/pull/482` |
|
|
143
|
+
| `{number}` | `482` |
|
|
144
|
+
| `{owner}` | `org` |
|
|
145
|
+
| `{repo}` | `repo` |
|
|
146
|
+
| `{title}` | `fix: handle edge case` |
|
|
147
|
+
| `{author}` | `alice` |
|
|
148
|
+
| `{days}` | `5` |
|
|
149
|
+
|
|
150
|
+
## Agent & Automation
|
|
151
|
+
|
|
152
|
+
PRQ is fully scriptable with `--json` output and `--yes` flags:
|
|
89
153
|
|
|
90
154
|
```bash
|
|
91
|
-
|
|
155
|
+
# Agent reads the queue
|
|
156
|
+
prq status --json
|
|
157
|
+
|
|
158
|
+
# Agent nudges all stale PRs
|
|
159
|
+
prq status --json | jq -r '.prs[] | select(.category == "stale") | .number' \
|
|
160
|
+
| xargs -I{} prq nudge {} --yes
|
|
161
|
+
|
|
162
|
+
# Claude Code cron
|
|
163
|
+
prq status --json | claude -p "Review needs-re-review PRs older than 7 days"
|
|
92
164
|
```
|
|
93
165
|
|
|
94
|
-
###
|
|
166
|
+
### Claude Code Skill
|
|
167
|
+
|
|
168
|
+
Install the `/prq` skill to use PRQ inside Claude Code sessions:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
prq skill --global
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Then in Claude Code:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
/prq → show queue, ask what to do
|
|
178
|
+
"review 2439" → dispatches to your configured review action
|
|
179
|
+
"nudge all stale PRs" → batch nudge
|
|
180
|
+
```
|
|
95
181
|
|
|
96
|
-
|
|
182
|
+
The skill reads your `.prqrc.json` actions — if you have `/review` configured, it uses that. If not, it falls back to opening the browser.
|
|
97
183
|
|
|
98
184
|
## Configuration
|
|
99
185
|
|
|
@@ -103,7 +189,7 @@ Config is loaded in this order (later overrides earlier):
|
|
|
103
189
|
2. `.prqrc.json` — per-project config
|
|
104
190
|
3. CLI flags
|
|
105
191
|
|
|
106
|
-
|
|
192
|
+
Full example:
|
|
107
193
|
|
|
108
194
|
```json
|
|
109
195
|
{
|
|
@@ -111,25 +197,12 @@ Example `.prqrc.json`:
|
|
|
111
197
|
"staleDays": 5,
|
|
112
198
|
"actions": {
|
|
113
199
|
"review": "claude -p '/review {url}'",
|
|
114
|
-
"checkout": "gh pr checkout {number} --repo {owner}/{repo}"
|
|
200
|
+
"checkout": "gh pr checkout {number} --repo {owner}/{repo}",
|
|
201
|
+
"approve": "gh pr review {number} --repo {owner}/{repo} --approve"
|
|
115
202
|
}
|
|
116
203
|
}
|
|
117
204
|
```
|
|
118
205
|
|
|
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
|
-
|
|
133
206
|
## License
|
|
134
207
|
|
|
135
208
|
MIT
|
package/dist/bin/prq.js
CHANGED
|
@@ -6732,7 +6732,8 @@ function toResolvedPR(pr) {
|
|
|
6732
6732
|
updatedAt: pr.updatedAt
|
|
6733
6733
|
};
|
|
6734
6734
|
}
|
|
6735
|
-
function render(
|
|
6735
|
+
function render(state) {
|
|
6736
|
+
const { result, selectedIndex, message, actionMenu } = state;
|
|
6736
6737
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
6737
6738
|
const lines = [];
|
|
6738
6739
|
lines.push(source_default.bold(` PRQ Status for ${result.user}`));
|
|
@@ -6775,8 +6776,20 @@ function render(result, selectedIndex, message) {
|
|
|
6775
6776
|
}
|
|
6776
6777
|
lines.push("");
|
|
6777
6778
|
lines.push(source_default.dim(` ${"─".repeat(50)}`));
|
|
6778
|
-
|
|
6779
|
-
|
|
6779
|
+
if (actionMenu) {
|
|
6780
|
+
const pr = result.prs[selectedIndex];
|
|
6781
|
+
lines.push("");
|
|
6782
|
+
lines.push(` ${source_default.bold("Actions")} for ${source_default.white(`#${pr.number}`)}`);
|
|
6783
|
+
lines.push("");
|
|
6784
|
+
for (let i = 0;i < actionMenu.length; i++) {
|
|
6785
|
+
lines.push(` ${source_default.white(String(i + 1))}. ${actionMenu[i].name}`);
|
|
6786
|
+
}
|
|
6787
|
+
lines.push("");
|
|
6788
|
+
lines.push(` ${source_default.dim("1-9")} run action ${source_default.white("q")} back`);
|
|
6789
|
+
} else {
|
|
6790
|
+
lines.push("");
|
|
6791
|
+
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("a")} actions ${source_default.white("q")} quit`);
|
|
6792
|
+
}
|
|
6780
6793
|
if (message) {
|
|
6781
6794
|
lines.push("");
|
|
6782
6795
|
lines.push(` ${message}`);
|
|
@@ -6784,6 +6797,17 @@ function render(result, selectedIndex, message) {
|
|
|
6784
6797
|
process.stdout.write(lines.join(`
|
|
6785
6798
|
`));
|
|
6786
6799
|
}
|
|
6800
|
+
async function runAction(actionName, template, pr, state) {
|
|
6801
|
+
const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
|
|
6802
|
+
state.message = source_default.dim(`running ${actionName} on ${pr.repo}#${pr.number}...`);
|
|
6803
|
+
render(state);
|
|
6804
|
+
try {
|
|
6805
|
+
await executeCommand(cmd);
|
|
6806
|
+
return source_default.green(`${actionName}: ${pr.repo}#${pr.number}`);
|
|
6807
|
+
} catch {
|
|
6808
|
+
return source_default.red(`${actionName} failed`);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6787
6811
|
async function interactiveMode(result, config) {
|
|
6788
6812
|
if (result.prs.length === 0) {
|
|
6789
6813
|
console.log(source_default.green(`
|
|
@@ -6791,9 +6815,14 @@ async function interactiveMode(result, config) {
|
|
|
6791
6815
|
`));
|
|
6792
6816
|
return;
|
|
6793
6817
|
}
|
|
6794
|
-
let selectedIndex = 0;
|
|
6795
|
-
let message = "";
|
|
6796
6818
|
const total = result.prs.length;
|
|
6819
|
+
const allActions = listActions(config);
|
|
6820
|
+
const state = {
|
|
6821
|
+
result,
|
|
6822
|
+
selectedIndex: 0,
|
|
6823
|
+
message: "",
|
|
6824
|
+
actionMenu: null
|
|
6825
|
+
};
|
|
6797
6826
|
if (!process.stdin.isTTY) {
|
|
6798
6827
|
process.stdout.write(`Interactive mode requires a terminal. Use prq status instead.
|
|
6799
6828
|
`);
|
|
@@ -6803,7 +6832,7 @@ async function interactiveMode(result, config) {
|
|
|
6803
6832
|
process.stdin.resume();
|
|
6804
6833
|
process.stdin.setEncoding("utf8");
|
|
6805
6834
|
process.stdout.write("\x1B[?25l");
|
|
6806
|
-
render(
|
|
6835
|
+
render(state);
|
|
6807
6836
|
return new Promise((resolve) => {
|
|
6808
6837
|
const cleanup = () => {
|
|
6809
6838
|
process.stdin.setRawMode(false);
|
|
@@ -6812,7 +6841,26 @@ async function interactiveMode(result, config) {
|
|
|
6812
6841
|
process.stdout.write("\x1B[?25h\x1B[2J\x1B[H");
|
|
6813
6842
|
};
|
|
6814
6843
|
process.stdin.on("data", async (key) => {
|
|
6815
|
-
const pr = result.prs[selectedIndex];
|
|
6844
|
+
const pr = result.prs[state.selectedIndex];
|
|
6845
|
+
if (state.actionMenu) {
|
|
6846
|
+
if (key === "q" || key === "\x1B" || key === "a") {
|
|
6847
|
+
state.actionMenu = null;
|
|
6848
|
+
state.message = "";
|
|
6849
|
+
} else if (key === "\x03") {
|
|
6850
|
+
cleanup();
|
|
6851
|
+
resolve();
|
|
6852
|
+
return;
|
|
6853
|
+
} else {
|
|
6854
|
+
const idx = parseInt(key, 10);
|
|
6855
|
+
if (idx >= 1 && idx <= state.actionMenu.length) {
|
|
6856
|
+
const action = state.actionMenu[idx - 1];
|
|
6857
|
+
state.message = await runAction(action.name, action.template, pr, state);
|
|
6858
|
+
state.actionMenu = null;
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
render(state);
|
|
6862
|
+
return;
|
|
6863
|
+
}
|
|
6816
6864
|
switch (key) {
|
|
6817
6865
|
case "q":
|
|
6818
6866
|
case "\x03":
|
|
@@ -6820,55 +6868,31 @@ async function interactiveMode(result, config) {
|
|
|
6820
6868
|
resolve();
|
|
6821
6869
|
return;
|
|
6822
6870
|
case "\x1B[A":
|
|
6823
|
-
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
6824
|
-
message = "";
|
|
6871
|
+
state.selectedIndex = Math.max(0, state.selectedIndex - 1);
|
|
6872
|
+
state.message = "";
|
|
6825
6873
|
break;
|
|
6826
6874
|
case "\x1B[B":
|
|
6827
|
-
selectedIndex = Math.min(total - 1, selectedIndex + 1);
|
|
6828
|
-
message = "";
|
|
6875
|
+
state.selectedIndex = Math.min(total - 1, state.selectedIndex + 1);
|
|
6876
|
+
state.message = "";
|
|
6829
6877
|
break;
|
|
6830
6878
|
case "o": {
|
|
6831
|
-
const template =
|
|
6879
|
+
const template = allActions.open;
|
|
6832
6880
|
if (template) {
|
|
6833
|
-
|
|
6834
|
-
message = source_default.dim(`opening ${pr.repo}#${pr.number}...`);
|
|
6835
|
-
render(result, selectedIndex, message);
|
|
6836
|
-
try {
|
|
6837
|
-
await executeCommand(cmd);
|
|
6838
|
-
message = source_default.green(`opened ${pr.repo}#${pr.number}`);
|
|
6839
|
-
} catch {
|
|
6840
|
-
message = source_default.red("failed to open");
|
|
6841
|
-
}
|
|
6881
|
+
state.message = await runAction("open", template, pr, state);
|
|
6842
6882
|
}
|
|
6843
6883
|
break;
|
|
6844
6884
|
}
|
|
6845
6885
|
case "r": {
|
|
6846
|
-
const template =
|
|
6886
|
+
const template = allActions.review;
|
|
6847
6887
|
if (template) {
|
|
6848
|
-
|
|
6849
|
-
message = source_default.dim(`opening review for ${pr.repo}#${pr.number}...`);
|
|
6850
|
-
render(result, selectedIndex, message);
|
|
6851
|
-
try {
|
|
6852
|
-
await executeCommand(cmd);
|
|
6853
|
-
message = source_default.green(`opened review for ${pr.repo}#${pr.number}`);
|
|
6854
|
-
} catch {
|
|
6855
|
-
message = source_default.red("failed to open review");
|
|
6856
|
-
}
|
|
6888
|
+
state.message = await runAction("review", template, pr, state);
|
|
6857
6889
|
}
|
|
6858
6890
|
break;
|
|
6859
6891
|
}
|
|
6860
6892
|
case "n": {
|
|
6861
|
-
const template =
|
|
6893
|
+
const template = allActions.nudge;
|
|
6862
6894
|
if (template) {
|
|
6863
|
-
|
|
6864
|
-
message = source_default.dim(`nudging ${pr.repo}#${pr.number}...`);
|
|
6865
|
-
render(result, selectedIndex, message);
|
|
6866
|
-
try {
|
|
6867
|
-
await executeCommand(cmd);
|
|
6868
|
-
message = source_default.green(`nudged ${pr.repo}#${pr.number}`);
|
|
6869
|
-
} catch {
|
|
6870
|
-
message = source_default.red("failed to nudge");
|
|
6871
|
-
}
|
|
6895
|
+
state.message = await runAction("nudge", template, pr, state);
|
|
6872
6896
|
}
|
|
6873
6897
|
break;
|
|
6874
6898
|
}
|
|
@@ -6877,16 +6901,25 @@ async function interactiveMode(result, config) {
|
|
|
6877
6901
|
try {
|
|
6878
6902
|
const proc = process.platform === "darwin" ? "pbcopy" : process.platform === "linux" ? "xclip -selection clipboard" : "clip";
|
|
6879
6903
|
await executeCommand(`echo "${url}" | ${proc}`);
|
|
6880
|
-
message = source_default.green("url copied");
|
|
6904
|
+
state.message = source_default.green("url copied");
|
|
6881
6905
|
} catch {
|
|
6882
|
-
message = source_default.dim(url);
|
|
6906
|
+
state.message = source_default.dim(url);
|
|
6883
6907
|
}
|
|
6884
6908
|
break;
|
|
6885
6909
|
}
|
|
6910
|
+
case "a": {
|
|
6911
|
+
const entries = Object.entries(allActions);
|
|
6912
|
+
state.actionMenu = entries.map(([name, template]) => ({
|
|
6913
|
+
name,
|
|
6914
|
+
template
|
|
6915
|
+
}));
|
|
6916
|
+
state.message = "";
|
|
6917
|
+
break;
|
|
6918
|
+
}
|
|
6886
6919
|
default:
|
|
6887
6920
|
break;
|
|
6888
6921
|
}
|
|
6889
|
-
render(
|
|
6922
|
+
render(state);
|
|
6890
6923
|
});
|
|
6891
6924
|
});
|
|
6892
6925
|
}
|
|
@@ -11009,12 +11042,12 @@ function getVersion() {
|
|
|
11009
11042
|
function createCLI() {
|
|
11010
11043
|
const program2 = new Command;
|
|
11011
11044
|
program2.name("prq").description("PR Queue — see what code reviews need your attention").version(getVersion());
|
|
11012
|
-
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("-
|
|
11045
|
+
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("--no-interactive", "Disable interactive mode").action(async (opts) => {
|
|
11013
11046
|
const config = loadConfig({
|
|
11014
11047
|
repos: opts.repos,
|
|
11015
11048
|
staleDays: opts.staleDays ? parseInt(opts.staleDays, 10) : undefined
|
|
11016
11049
|
});
|
|
11017
|
-
await statusCommand(config, opts.json ?? false, opts.interactive ??
|
|
11050
|
+
await statusCommand(config, opts.json ?? false, opts.interactive ?? true);
|
|
11018
11051
|
});
|
|
11019
11052
|
program2.command("open <identifier>").description("Open a PR in the browser").action(async (identifier) => {
|
|
11020
11053
|
const config = loadConfig({});
|