prq-cli 0.4.0 → 0.6.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
@@ -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. Accepts a PR number, `org/repo#number`, or a full GitHub URL.
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 init`
86
+ ### `prq run <action> <identifier>`
75
87
 
76
- Creates a `.prqrc.json` config file in the current directory.
88
+ Run a custom action defined in your config.
77
89
 
78
90
  ```bash
79
- prq init
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
@@ -6478,6 +6478,122 @@ Available actions: ${available}`);
6478
6478
  await executeCommand(command);
6479
6479
  }
6480
6480
 
6481
+ // src/commands/skill.ts
6482
+ var SKILL_CONTENT = `---
6483
+ name: prq
6484
+ 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
+ ---
6486
+
6487
+ # PRQ — PR Review Queue
6488
+
6489
+ You have access to the \`prq\` CLI tool installed on this machine. Use it to help the user manage their code review queue.
6490
+
6491
+ ## Step 1: Get the queue
6492
+
6493
+ Run this command to get the user's current review queue:
6494
+
6495
+ \`\`\`bash
6496
+ prq status --json
6497
+ \`\`\`
6498
+
6499
+ This returns a JSON object with categorized PRs:
6500
+ - **needs-re-review** — PRs where the user left a review but new commits were pushed after
6501
+ - **requested** — PRs where the user is a requested reviewer
6502
+ - **stale** — PRs with no activity for N days
6503
+ - **waiting-on-others** — PRs the user authored that are waiting on someone else
6504
+
6505
+ ## Step 2: Present the queue
6506
+
6507
+ Show the results in a clear, scannable format grouped by category. For each PR, show:
6508
+ - The category symbol (◆ needs re-review, ● requested, ○ stale, ◇ waiting)
6509
+ - The repo and PR number
6510
+ - The title
6511
+ - The detail (e.g., "new commits since your review 2d ago")
6512
+
6513
+ Then ask what the user wants to do.
6514
+
6515
+ ## Step 3: Act on PRs
6516
+
6517
+ 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:
6518
+
6519
+ \`\`\`json
6520
+ {
6521
+ "actions": {
6522
+ "review": "/review {url}",
6523
+ "nudge": "shell:prq nudge {number} --yes"
6524
+ }
6525
+ }
6526
+ \`\`\`
6527
+
6528
+ ### Action resolution
6529
+
6530
+ For each action template:
6531
+ - **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\`
6532
+ - **Starts with \`shell:\`** — it's a shell command. Run it with the Bash tool. For example, \`prq nudge 123 --yes\`
6533
+ - **Otherwise** — treat it as a prompt. Send it as a message.
6534
+
6535
+ ### Template variables
6536
+
6537
+ Replace these in the action template:
6538
+ - \`{url}\` — full PR URL
6539
+ - \`{number}\` — PR number
6540
+ - \`{owner}\` — repo owner
6541
+ - \`{repo}\` — repo name
6542
+ - \`{title}\` — PR title
6543
+ - \`{author}\` — PR author
6544
+
6545
+ ### Default actions (if no config found)
6546
+
6547
+ If no actions are configured, use these defaults:
6548
+ - **review** — invoke \`/review {url}\` if the /review skill exists, otherwise run \`prq review {number}\` to open files changed in browser
6549
+ - **nudge** — run \`prq nudge {number} --yes\`
6550
+ - **open** — run \`prq open {number}\`
6551
+
6552
+ ## Step 4: Batch operations
6553
+
6554
+ If the user says things like "review all needs-re-review PRs" or "nudge all stale PRs":
6555
+
6556
+ 1. Filter the queue JSON by the requested category
6557
+ 2. Confirm the list with the user: "I'll review these 3 PRs: #2439, #2380, #2352. Proceed?"
6558
+ 3. Execute the action on each PR sequentially
6559
+
6560
+ ## Examples
6561
+
6562
+ **User:** "check my review queue"
6563
+ → Run \`prq status --json\`, present results, ask what to do
6564
+
6565
+ **User:** "review 2439"
6566
+ → Look up action for "review", interpolate with PR data, execute
6567
+
6568
+ **User:** "nudge all stale PRs"
6569
+ → Filter stale PRs from queue, confirm, run nudge on each
6570
+
6571
+ **User:** "what PRs are waiting on me?"
6572
+ → Run \`prq status --json\`, show only needs-re-review and requested categories
6573
+
6574
+ **User:** "/prq" with no context
6575
+ → Run \`prq status --json\`, present full queue, ask what to do
6576
+ `;
6577
+ function skillCommand(global) {
6578
+ if (global) {
6579
+ const fs2 = __require("node:fs");
6580
+ const path2 = __require("node:path");
6581
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6582
+ const skillDir = path2.join(home, ".claude", "skills", "prq");
6583
+ const skillPath = path2.join(skillDir, "SKILL.md");
6584
+ fs2.mkdirSync(skillDir, { recursive: true });
6585
+ fs2.writeFileSync(skillPath, SKILL_CONTENT);
6586
+ console.log(`Installed to ${skillPath}`);
6587
+ } else {
6588
+ const fs2 = __require("node:fs");
6589
+ const skillDir = ".claude/skills/prq";
6590
+ const skillPath = `${skillDir}/SKILL.md`;
6591
+ fs2.mkdirSync(skillDir, { recursive: true });
6592
+ fs2.writeFileSync(skillPath, SKILL_CONTENT);
6593
+ console.log(`Installed to ${skillPath}`);
6594
+ }
6595
+ }
6596
+
6481
6597
  // src/categorize.ts
