prq-cli 0.6.0 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +146 -57
  2. package/dist/bin/prq.js +105 -61
  3. 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 queries GitHub and shows you a categorized view of PRs that need action. No more missing review requests because someone forgot to re-request, no more stale PRs sitting idle.
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,185 @@ 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
- # Interactive mode navigate with arrow keys, act with shortcuts
24
- prq -i
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, grouped into four categories:
36
+ Shows PRs needing your attention in four categories:
32
37
 
33
- - **Needs Re-review** — you left a review, but new commits were pushed after
34
- - **Requested Reviews** — you're a requested reviewer and haven't reviewed yet
35
- - **Stale** — PRs you're involved in with no activity for N days
36
- - **Your PRs Waiting** — PRs you authored that are waiting on someone else
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 # all repos you have access to
44
+ prq # interactive mode (default)
40
45
  prq status --repos org/repo1 org/repo2 # specific repos
41
- prq status --stale-days 7 # custom stale threshold
42
- prq status --json # machine-readable output
43
- prq status -i # interactive mode
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)
52
+
53
+ Interactive mode is the default when running in a terminal. Navigate your queue with keyboard shortcuts:
47
54
 
48
- Run `prq -i` to navigate your queue with keyboard shortcuts:
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 |
49
64
 
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
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.
56
66
 
57
- ### `prq open <identifier>`
67
+ ### `prq open/review/nudge <identifier>`
58
68
 
59
- Open a PR in the browser.
69
+ Act on PRs by number, `org/repo#number`, or full URL:
60
70
 
61
71
  ```bash
62
- prq open 482 # searches your queue for PR #482
63
- prq open superdoc-dev/superdoc#482 # opens directly
64
- prq open https://github.com/org/repo/pull/482
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 review <identifier>`
78
+ ### `prq run <action> <identifier>`
68
79
 
69
- Open a PR's "Files changed" tab for review.
80
+ Run any custom action you've defined:
70
81
 
71
82
  ```bash
72
- prq review 482
73
- prq review superdoc-dev/superdoc#482
83
+ prq run checkout 482
84
+ prq run ai-review 482
74
85
  ```
75
86
 
76
- ### `prq nudge <identifier>`
87
+ ### `prq skill`
77
88
 
78
- Post a comment on a PR asking if it's still active.
89
+ Install the `/prq` skill for Claude Code:
79
90
 
