prq-cli 0.4.0 → 0.5.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
@@ -6583,7 +6583,7 @@ function categorize(reviewedPRs, requestedPRs, authoredPRs, staleDays) {
6583
6583
  return results;
6584
6584
  }
6585
6585
 
6586
- // src/output.ts
6586
+ // src/interactive.ts
6587
6587
  var CATEGORY_CONFIG = {
6588
6588
  "needs-re-review": {
6589
6589
  icon: "◆",
@@ -6604,6 +6604,198 @@ var CATEGORY_ORDER = [
6604
6604
  "stale",
6605
6605
  "waiting-on-others"
6606
6606
  ];
6607
+ function toResolvedPR(pr) {
6608
+ const [owner, repo] = pr.repo.split("/");
6609
+ return {
6610
+ owner,
6611
+ repo,
6612
+ number: pr.number,
6613
+ url: pr.url,
6614
+ title: pr.title,
6615
+ author: pr.author,
6616
+ updatedAt: pr.updatedAt
6617
+ };
6618
+ }
6619
+ function render(result, selectedIndex, message) {
6620
+ process.stdout.write("\x1B[2J\x1B[H");
6621
+ const lines = [];
6622
+ lines.push(source_default.bold(` PRQ Status for ${result.user}`));
6623
+ lines.push(source_default.dim(` ${"─".repeat(50)}`));
6624
+ const grouped = new Map;
6625
+ for (const pr of result.prs) {
6626
+ const list = grouped.get(pr.category) ?? [];
6627
+ list.push(pr);
6628
+ grouped.set(pr.category, list);
6629
+ }
6630
+ let flatIndex = 0;
6631
+ for (const category of CATEGORY_ORDER) {
6632
+ const prs = grouped.get(category);
6633
+ if (!prs || prs.length === 0)
6634
+ continue;
6635
+ const config = CATEGORY_CONFIG[category];
6636
+ lines.push("");
6637
+ lines.push(` ${config.color(`${config.icon} ${config.label}`)} ${source_default.dim(`(${prs.length})`)}`);
6638
+ for (const pr of prs) {
6639
+ const isSelected = flatIndex === selectedIndex;
6640
+ const arrow = isSelected ? source_default.yellow("›") : " ";
6641
+ const draft = pr.isDraft ? source_default.dim(" [draft]") : "";
6642
+ const maxTitle = 50;
6643
+ const title = pr.title.length > maxTitle ? `${pr.title.slice(0, maxTitle - 3)}...` : pr.title;
6644
+ if (isSelected) {
6645
+ const ref = source_default.white(`#${pr.number}`);
6646
+ lines.push(` ${arrow} ${ref} ${source_default.white(title)}${draft}`);
6647
+ lines.push(` ${source_default.dim("↳")} ${source_default.dim(pr.detail)}`);
6648
+ } else {
6649
+ const ref = source_default.dim(`#${pr.number}`);
6650
+ lines.push(` ${arrow} ${ref} ${source_default.dim(title)}${draft}`);
6651
+ lines.push(` ${source_default.dim("↳")} ${source_default.dim(pr.detail)}`);
6652
+ }
6653
+ flatIndex++;
6654
+ }
6655
+ }
6656
+ if (result.prs.length === 0) {
6657
+ lines.push("");
6658
+ lines.push(source_default.green(" All clear! No PRs need your attention."));
6659
+ }
6660
+ lines.push("");
6661
+ lines.push(source_default.dim(` ${"─".repeat(50)}`));
6662
+ lines.push("");
6663
+ 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`);
6664
+ if (message) {
6665
+ lines.push("");
6666
+ lines.push(` ${message}`);
6667
+ }
6668
+ process.stdout.write(lines.join(`
6669
+ `));
6670
+ }
6671
+ async function interactiveMode(result, config) {
6672
+ if (result.prs.length === 0) {
6673
+ console.log(source_default.green(`
6674
+ All clear! No PRs need your attention.
6675
+ `));
6676
+ return;
6677
+ }
6678
+ let selectedIndex = 0;
6679
+ let message = "";
6680
+ const total = result.prs.length;
6681
+ if (!process.stdin.isTTY) {
6682
+ process.stdout.write(`Interactive mode requires a terminal. Use prq status instead.
6683
+ `);
6684
+ return;
6685
+ }
6686
+ process.stdin.setRawMode(true);
6687
+ process.stdin.resume();
6688
+ process.stdin.setEncoding("utf8");
6689
+ process.stdout.write("\x1B[?25l");
6690
+ render(result, selectedIndex, message);
6691
+ return new Promise((resolve) => {
6692
+ const cleanup = () => {
6693
+ process.stdin.setRawMode(false);
6694
+ process.stdin.pause();
6695
+ process.stdin.removeAllListeners("data");
6696
+ process.stdout.write("\x1B[?25h\x1B[2J\x1B[H");
6697
+ };
6698
+ process.stdin.on("data", async (key) => {
6699
+ const pr = result.prs[selectedIndex];
6700
+ switch (key) {
6701
+ case "q":
6702
+ case "\x03":
6703
+ cleanup();
6704
+ resolve();
6705
+ return;
6706
+ case "\x1B[A":
6707
+ selectedIndex = Math.max(0, selectedIndex - 1);
6708
+ message = "";
6709
+ break;
6710
+ case "\x1B[B":
6711
+ selectedIndex = Math.min(total - 1, selectedIndex + 1);
6712
+ message = "";
6713
+ break;
6714
+ case "o": {
6715
+ const template = getAction("open", config);
6716
+ if (template) {
6717
+ const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
6718
+ message = source_default.dim(`opening ${pr.repo}#${pr.number}...`);
6719
+ render(result, selectedIndex, message);
6720
+ try {
6721
+ await executeCommand(cmd);
6722
+ message = source_default.green(`opened ${pr.repo}#${pr.number}`);
6723
+ } catch {
6724
+ message = source_default.red("failed to open");
6725
+ }
6726
+ }
6727
+ break;
6728
+ }
6729
+ case "r": {
6730
+ const template = getAction("review", config);
6731
+ if (template) {
6732
+ const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
6733
+ message = source_default.dim(`opening review for ${pr.repo}#${pr.number}...`);
6734
+ render(result, selectedIndex, message);
6735
+ try {
6736
+ await executeCommand(cmd);
6737
+ message = source_default.green(`opened review for ${pr.repo}#${pr.number}`);
6738
+ } catch {
6739
+ message = source_default.red("failed to open review");
6740
+ }
6741
+ }
6742
+ break;
6743
+ }
6744
+ case "n": {
6745
+ const template = getAction("nudge", config);
6746
+ if (template) {
6747
+ const cmd = interpolate(template, buildContext(toResolvedPR(pr)));
6748
+ message = source_default.dim(`nudging ${pr.repo}#${pr.number}...`);
6749
+ render(result, selectedIndex, message);
6750
+ try {
6751
+ await executeCommand(cmd);
6752
+ message = source_default.green(`nudged ${pr.repo}#${pr.number}`);
6753
+ } catch {
6754
+ message = source_default.red("failed to nudge");
6755
+ }
6756
+ }
6757
+ break;
6758
+ }
6759
+ case "c": {
6760
+ const url = pr.url;
6761
+ try {
6762
+ const proc = process.platform === "darwin" ? "pbcopy" : process.platform === "linux" ? "xclip -selection clipboard" : "clip";
6763
+ await executeCommand(`echo "${url}" | ${proc}`);
6764
+ message = source_default.green("url copied");
6765
+ } catch {
6766
+ message = source_default.dim(url);
6767
+ }
6768
+ break;
6769
+ }
6770
+ default:
6771
+ break;
6772
+ }
6773
+ render(result, selectedIndex, message);
6774
+ });
6775
+ });
6776
+ }
6777
+
6778
+ // src/output.ts
6779
+ var CATEGORY_CONFIG2 = {
6780
+ "needs-re-review": {
6781
+ icon: "◆",
6782
+ label: "Needs Re-review",
6783
+ color: source_default.yellow
6784
+ },
6785
+ requested: { icon: "●", label: "Requested Reviews", color: source_default.green },
6786
+ stale: { icon: "○", label: "Stale", color: source_default.red },
6787
+ "waiting-on-others": {
6788
+ icon: "◇",
6789
+ label: "Your PRs Waiting",
6790
+ color: source_default.dim
6791
+ }
6792
+ };
6793
+ var CATEGORY_ORDER2 = [
6794
+ "needs-re-review",
6795
+ "requested",
6796
+ "stale",
6797
+ "waiting-on-others"
6798
+ ];
6607
6799
  function formatPR(pr) {
6608
6800
  const draft = pr.isDraft ? source_default.dim(" [draft]") : "";
6609
6801
  const prRef = source_default.cyan(`${pr.repo}#${pr.number}`);
@@ -6627,12 +6819,12 @@ function formatStatus(result) {
6627
6819
  grouped.set(pr.category, list);
6628
6820
  }
6629
6821
  let hasContent = false;
6630
- for (const category of CATEGORY_ORDER) {
6822
+ for (const category of CATEGORY_ORDER2) {
6631
6823
  const prs = grouped.get(category);
6632
6824
  if (!prs || prs.length === 0)
6633
6825
  continue;
6634
6826
  hasContent = true;
6635
- const config = CATEGORY_CONFIG[category];
6827
+ const config = CATEGORY_CONFIG2[category];
6636
6828
  lines.push("");
6637
6829
  lines.push(` ${config.color(`${config.icon} ${config.label}`)} ${source_default.dim(`(${prs.length})`)}`);
6638
6830
  for (const pr of prs) {
@@ -6656,7 +6848,7 @@ function formatStatus(result) {
6656
6848
  }
6657
6849
 
6658
6850
  // src/commands/status.ts
6659
- async function statusCommand(config, json) {
6851
+ async function statusCommand(config, json, interactive) {
6660
6852
  const user = config.user ?? await getAuthenticatedUser();
6661
6853
  process.stderr.write(`Fetching PRs for ${user}...
6662
6854
  `);
@@ -6677,6 +6869,8 @@ async function statusCommand(config, json) {
6677
6869
  if (json) {
6678
6870
  process.stdout.write(`${JSON.stringify(result, null, 2)}
6679
6871
  `);
6872
+ } else if (interactive && process.stdin.isTTY) {
6873
+ await interactiveMode(result, config);
6680
6874
  } else {
6681
6875
  process.stdout.write(formatStatus(result));
6682
6876
  }
@@ -10699,12 +10893,12 @@ function getVersion() {
10699
10893
  function createCLI() {
10700
10894
  const program2 = new Command;
10701
10895
  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) => {
10896
+ 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
10897
  const config = loadConfig({
10704
10898
  repos: opts.repos,
10705
10899
  staleDays: opts.staleDays ? parseInt(opts.staleDays, 10) : undefined
10706
10900
  });
10707
- await statusCommand(config, opts.json ?? false);
10901
+ await statusCommand(config, opts.json ?? false, opts.interactive ?? false);
10708
10902
  });
10709
10903
  program2.command("open <identifier>").description("Open a PR in the browser").action(async (identifier) => {
10710
10904
  const config = loadConfig({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prq-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "PR Queue — see what code reviews need your attention",
5
5
  "type": "module",
6
6
  "bin": {