6482
6598
  function timeAgo(dateStr) {
6483
6599
  const now = Date.now();
@@ -6583,7 +6699,7 @@ function categorize(reviewedPRs, requestedPRs, authoredPRs, staleDays) {
6583
6699
  return results;
6584
6700
  }
6585
6701
 
6586
- // src/output.ts
6702
+ // src/interactive.ts
6587
6703
  var CATEGORY_CONFIG = {
6588
6704
  "needs-re-review": {
6589
6705
  icon: "◆",
@@ -6604,6 +6720,198 @@ var CATEGORY_ORDER = [
6604
6720
  "stale",
6605
6721
  "waiting-on-others"
6606
6722
  ];
6723
+ function toResolvedPR(pr) {
6724
+ const [owner, repo] = pr.repo.split("/");
6725
+ return {
6726
+ owner,
6727
+ repo,
6728
+ number: pr.number,
6729
+ url: pr.url,
6730
+ title: pr.title,
6731
+ author: pr.author,
6732
+ updatedAt: pr.updatedAt
6733
+ };
6734
+ }
6735
+ function render(result, selectedIndex, message) {
6736
+ process.stdout.write("\x1B[2J\x1B[H");
6737
+ const lines = [];
6738
+ lines.push(source_default.bold(` PRQ Status for ${result.user}`));
6739
+ lines.push(source_default.dim(` ${"─".repeat(50)}`));
6740
+ const grouped = new Map;
6741
+ for (const pr of result.prs) {
6742
+ const list = grouped.get(pr.category) ?? [];
6743
+ list.push(pr);
6744
+ grouped.set(pr.category, list);
6745
+ }
6746
+ let flatIndex = 0;
6747
+ for (const category of CATEGORY_ORDER) {
6748
+ const prs = grouped.get(category);
6749
+ if (!prs || prs.length === 0)
6750
+ continue;
6751
+ const config = CATEGORY_CONFIG[category];
6752
+ lines.push("");
6753
+ lines.push(` ${config.color(`${config.icon} ${config.label}`)} ${source_default.dim(`(${prs.length})`)}`);
6754
+ for (const pr of prs) {
6755
+ const isSelected = flatIndex === selectedIndex;
6756
+ const arrow = isSelected ? source_default.yellow("›") : " ";
6757
+ const draft = pr.isDraft ? source_default.dim(" [draft]") : "";
6758
+ const maxTitle = 50;
6759
+ const title = pr.title.length > maxTitle ? `${pr.title.slice(0, maxTitle - 3)}...` : pr.title;
6760
+ if (isSelected) {
6761
+ const ref = source_default.white(`#${pr.number}`);
6762
+ lines.push(` ${arrow} ${ref} ${source_default.white(title)}${draft}`);
6763
+ lines.push(` ${source_default.dim("↳")} ${source_default.dim(pr.detail)}`);
6764
+ } else {
6765
+ const ref = source_default.dim(`#${pr.number}`);
6766
+ lines.push(` ${arrow} ${ref} ${source_default.dim(title)}${draft}`);
6767
+ lines.push(` ${source_default.dim("↳")} ${source_default.dim(pr.detail)}`);
6768
+ }
6769
+ flatIndex++;
6770
+ }
6771
+ }
6772
+ if (result.prs.length === 0) {
6773
+ lines.push("");
6774
+ lines.push(source_default.green(" All clear! No PRs need your attention."));
6775
+ }
6776
+ lines.push("");
6777
+ 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 (message) {
6781
+ lines.push("");
6782
+ lines.push(` ${message}`);
6783
+ }
6784
+ process.stdout.write(lines.join(`
6785
+ `));
6786
+ }
6787
+ async function interactiveMode(result, config) {
6788
+ if (result.prs.length === 0) {
6789
+ console.log(source_default.green(`
6790
+ All clear! No PRs need your attention.
6791
+ `));
6792
+ return;
6793
+ }
6794
+ let selectedIndex = 0;
6795
+ let message = "";
6796
+ const total = result.prs.length;
6797
+ if (!process.stdin.isTTY) {
6798
+ process.stdout.write(`Interactive mode requires a terminal. Use prq status instead.
6799
+ `);
6800
+ return;
6801
+ }
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
+ 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];
6816
+ switch (key) {
6817
+ case "q":
6818
+ case "\x03":
6819
+ cleanup();
6820
+ resolve();
6821
+ return;
6822
+ case "\x1B[A":
6823
+ selectedIndex = Math.max(0, selectedIndex - 1);
6824
+ message = "";
6825
+ break;
6826
+ case "\x1B[B":
6827
+ selectedIndex = Math.min(total - 1, selectedIndex + 1);
6828
+ message = "";
6829
+ break;
6830
+ case "o": {
6831
+ const template = getAction("open", config);
6832
+ 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
+ }
6842
+ }
6843
+ break;
6844
+ }
6845
+ case "r": {
6846
+ const template = getAction("review", config);
6847
+ 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
+ }
6857
+ }
6858
+ break;
6859
+ }
6860
+ case "n": {
6861
+ const template = getAction("nudge", config);
6862
+ 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
+ }
6872
+ }
6873
+ break;
6874
+ }
6875
+ case "c": {
6876
+ const url = pr.url;
6877
+ try {
6878
+ const proc = process.platform === "darwin" ? "pbcopy" : process.platform === "linux" ? "xclip -selection clipboard" : "clip";
6879
+ await executeCommand(`echo "${url}" | ${proc}`);
6880
+ message = source_default.green("url copied");
6881
+ } catch {
6882
+ message = source_default.dim(url);
6883
+ }
6884
+ break;
6885
+ }
6886
+ default:
6887
+ break;
6888
+ }
6889
+ render(result, selectedIndex, message);
6890
+ });
6891
+ });
6892
+ }
6893
+
6894
+ // src/output.ts
6895
+ var CATEGORY_CONFIG2 = {
6896
+ "needs-re-review": {
6897
+ icon: "◆",
6898
+ label: "Needs Re-review",
6899
+ color: source_default.yellow
6900
+ },
6901
+ requested: { icon: "●", label: "Requested Reviews", color: source_default.green },
6902
+ stale: { icon: "○", label: "Stale", color: source_default.red },
6903
+ "waiting-on-others": {
6904
+ icon: "◇",
6905
+ label: "Your PRs Waiting",
6906
+ color: source_default.dim
6907
+ }
6908
+ };
6909
+ var CATEGORY_ORDER2 = [
6910
+ "needs-re-review",
6911
+ "requested",
6912
+ "stale",
6913
+ "waiting-on-others"
6914
+ ];
6607
6915
  function formatPR(pr) {
6608
6916
  const draft = pr.isDraft ? source_default.dim(" [draft]") : "";
6609
6917
  const prRef = source_default.cyan(`${pr.repo}#${pr.number}`);
@@ -6627,12 +6935,12 @@ function formatStatus(result) {
6627
6935
  grouped.set(pr.category, list);
6628
6936
  }
6629
6937
  let hasContent = false;
6630
- for (const category of CATEGORY_ORDER) {
6938
+ for (const category of CATEGORY_ORDER2) {
6631
6939
  const prs = grouped.get(category);
6632
6940
  if (!prs || prs.length === 0)
6633
6941
  continue;
6634
6942
  hasContent = true;
6635
- const config = CATEGORY_CONFIG[category];
6943
+ const config = CATEGORY_CONFIG2[category];
6636
6944
  lines.push("");
6637
6945
  lines.push(` ${config.color(`${config.icon} ${config.label}`)} ${source_default.dim(`(${prs.length})`)}`);
6638
6946
  for (const pr of prs) {
@@ -6656,7 +6964,7 @@ function formatStatus(result) {
6656
6964
  }
6657
6965
 
6658
6966
  // src/commands/status.ts
6659
- async function statusCommand(config, json) {
6967
+ async function statusCommand(config, json, interactive) {
6660
6968
  const user = config.user ?? await getAuthenticatedUser();
6661
6969
  process.stderr.write(`Fetching PRs for ${user}...
6662
6970
  `);
@@ -6677,6 +6985,8 @@ async function statusCommand(config, json) {
6677
6985
  if (json) {
6678
6986
  process.stdout.write(`${JSON.stringify(result, null, 2)}
6679
6987
  `);
6988
+ } else if (interactive && process.stdin.isTTY) {
6989
+ await interactiveMode(result, config);
6680
6990
  } else {
6681
6991
  process.stdout.write(formatStatus(result));
6682
6992
  }
@@ -10699,12 +11009,12 @@ function getVersion() {
10699
11009
  function createCLI() {
10700
11010
  const program2 = new Command;
10701
11011
  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) => {
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) => {
10703
11013
  const config = loadConfig({
10704
11014
  repos: opts.repos,
10705
11015
  staleDays: opts.staleDays ? parseInt(opts.staleDays, 10) : undefined
10706
11016
  });
10707
- await statusCommand(config, opts.json ?? false);
11017
+ await statusCommand(config, opts.json ?? false, opts.interactive ?? false);
10708
11018
  });
10709
11019
  program2.command("open <identifier>").description("Open a PR in the browser").action(async (identifier) => {
10710
11020
  const config = loadConfig({});
@@ -10725,6 +11035,9 @@ function createCLI() {
10725
11035
  const config = loadConfig({});
10726
11036
  await runCommand(action, identifier, config);
10727
11037
  });
11038
+ program2.command("skill").description("Install the /prq skill for Claude Code").option("-g, --global", "Install globally (~/.claude/skills/prq/)").action((opts) => {
11039
+ skillCommand(opts.global ?? false);
11040
+ });
10728
11041
  program2.command("init").description("Create config file interactively").action(async () => {
10729
11042
  await initCommand();
10730
11043
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prq-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "PR Queue — see what code reviews need your attention",
5
5
  "type": "module",
6
6
  "bin": {