80
91
  ```bash
81
- prq nudge 482 # confirm before posting
82
- prq nudge 482 --yes # skip confirmation
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
- ### `prq run <action> <identifier>`
96
+ ## Pluggable Actions
97
+
98
+ PRQ doesn't force a workflow. Every action is a configurable shell command template — inline commands or scripts. Override the defaults or add your own in `.prqrc.json`.
99
+
100
+ Actions run with full terminal control. When you trigger an action, prq suspends its TUI, the command takes over the screen (interactive tools like Claude Code work as normal), and prq resumes when the command exits.
101
+
102
+ ### Use Claude Code for reviews
103
+
104
+ ```json
105
+ {
106
+ "actions": {
107
+ "review": "claude '/review {url}'"
108
+ }
109
+ }
110
+ ```
111
+
112
+ Now `prq review 482` opens an interactive Claude Code session.
113
+
114
+ ### Use Codex for reviews
115
+
116
+ ```json
117
+ {
118
+ "actions": {
119
+ "review": "codex exec --full-auto 'review the PR at {url}'"
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Use a script for complex workflows
125
+
126
+ ```json
127
+ {
128
+ "actions": {
129
+ "review": "./scripts/review.sh {number} {url}"
130
+ }
131
+ }
132
+ ```
133
+
134
+ The script handles its own logic — session management, resuming, branching, whatever you need.
135
+
136
+ ### Use gh CLI to checkout
137
+
138
+ ```json
139
+ {
140
+ "actions": {
141
+ "checkout": "gh pr checkout {number} --repo {owner}/{repo}"
142
+ }
143
+ }
144
+ ```
145
+
146
+ Then `prq run checkout 482`.
147
+
148
+ ### Just open in browser (default)
149
+
150
+ With no config, `prq review` opens the files changed tab and `prq open` opens the PR page. Zero setup needed.
151
+
152
+ ### Template variables
153
+
154
+ | Variable | Example |
155
+ |----------|---------|
156
+ | `{url}` | `https://github.com/org/repo/pull/482` |
157
+ | `{number}` | `482` |
158
+ | `{owner}` | `org` |
159
+ | `{repo}` | `repo` |
160
+ | `{fullRepo}` | `org/repo` |
161
+ | `{title}` | `fix: handle edge case` |
162
+ | `{author}` | `alice` |
163
+ | `{days}` | `5` |
164
+ | `{category}` | `needs-re-review` |
165
+
166
+ ## Agent & Automation
87
167
 
88
- Run a custom action defined in your config.
168
+ PRQ is fully scriptable with `--json` output and `--yes` flags:
89
169
 
90
170
  ```bash
91
- prq run checkout 482
171
+ # Agent reads the queue
172
+ prq status --json
173
+
174
+ # Agent nudges all stale PRs
175
+ prq status --json | jq -r '.prs[] | select(.category == "stale") | .number' \
176
+ | xargs -I{} prq nudge {} --yes
177
+
178
+ # Claude Code cron
179
+ prq status --json | claude -p "Review needs-re-review PRs older than 7 days"
180
+ ```
181
+
182
+ ### Claude Code Skill
183
+
184
+ Install the `/prq` skill to use PRQ inside Claude Code sessions:
185
+
186
+ ```bash
187
+ prq skill --global
92
188
  ```
93
189
 
94
- ### `prq init`
190
+ Then in Claude Code:
95
191
 
96
- Creates a `.prqrc.json` config file in the current directory.
192
+ ```
193
+ /prq → show queue, ask what to do
194
+ "review 2439" → dispatches to your configured review action
195
+ "nudge all stale PRs" → batch nudge
196
+ ```
197
+
198
+ 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
199
 
98
200
  ## Configuration
99
201
 
@@ -103,33 +205,20 @@ Config is loaded in this order (later overrides earlier):
103
205
  2. `.prqrc.json` — per-project config
104
206
  3. CLI flags
105
207
 
106
- Example `.prqrc.json`:
208
+ Full example:
107
209
 
108
210
  ```json
109
211
  {
110
212
  "repos": ["org/repo1", "org/repo2"],
111
213
  "staleDays": 5,
112
214
  "actions": {
113
- "review": "claude -p '/review {url}'",
114
- "checkout": "gh pr checkout {number} --repo {owner}/{repo}"
215
+ "review": "claude '/review {url}'",
216
+ "checkout": "gh pr checkout {number} --repo {owner}/{repo}",
217
+ "approve": "gh pr review {number} --repo {owner}/{repo} --approve"
115
218
  }
116
219
  }
117
220
  ```
118
221
 
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
222
  ## License
134
223
 
135
224
  MIT
package/dist/bin/prq.js CHANGED
@@ -6424,7 +6424,7 @@ function getAction(name, config) {
6424
6424
  function listActions(config) {
6425
6425
  return { ...DEFAULT_ACTIONS, ...config.actions };
6426
6426
  }
6427
- function buildContext(pr) {
6427
+ function buildContext(pr, category = "") {
6428
6428
  const days = Math.floor((Date.now() - new Date(pr.updatedAt || Date.now()).getTime()) / 86400000);
6429
6429
  return {
6430
6430
  url: pr.url,
@@ -6434,7 +6434,8 @@ function buildContext(pr) {
6434
6434
  fullRepo: `${pr.owner}/${pr.repo}`,
6435
6435
  title: pr.title,
6436
6436
  author: pr.author,
6437
- days
6437
+ days,
6438
+ category
6438
6439
  };
6439
6440
  }
6440
6441
  function interpolate(template, context) {
@@ -6732,7 +6733,8 @@ function toResolvedPR(pr) {
6732
6733
  updatedAt: pr.updatedAt
6733
6734
  };
6734
6735
  }
6735
- function render(result, selectedIndex, message) {
6736
+ function render(state) {
6737
+ const { result, selectedIndex, message, actionMenu } = state;
6736
6738
  process.stdout.write("\x1B[2J\x1B[H");
6737
6739
  const lines = [];
6738
6740
  lines.push(source_default.bold(` PRQ Status for ${result.user}`));
@@ -6775,8 +6777,20 @@ function render(result, selectedIndex, message) {
6775
6777
  }
6776
6778
  lines.push("");
6777
6779
  lines.push(source_default.dim(` ${"─".repeat(50)}`));
6778
- lines.push("");
6779
- 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`);
6780
+ if (actionMenu) {
6781
+ const pr = result.prs[selectedIndex];
6782
+ lines.push("");
6783
+ lines.push(` ${source_default.bold("Actions")} for ${source_default.white(`#${pr.number}`)}`);
6784
+ lines.push("");
6785
+ for (let i = 0;i < actionMenu.length; i++) {
6786
+ lines.push(` ${source_default.white(String(i + 1))}. ${actionMenu[i].name}`);
6787
+ }
6788
+ lines.push("");
6789
+ lines.push(` ${source_default.dim("1-9")} run action ${source_default.white("q")} back`);
6790
+ } else {
6791
+ lines.push("");
6792
+ 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`);
6793
+ }
6780
6794
  if (message) {
6781
6795
  lines.push("");
6782
6796
  lines.push(` ${message}`);
@@ -6784,6 +6798,37 @@ function render(result, selectedIndex, message) {
6784
6798
  process.stdout.write(lines.join(`
6785
6799
  `));
6786
6800
  }
6801
+ function suspend() {
6802
+ process.stdin.setRawMode(false);
6803
+ process.stdin.pause();
6804
+ process.stdin.removeAllListeners("data");
6805
+ process.stdout.write("\x1B[?25h\x1B[2J\x1B[H");
6806
+ }
6807
+ function resume(state, onData) {
6808
+ process.stdin.setRawMode(true);
6809
+ process.stdin.resume();
6810
+ process.stdin.setEncoding("utf8");
6811
+ process.stdout.write("\x1B[?25l");
6812
+ process.stdin.on("data", onData);
6813
+ render(state);
6814
+ }
6815
+ async function runAction(actionName, template, pr, state, onData) {
6816
+ const context = buildContext(toResolvedPR(pr), pr.category);
6817
+ const cmd = interpolate(template, context);
6818
+ suspend();
6819
+ process.stdout.write(source_default.dim(`
6820
+ running ${actionName} on ${pr.repo}#${pr.number}...
6821
+
6822
+ `));
6823
+ try {
6824
+ await executeCommand(cmd);
6825
+ resume(state, onData);
6826
+ return source_default.green(`${actionName}: ${pr.repo}#${pr.number}`);
6827
+ } catch {
6828
+ resume(state, onData);
6829
+ return source_default.red(`${actionName} failed`);
6830
+ }
6831
+ }
6787
6832
  async function interactiveMode(result, config) {
6788
6833
  if (result.prs.length === 0) {
6789
6834
  console.log(source_default.green(`
@@ -6791,84 +6836,73 @@ async function interactiveMode(result, config) {
6791
6836
  `));
6792
6837
  return;
6793
6838
  }
6794
- let selectedIndex = 0;
6795
- let message = "";
6796
6839
  const total = result.prs.length;
6840
+ const allActions = listActions(config);
6841
+ const state = {
6842
+ result,
6843
+ selectedIndex: 0,
6844
+ message: "",
6845
+ actionMenu: null
6846
+ };
6797
6847
  if (!process.stdin.isTTY) {
6798
6848
  process.stdout.write(`Interactive mode requires a terminal. Use prq status instead.
6799
6849
  `);
6800
6850
  return;
6801
6851
  }
6802
- process.stdin.setRawMode(true);
6803
- process.stdin.resume();
6804
- process.stdin.setEncoding("utf8");
6805
- process.stdout.write("\x1B[?25l");
6806
- render(result, selectedIndex, message);
6807
6852
  return new Promise((resolve) => {
6808
- const cleanup = () => {
6809
- process.stdin.setRawMode(false);
6810
- process.stdin.pause();
6811
- process.stdin.removeAllListeners("data");
6812
- process.stdout.write("\x1B[?25h\x1B[2J\x1B[H");
6813
- };
6814
- process.stdin.on("data", async (key) => {
6815
- const pr = result.prs[selectedIndex];
6853
+ const onData = async (key) => {
6854
+ const pr = result.prs[state.selectedIndex];
6855
+ if (state.actionMenu) {
6856
+ if (key === "q" || key === "\x1B" || key === "a") {
6857
+ state.actionMenu = null;
6858
+ state.message = "";
6859
+ } else if (key === "\x03") {
6860
+ suspend();
6861
+ resolve();
6862
+ return;
6863
+ } else {
6864
+ const idx = parseInt(key, 10);
6865
+ if (idx >= 1 && idx <= state.actionMenu.length) {
6866
+ const action = state.actionMenu[idx - 1];
6867
+ state.message = await runAction(action.name, action.template, pr, state, onData);
6868
+ state.actionMenu = null;
6869
+ }
6870
+ }
6871
+ render(state);
6872
+ return;
6873
+ }
6816
6874
  switch (key) {
6817
6875
  case "q":
6818
6876
  case "\x03":
6819
- cleanup();
6877
+ suspend();
6820
6878
  resolve();
6821
6879
  return;
6822
6880
  case "\x1B[A":
6823
- selectedIndex = Math.max(0, selectedIndex - 1);
6824
- message = "";
6881
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
6882
+ state.message = "";
6825
6883
  break;
6826
6884
  case "\x1B[B":
6827
- selectedIndex = Math.min(total - 1, selectedIndex + 1);
6828
- message = "";
6885
+ state.selectedIndex = Math.min(total - 1, state.selectedIndex + 1);
6886
+ state.message = "";
6829
6887
  break;
6830
6888
  case "o": {
6831
- const template = getAction("open", config);
6889
+ const template = allActions.open;
6832
6890
  if (template) {
6833
- const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
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
- }
6891
+ state.message = await runAction("open", template, pr, state, onData);
6842
6892
  }
6843
6893
  break;
6844
6894
  }
6845
6895
  case "r": {
6846
- const template = getAction("review", config);
6896
+ const template = allActions.review;
6847
6897
  if (template) {
6848
- const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
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
- }
6898
+ state.message = await runAction("review", template, pr, state, onData);
6857
6899
  }
6858
6900
  break;
6859
6901
  }
6860
6902
  case "n": {
6861
- const template = getAction("nudge", config);
6903
+ const template = allActions.nudge;
6862
6904
  if (template) {
6863
- const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
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
- }
6905
+ state.message = await runAction("nudge", template, pr, state, onData);
6872
6906
  }
6873
6907
  break;
6874
6908
  }
@@ -6877,17 +6911,27 @@ async function interactiveMode(result, config) {
6877
6911
  try {
6878
6912
  const proc = process.platform === "darwin" ? "pbcopy" : process.platform === "linux" ? "xclip -selection clipboard" : "clip";
6879
6913
  await executeCommand(`echo "${url}" | ${proc}`);
6880
- message = source_default.green("url copied");
6914
+ state.message = source_default.green("url copied");
6881
6915
  } catch {
6882
- message = source_default.dim(url);
6916
+ state.message = source_default.dim(url);
6883
6917
  }
6884
6918
  break;
6885
6919
  }
6920
+ case "a": {
6921
+ const entries = Object.entries(allActions);
6922
+ state.actionMenu = entries.map(([name, template]) => ({
6923
+ name,
6924
+ template
6925
+ }));
6926
+ state.message = "";
6927
+ break;
6928
+ }
6886
6929
  default:
6887
6930
  break;
6888
6931
  }
6889
- render(result, selectedIndex, message);
6890
- });
6932
+ render(state);
6933
+ };
6934
+ resume(state, onData);
6891
6935
  });
6892
6936
  }
6893
6937
 
@@ -11009,12 +11053,12 @@ function getVersion() {
11009
11053
  function createCLI() {
11010
11054
  const program2 = new Command;
11011
11055
  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("-i, --interactive", "Interactive mode with keyboard shortcuts").action(async (opts) => {
11056
+ 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
11057
  const config = loadConfig({
11014
11058
  repos: opts.repos,
11015
11059
  staleDays: opts.staleDays ? parseInt(opts.staleDays, 10) : undefined
11016
11060
  });
11017
- await statusCommand(config, opts.json ?? false, opts.interactive ?? false);
11061
+ await statusCommand(config, opts.json ?? false, opts.interactive ?? true);
11018
11062
  });
11019
11063
  program2.command("open <identifier>").description("Open a PR in the browser").action(async (identifier) => {
11020
11064
  const config = loadConfig({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prq-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "PR Queue — see what code reviews need your attention",
5
5
  "type": "module",
6
6
  "bin": {