prq-cli 0.8.0 → 0.10.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 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 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.
7
+ Four categories. What needs re-review, what's requested, what's stale, what's waiting on others. `prq` is the queue. You bring the workflow.
8
8
 
9
9
  ## Install
10
10
 
@@ -40,6 +40,8 @@ Shows PRs needing your attention in four categories:
40
40
  - **○ Stale** — no activity for N days
41
41
  - **◇ Your PRs Waiting** — waiting on someone else
42
42
 
43
+ PRs you mark as started appear in a separate **▸ In Progress** group at the top.
44
+
43
45
  ```bash
44
46
  prq # interactive mode (default)
45
47
  prq status --repos org/repo1 org/repo2 # specific repos
@@ -58,6 +60,7 @@ Interactive mode is the default when running in a terminal. Navigate your queue
58
60
  | r | Review — open files changed |
59
61
  | o | Open — open PR in browser |
60
62
  | n | Nudge — post a comment |
63
+ | s | Start/Stop — mark as in progress |
61
64
  | c | Copy URL to clipboard |
62
65
  | a | Actions — open menu with all actions |
63
66
  | q | Quit |
@@ -93,9 +96,9 @@ prq skill # install in current project
93
96
  prq skill --global # install globally
94
97
  ```
95
98
 
96
- ## Pluggable Actions
99
+ ## Custom Actions
97
100
 
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`.
101
+ `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
102
 
100
103
  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
104
 
@@ -163,9 +166,9 @@ With no config, `prq review` opens the files changed tab and `prq open` opens th
163
166
  | `{days}` | `5` |
164
167
  | `{category}` | `needs-re-review` |
165
168
 
166
- ## Agent & Automation
169
+ ## Scripting
167
170
 
168
- PRQ is fully scriptable with `--json` output and `--yes` flags:
171
+ `prq` is fully scriptable with `--json` output and `--yes` flags:
169
172
 
170
173
  ```bash
171
174
  # Agent reads the queue
@@ -181,7 +184,7 @@ prq status --json | claude -p "Review needs-re-review PRs older than 7 days"
181
184
 
182
185
  ### Claude Code Skill
183
186
 
184
- Install the `/prq` skill to use PRQ inside Claude Code sessions:
187
+ Install the `/prq` skill to use `prq` inside Claude Code sessions:
185
188
 
186
189
  ```bash
187
190
  prq skill --global
package/dist/bin/prq.js CHANGED
@@ -2081,8 +2081,8 @@ var require_commander = __commonJS((exports) => {
2081
2081
  });
2082
2082
 
2083
2083
  // src/cli.ts
2084
- import fs3 from "node:fs";
2085
- import path3 from "node:path";
2084
+ import fs4 from "node:fs";
2085
+ import path4 from "node:path";
2086
2086
  import { fileURLToPath } from "node:url";
2087
2087
 
2088
2088
  // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
@@ -6482,98 +6482,62 @@ Available actions: ${available}`);
6482
6482
  // src/commands/skill.ts
6483
6483
  var SKILL_CONTENT = `---
6484
6484
  name: prq
6485
- description: PR review queue manager. Use when the user wants to check their PR review queue, review PRs, nudge stale PRs, or manage code review workflow. Triggers on mentions of "review queue", "PRs waiting", "stale PRs", "what needs review", or "prq".
6485
+ description: PR review queue manager. Use when the user wants to check their PR review queue, review PRs, nudge stale PRs, open PRs, or manage code review workflow. Triggers on mentions of "review queue", "PRs waiting", "stale PRs", "what needs review", "open PR", "nudge", or "prq".
6486
6486
  ---
6487
6487
 
