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.
Files changed (3) hide show
  1. package/README.md +129 -56
  2. package/dist/bin/prq.js +79 -46
  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,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
- # 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)
47
52
 
48
- Run `prq -i` to navigate your queue with keyboard shortcuts:
53
+ Interactive mode is the default when running in a terminal. Navigate your queue with keyboard shortcuts:
49
54
 
50
- - **↑↓** navigate between PRs
51
- - **r** review — open files changed tab
52
- - **o** open open PR in browser
53
- - **n** nudgepost a comment
54
- - **c** copy URL to clipboard
55
- - **q** quit
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
- ### `prq open <identifier>`
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
- Open a PR in the browser.
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 # 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. 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
- Run a custom action defined in your config.
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
- prq run checkout 482
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
- ### `prq init`
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
- Creates a `.prqrc.json` config file in the current directory.
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
- Example `.prqrc.json`:
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(result, selectedIndex, message) {
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
- 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`);
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(result, selectedIndex, message);
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 = getAction("open", config);
6879
+ const template = allActions.open;
6832
6880
  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
- }
6881
+ state.message = await runAction("open", template, pr, state);
6842
6882
  }
6843
6883
  break;
6844
6884
  }
6845
6885
  case "r": {
6846
- const template = getAction("review", config);
6886
+ const template = allActions.review;
6847
6887
  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
- }
6888
+ state.message = await runAction("review", template, pr, state);
6857
6889
  }
6858
6890
  break;
6859
6891
  }
6860
6892
  case "n": {
6861
- const template = getAction("nudge", config);
6893
+ const template = allActions.nudge;
6862
6894
  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
- }
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(result, selectedIndex, message);
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("-i, --interactive", "Interactive mode with keyboard shortcuts").action(async (opts) => {
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 ?? false);
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({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prq-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "PR Queue — see what code reviews need your attention",
5
5
  "type": "module",
6
6
  "bin": {