6488
- # PRQ — PR Review Queue
6489
-
6490
- You have access to the \`prq\` CLI tool installed on this machine. Use it to help the user manage their code review queue.
6488
+ # PRQ CLI
6491
6489
 
6492
- ## Step 1: Get the queue
6490
+ \`prq\` is a CLI tool for managing code review queues. It's installed on this machine.
6493
6491
 
6494
- Run this command to get the user's current review queue:
6492
+ ## Commands
6495
6493
 
6496
6494
  \`\`\`bash
6495
+ # Show the review queue (JSON for parsing)
6497
6496
  prq status --json
6498
- \`\`\`
6499
6497
 
6500
- This returns a JSON object with categorized PRs:
6501
- - **needs-re-review** — PRs where the user left a review but new commits were pushed after
6502
- - **requested** — PRs where the user is a requested reviewer
6503
- - **stale** — PRs with no activity for N days
6504
- - **waiting-on-others** — PRs the user authored that are waiting on someone else
6498
+ # Open a PR in the browser
6499
+ prq open <number>
6500
+ prq open <owner/repo#number>
6505
6501
 
6506
- ## Step 2: Present the queue
6502
+ # Open files changed tab for review
6503
+ prq review <number>
6507
6504
 
6508
- Show the results in a clear, scannable format grouped by category. For each PR, show:
6509
- - The category symbol (◆ needs re-review, ● requested, ○ stale, ◇ waiting)
6510
- - The repo and PR number
6511
- - The title
6512
- - The detail (e.g., "new commits since your review 2d ago")
6505
+ # Post a nudge comment on a stale PR
6506
+ prq nudge <number> --yes
6507
+ prq nudge <number> --message "Custom message" --yes
6513
6508
 
6514
- Then ask what the user wants to do.
6509
+ # Run any configured action
6510
+ prq run <action> <number>
6511
+ \`\`\`
6515
6512
 
6516
- ## Step 3: Act on PRs
6513
+ ## JSON output
6517
6514
 
6518
- When the user asks to act on a PR, check the \`.prqrc.json\` file in the current directory (or \`~/.config/prq/config.json\`) for custom actions:
6515
+ \`prq status --json\` returns:
6519
6516
 
6520
6517
  \`\`\`json
6521
6518
  {
6522
- "actions": {
6523
- "review": "/review {url}",
6524
- "nudge": "shell:prq nudge {number} --yes"
6525
- }
6519
+ "user": "caio-pizzol",
6520
+ "prs": [
6521
+ {
6522
+ "category": "needs-re-review",
6523
+ "repo": "org/repo",
6524
+ "number": 2439,
6525
+ "title": "fix: something",
6526
+ "author": "alice",
6527
+ "url": "https://github.com/org/repo/pull/2439",
6528
+ "detail": "New commits since your review 1d ago"
6529
+ }
6530
+ ]
6526
6531
  }
6527
6532
  \`\`\`
6528
6533
 
6529
- ### Action resolution
6530
-
6531
- For each action template:
6532
- - **Starts with \`/\`** — it's a Claude Code skill. Invoke it by running the skill with the interpolated value. For example, \`/review https://github.com/org/repo/pull/123\`
6533
- - **Starts with \`shell:\`** — it's a shell command. Run it with the Bash tool. For example, \`prq nudge 123 --yes\`
6534
- - **Otherwise** — treat it as a prompt. Send it as a message.
6535
-
6536
- ### Template variables
6537
-
6538
- Replace these in the action template:
6539
- - \`{url}\` — full PR URL
6540
- - \`{number}\` — PR number
6541
- - \`{owner}\` — repo owner
6542
- - \`{repo}\` — repo name
6543
- - \`{title}\` — PR title
6544
- - \`{author}\` — PR author
6545
-
6546
- ### Default actions (if no config found)
6547
-
6548
- If no actions are configured, use these defaults:
6549
- - **review** — invoke \`/review {url}\` if the /review skill exists, otherwise run \`prq review {number}\` to open files changed in browser
6550
- - **nudge** — run \`prq nudge {number} --yes\`
6551
- - **open** — run \`prq open {number}\`
6534
+ Categories: \`needs-re-review\`, \`requested\`, \`stale\`, \`waiting-on-others\`.
6552
6535
 
6553
- ## Step 4: Batch operations
6536
+ ## Usage
6554
6537
 
6555
- If the user says things like "review all needs-re-review PRs" or "nudge all stale PRs":
6556
-
6557
- 1. Filter the queue JSON by the requested category
6558
- 2. Confirm the list with the user: "I'll review these 3 PRs: #2439, #2380, #2352. Proceed?"
6559
- 3. Execute the action on each PR sequentially
6560
-
6561
- ## Examples
6562
-
6563
- **User:** "check my review queue"
6564
- → Run \`prq status --json\`, present results, ask what to do
6565
-
6566
- **User:** "review 2439"
6567
- → Look up action for "review", interpolate with PR data, execute
6568
-
6569
- **User:** "nudge all stale PRs"
6570
- → Filter stale PRs from queue, confirm, run nudge on each
6571
-
6572
- **User:** "what PRs are waiting on me?"
6573
- → Run \`prq status --json\`, show only needs-re-review and requested categories
6574
-
6575
- **User:** "/prq" with no context
6576
- → Run \`prq status --json\`, present full queue, ask what to do
6538
+ - When the user asks about their review queue, run \`prq status --json\` and present the results grouped by category.
6539
+ - When the user asks to open, review, or nudge a PR, use the corresponding \`prq\` command.
6540
+ - For batch operations ("nudge all stale PRs"), filter the JSON output and confirm with the user before acting.
6577
6541
  `;
6578
6542
  function skillCommand(global) {
6579
6543
  if (global) {
@@ -6700,8 +6664,59 @@ function categorize(reviewedPRs, requestedPRs, authoredPRs, staleDays) {
6700
6664
  return results;
6701
6665
  }
6702
6666
 
6667
+ // src/state.ts
6668
+ import fs2 from "node:fs";
6669
+ import path2 from "node:path";
6670
+ var STATE_DIR = path2.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".config", "prq");
6671
+ var STATE_PATH = path2.join(STATE_DIR, "in-progress.json");
6672
+ function prKey(pr) {
6673
+ return `${pr.repo}#${pr.number}`;
6674
+ }
6675
+ function loadInProgress() {
6676
+ try {
6677
+ const data = JSON.parse(fs2.readFileSync(STATE_PATH, "utf8"));
6678
+ return new Set(Array.isArray(data) ? data : []);
6679
+ } catch {
6680
+ return new Set;
6681
+ }
6682
+ }
6683
+ function save(keys) {
6684
+ fs2.mkdirSync(STATE_DIR, { recursive: true });
6685
+ fs2.writeFileSync(STATE_PATH, JSON.stringify(Array.from(keys), null, 2));
6686
+ }
6687
+ function toggleInProgress(pr) {
6688
+ const keys = loadInProgress();
6689
+ const k = prKey(pr);
6690
+ if (keys.has(k)) {
6691
+ keys.delete(k);
6692
+ save(keys);
6693
+ return false;
6694
+ }
6695
+ keys.add(k);
6696
+ save(keys);
6697
+ return true;
6698
+ }
6699
+ function applyInProgress(prs) {
6700
+ const keys = loadInProgress();
6701
+ if (keys.size === 0)
6702
+ return prs;
6703
+ const activeKeys = new Set(prs.map(prKey));
6704
+ const staleKeys = Array.from(keys).filter((k) => !activeKeys.has(k));
6705
+ if (staleKeys.length > 0) {
6706
+ for (const k of staleKeys)
6707
+ keys.delete(k);
6708
+ save(keys);
6709
+ }
6710
+ return prs.map((pr) => keys.has(prKey(pr)) ? { ...pr, category: "in-progress" } : pr);
6711
+ }
6712
+
6703
6713
  // src/interactive.ts
6704
6714
  var CATEGORY_CONFIG = {
6715
+ "in-progress": {
6716
+ icon: "▸",
6717
+ label: "In Progress",
6718
+ color: source_default.cyan
6719
+ },
6705
6720
  "needs-re-review": {
6706
6721
  icon: "◆",
6707
6722
  label: "Needs Re-review",
@@ -6716,6 +6731,7 @@ var CATEGORY_CONFIG = {
6716
6731
  }
6717
6732
  };
6718
6733
  var CATEGORY_ORDER = [
6734
+ "in-progress",
6719
6735
  "needs-re-review",
6720
6736
  "requested",
6721
6737
  "stale",
@@ -6788,8 +6804,10 @@ function render(state) {
6788
6804
  lines.push("");
6789
6805
  lines.push(` ${source_default.dim("1-9")} run action ${source_default.white("q")} back`);
6790
6806
  } else {
6807
+ const pr = result.prs[selectedIndex];
6808
+ const sLabel = pr?.category === "in-progress" ? "stop" : "start";
6791
6809
  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`);
6810
+ lines.push(` ${source_default.dim("↑↓")} navigate ${source_default.white("r")} review ${source_default.white("o")} open ${source_default.white("n")} nudge ${source_default.white("s")} ${sLabel} ${source_default.white("c")} copy url ${source_default.white("a")} actions ${source_default.white("q")} quit`);
6793
6811
  }
6794
6812
  if (message) {
6795
6813
  lines.push("");
@@ -6829,7 +6847,7 @@ async function runAction(actionName, template, pr, state, onData) {
6829
6847
  return source_default.red(`${actionName} failed`);
6830
6848
  }
6831
6849
  }
6832
- async function interactiveMode(result, config) {
6850
+ async function interactiveMode(result, sourcePrs, config) {
6833
6851
  if (result.prs.length === 0) {
6834
6852
  console.log(source_default.green(`
6835
6853
  All clear! No PRs need your attention.
@@ -6840,6 +6858,7 @@ async function interactiveMode(result, config) {
6840
6858
  const allActions = listActions(config);
6841
6859
  const state = {
6842
6860
  result,
6861
+ sourcePrs,
6843
6862
  selectedIndex: 0,
6844
6863
  message: "",
6845
6864
  actionMenu: null
@@ -6917,6 +6936,19 @@ async function interactiveMode(result, config) {
6917
6936
  }
6918
6937
  break;
6919
6938
  }
6939
+ case "s": {
6940
+ const started = toggleInProgress(pr);
6941
+ state.result = {
6942
+ ...state.result,
6943
+ prs: applyInProgress(state.sourcePrs)
6944
+ };
6945
+ const newTotal = state.result.prs.length;
6946
+ if (state.selectedIndex >= newTotal) {
6947
+ state.selectedIndex = Math.max(0, newTotal - 1);
6948
+ }
6949
+ state.message = started ? source_default.cyan(`started: ${pr.repo}#${pr.number}`) : source_default.dim(`unmarked: ${pr.repo}#${pr.number}`);
6950
+ break;
6951
+ }
6920
6952
  case "a": {
6921
6953
  const entries = Object.entries(allActions);
6922
6954
  state.actionMenu = entries.map(([name, template]) => ({
@@ -6937,6 +6969,11 @@ async function interactiveMode(result, config) {
6937
6969
 
6938
6970
  // src/output.ts
6939
6971
  var CATEGORY_CONFIG2 = {
6972
+ "in-progress": {
6973
+ icon: "▸",
6974
+ label: "In Progress",
6975
+ color: source_default.cyan
6976
+ },
6940
6977
  "needs-re-review": {
6941
6978
  icon: "◆",
6942
6979
  label: "Needs Re-review",
@@ -6951,6 +6988,7 @@ var CATEGORY_CONFIG2 = {
6951
6988
  }
6952
6989
  };
6953
6990
  var CATEGORY_ORDER2 = [
6991
+ "in-progress",
6954
6992
  "needs-re-review",
6955
6993
  "requested",
6956
6994
  "stale",
@@ -7020,7 +7058,8 @@ async function statusCommand(config, json, interactive) {
7020
7058
  process.stderr.write(`Found ${reviewedRaw.length} reviewed, ${requestedPRs.length} requested, ${authoredPRs.length} authored
7021
7059
  `);
7022
7060
  const reviewedPRs = await enrichAllWithReviews(reviewedRaw, user);
7023
- const prs = categorize(reviewedPRs, requestedPRs, authoredPRs, config.staleDays);
7061
+ const categorized = categorize(reviewedPRs, requestedPRs, authoredPRs, config.staleDays);
7062
+ const prs = applyInProgress(categorized);
7024
7063
  const result = {
7025
7064
  user,
7026
7065
  timestamp: new Date().toISOString(),
@@ -7030,15 +7069,15 @@ async function statusCommand(config, json, interactive) {
7030
7069
  process.stdout.write(`${JSON.stringify(result, null, 2)}
7031
7070
  `);
7032
7071
  } else if (interactive && process.stdin.isTTY) {
7033
- await interactiveMode(result, config);
7072
+ await interactiveMode(result, categorized, config);
7034
7073
  } else {
7035
7074
  process.stdout.write(formatStatus(result));
7036
7075
  }
7037
7076
  }
7038
7077
 
7039
7078
  // src/config.ts
7040
- import fs2 from "node:fs";
7041
- import path2 from "node:path";
7079
+ import fs3 from "node:fs";
7080
+ import path3 from "node:path";
7042
7081
 
7043
7082
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
7044
7083
  var exports_external = {};
@@ -7515,8 +7554,8 @@ function getErrorMap() {
7515
7554
  }
7516
7555
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
7517
7556
  var makeIssue = (params) => {
7518
- const { data, path: path2, errorMaps, issueData } = params;
7519
- const fullPath = [...path2, ...issueData.path || []];
7557
+ const { data, path: path3, errorMaps, issueData } = params;
7558
+ const fullPath = [...path3, ...issueData.path || []];
7520
7559
  const fullIssue = {
7521
7560
  ...issueData,
7522
7561
  path: fullPath
@@ -7628,11 +7667,11 @@ var errorUtil;
7628
7667
 
7629
7668
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
7630
7669
  class ParseInputLazyPath {
7631
- constructor(parent, value, path2, key) {
7670
+ constructor(parent, value, path3, key) {
7632
7671
  this._cachedPath = [];
7633
7672
  this.parent = parent;
7634
7673
  this.data = value;
7635
- this._path = path2;
7674
+ this._path = path3;
7636
7675
  this._key = key;
7637
7676
  }
7638
7677
  get path() {
@@ -11022,14 +11061,14 @@ var configSchema = exports_external.object({
11022
11061
  });
11023
11062
  function loadConfig(cliOverrides) {
11024
11063
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
11025
- const globalPath = path2.join(home, ".config", "prq", "config.json");
11026
- const localPath = path2.join(process.cwd(), ".prqrc.json");
11064
+ const globalPath = path3.join(home, ".config", "prq", "config.json");
11065
+ const localPath = path3.join(process.cwd(), ".prqrc.json");
11027
11066
  let config = {};
11028
- if (fs2.existsSync(globalPath)) {
11029
- config = JSON.parse(fs2.readFileSync(globalPath, "utf8"));
11067
+ if (fs3.existsSync(globalPath)) {
11068
+ config = JSON.parse(fs3.readFileSync(globalPath, "utf8"));
11030
11069
  }
11031
- if (fs2.existsSync(localPath)) {
11032
- config = { ...config, ...JSON.parse(fs2.readFileSync(localPath, "utf8")) };
11070
+ if (fs3.existsSync(localPath)) {
11071
+ config = { ...config, ...JSON.parse(fs3.readFileSync(localPath, "utf8")) };
11033
11072
  }
11034
11073
  for (const [key, value] of Object.entries(cliOverrides)) {
11035
11074
  if (value !== undefined) {
@@ -11041,11 +11080,11 @@ function loadConfig(cliOverrides) {
11041
11080
 
11042
11081
  // src/cli.ts
11043
11082
  function getVersion() {
11044
- const __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
11083
+ const __dirname2 = path4.dirname(fileURLToPath(import.meta.url));
11045
11084
  for (const rel of ["../../package.json", "../package.json"]) {
11046
- const p = path3.resolve(__dirname2, rel);
11047
- if (fs3.existsSync(p)) {
11048
- return JSON.parse(fs3.readFileSync(p, "utf8")).version;
11085
+ const p = path4.resolve(__dirname2, rel);
11086
+ if (fs4.existsSync(p)) {
11087
+ return JSON.parse(fs4.readFileSync(p, "utf8")).version;
11049
11088
  }
11050
11089
  }
11051
11090
  return "0.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prq-cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "PR Queue — see what code reviews need your attention",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,6 @@
37
37
  "type": "git",
38
38
  "url": "git+https://github.com/caiopizzol/prq.git"
39
39
  },
40
- "homepage": "https://github.com/caiopizzol/prq",
40
+ "homepage": "https://prq.sh",
41
41
  "bugs": "https://github.com/caiopizzol/prq/issues"
42
42
  }