@nvice-r/pr-commands 0.1.0 → 0.1.2

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
@@ -27,7 +27,10 @@ The interactive wizard:
27
27
  template-sections (each pre-filled from the preset, editable). A placeholder legend
28
28
  (`<ver>`, `<desc>`, `<ticket>`, …) is shown. Concrete values are written to config — `/pr`
29
29
  reads those, not a hard-coded table.
30
- 7. **Install** — writes config + roster, drops the binaries and the `/pr` skill.
30
+ 7. **Agents** — checklist of supported agents (claude-code / antigravity / codex / opencode) with
31
+ installed ones pre-checked; toggle which get the `pr` skill. (`--agent` skips this; `--yes` = detected.)
32
+ 8. **Install** — writes config + roster + binaries, then installs the portable `pr` skill into
33
+ the chosen agents (symlinked to one canonical copy) + Claude's `/pr` command.
31
34
 
32
35
  Every prompt is reversible: in a list press `b` to step back; in a text field, append `:b` to
33
36
  the value and press enter (the field is pre-filled, so `:b` at the end triggers back). Back
@@ -39,6 +42,7 @@ Flags:
39
42
  |------|--------|
40
43
  | `--dir <path>` | repo to configure (default: cwd) |
41
44
  | `--provider <p>` | force `bitbucket`\|`github`\|`gitlab` instead of detecting |
45
+ | `--agent <id>` | force skill install to this agent (repeatable): `claude-code`\|`antigravity`\|`codex`\|`opencode`. Default: auto-detect |
42
46
  | `--all` | install every ready provider binary, not just the detected one |
43
47
  | `--yes` | non-interactive: reviewers = all, convention = `trueid` |
44
48
  | `--dry-run` | print planned actions, write nothing |
@@ -65,16 +69,23 @@ npx @nvice-r/pr-commands --dry-run --dir /path/to/repo
65
69
 
66
70
  ## What gets installed
67
71
 
68
- Everything is **global** — nothing lands in a repo's `.claude/`, so it works identically from
69
- every clone and every git worktree, no symlinks.
72
+ Everything user-global is **global** — nothing lands in a repo's agent dir, so it works
73
+ identically from every clone and every git worktree, no repo-local symlinks.
70
74
 
71
75
  | Artifact | Path |
72
76
  |----------|------|
73
77
  | Shared library | `~/.local/bin/pr-common.sh` |
74
78
  | Provider binary | `~/.local/bin/bbpr` (`ghpr`/`glpr` later) |
75
79
  | Config | `~/.config/pr/config` |
76
- | Reviewer roster | `<main-repo-root>/.pr-reviewers.txt` (shared across worktrees) |
77
- | `/pr` skill | `~/.claude/commands/pr.md` |
80
+ | Reviewer roster | `<main-repo-root>/.pr/reviewers.txt` (neutral, agent-agnostic; shared across worktrees) |
81
+ | Canonical skill | `~/.config/pr/skill/pr/SKILL.md` (single source) |
82
+ | Per-agent skill | `<agent-skills-dir>/pr` → symlink to canonical |
83
+ | Claude `/pr` command | `~/.claude/commands/pr.md` (dual-install, keeps the slash trigger) |
84
+
85
+ The skill is a **portable `SKILL.md`** installed into every detected agent — **claude-code,
86
+ antigravity, codex, opencode** — via one canonical copy each agent symlinks to (`npx skills`
87
+ single-source model). Force a target with `--agent <id>`. A pre-existing non-symlink `pr` skill
88
+ dir is never overwritten (the installer skips and warns).
78
89
 
79
90
  ## Usage
80
91
 
@@ -124,11 +135,12 @@ template_sections=Context|Changes|Risk
124
135
 
125
136
  ### Reviewers
126
137
 
127
- Roster lives **in the work dir** at the main repo root, so it's editable next to the code.
128
- Resolved via `git --git-common-dir/..`, so all worktrees of a repo share one roster:
138
+ Roster lives in a **neutral `.pr/` folder** under the main repo root — it's data the bash binary
139
+ reads, not an agent artifact, so it's agent-agnostic (survives switching agents). Editable next to
140
+ the code. Resolved via `git --git-common-dir/..`, so all worktrees of a repo share one roster:
129
141
 
130
142
  ```
131
- <main-repo-root>/.pr-reviewers.txt
143
+ <main-repo-root>/.pr/reviewers.txt
132
144
  ```
133
145
 
134
146
  You (the author) are excluded on install — bbpr also drops self at PR time. One entry per line,
@@ -139,10 +151,10 @@ trailing `# Name` comment optional:
139
151
  {fedcba98-7654-3210-fedc-ba9876543210} # Bob
140
152
  ```
141
153
 
142
- It's plain data — edit freely, no reinstall. Consider gitignoring it unless the team wants the
143
- roster committed. Load order: `$BBPR_REVIEWERS` → work-dir file → global
144
- `~/.config/pr/reviewers/<ws>-<slug>.txt` → legacy. Run-time overrides: `--no-reviewers` skips
145
- all, `--reviewer <uuid>` appends.
154
+ It's plain data — edit freely, no reinstall. Consider gitignoring `.pr/` unless the team wants the
155
+ roster committed. Load order: `$BBPR_REVIEWERS` → `.pr/reviewers.txt` → `<agent_dir>/pr-reviewers.txt`
156
+ (0.1.1) → root `.pr-reviewers.txt` (pre-0.1.1) → global `~/.config/pr/reviewers/<ws>-<slug>.txt` →
157
+ legacy. Run-time overrides: `--no-reviewers` skips all, `--reviewer <uuid>` appends.
146
158
 
147
159
  ### Auth precedence (email)
148
160
 
@@ -184,7 +196,8 @@ Full rationale in [`DECISIONS.md`](./DECISIONS.md).
184
196
  | reviewers not fetched | add the keychain token (see Prerequisites), re-run |
185
197
  | `WARN ~/.local/bin not on PATH` | add the export line above to your shell rc |
186
198
  | `not-yet-supported: <provider>` | GitHub/GitLab aren't implemented yet |
187
- | worktree runs old behavior | nothing is repo-local here; ensure `pr.md` is at `~/.claude/commands/pr.md`, not a stale repo copy |
199
+ | worktree runs old behavior | skill/binaries are global, not repo-local; ensure no stale per-repo copy shadows them |
200
+ | agent doesn't see the skill | re-run and force it: `npx @nvice-r/pr-commands --agent <id>`; check `<agent-skills-dir>/pr` symlink exists |
188
201
 
189
202
  ## Uninstall
190
203
 
@@ -192,16 +205,18 @@ Full rationale in [`DECISIONS.md`](./DECISIONS.md).
192
205
  rm ~/.local/bin/bbpr ~/.local/bin/pr-common.sh
193
206
  rm -rf ~/.config/pr
194
207
  rm ~/.claude/commands/pr.md
208
+ # per-agent skill symlinks (remove those you installed):
209
+ rm ~/.claude/skills/pr ~/.gemini/antigravity/skills/pr ~/.codex/skills/pr ~/.config/opencode/skills/pr
195
210
  # roster lives in each repo — remove per repo if wanted:
196
- rm <main-repo-root>/.pr-reviewers.txt
211
+ rm -rf <main-repo-root>/.pr
197
212
  ```
198
213
 
199
214
  ## Development
200
215
 
201
216
  ```
202
- install.js npx entry (wizard)
203
- assets/bin/* provider binaries + shared lib (source of truth; copied on install)
204
- assets/skill/pr.md generic /pr skill (source of truth; copied on install)
217
+ install.js npx entry (wizard)
218
+ assets/bin/* provider binaries + shared lib (source of truth; copied on install)
219
+ assets/skill/pr.body.md shared /pr skill body (frontmatter added per-target on install)
205
220
  ```
206
221
 
207
222
  `assets/` is the single source of truth — both the installer (`install.js`)
@@ -20,11 +20,11 @@ CONFIG_FILE="${PR_CONFIG:-$HOME/.config/pr/config}"
20
20
  die() { echo "${PR_CMD:-pr}: $*" >&2; exit 1; }
21
21
 
22
22
  # --- config load (safe parse, no `source` of untrusted file) ---
23
- # Sets: CFG_OS CFG_TOKEN_SRC CFG_EMAIL CFG_CONVENTION (+ optional custom_* keys)
23
+ # Sets: CFG_OS CFG_TOKEN_SRC CFG_EMAIL CFG_CONVENTION CFG_AGENT_DIR (+ optional custom_* keys)
24
24
  pr_load_config() {
25
25
  [ -f "$CONFIG_FILE" ] || die "no $CONFIG_FILE — run: npx @nvice-r/pr-commands"
26
26
  local k v
27
- CFG_OS=""; CFG_TOKEN_SRC=""; CFG_EMAIL=""; CFG_CONVENTION=""
27
+ CFG_OS=""; CFG_TOKEN_SRC=""; CFG_EMAIL=""; CFG_CONVENTION=""; CFG_AGENT_DIR=""
28
28
  while IFS='=' read -r k v; do
29
29
  k="${k%%#*}"; k="${k//[[:space:]]/}" # strip comment + whitespace from key
30
30
  [ -z "$k" ] && continue
@@ -34,9 +34,11 @@ pr_load_config() {
34
34
  token_src) CFG_TOKEN_SRC="$v" ;;
35
35
  email) CFG_EMAIL="$v" ;;
36
36
  convention) CFG_CONVENTION="$v" ;;
37
+ agent_dir) CFG_AGENT_DIR="$v" ;;
37
38
  esac
38
39
  done < "$CONFIG_FILE"
39
40
  : "${CFG_TOKEN_SRC:=keychain}"
41
+ : "${CFG_AGENT_DIR:=.claude}" # agent folder holding the roster (relative to main repo root)
40
42
  }
41
43
 
42
44
  # --- token retrieval, OS-switched (D1: OS is a function, not a binary axis) ---
@@ -127,9 +129,11 @@ pr_load_reviewers() {
127
129
  # main repo root, shared across worktrees: git-common-dir/.. (empty if git call fails)
128
130
  local mainroot; mainroot="$(cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)"
129
131
  for cand in \
132
+ ${mainroot:+"$mainroot/.pr/reviewers.txt"} \
133
+ ${mainroot:+"$mainroot/${CFG_AGENT_DIR:-.claude}/pr-reviewers.txt"} \
130
134
  ${mainroot:+"$mainroot/.pr-reviewers.txt"} \
131
135
  "$HOME/.config/pr/reviewers/${slug}.txt" \
132
- "$HOME/.config/bbpr/bbpr-reviewers.txt"; do # legacy fallback
136
+ "$HOME/.config/bbpr/bbpr-reviewers.txt"; do # neutral primary; agent-dir + root + global legacy fallbacks
133
137
  [ -f "$cand" ] && { file="$cand"; break; }
134
138
  done
135
139
  fi
@@ -1,9 +1,3 @@
1
- ---
2
- description: Create a pull request for the current branch (provider auto-detected)
3
- argument-hint: [base-branch] [draft]
4
- model: sonnet
5
- ---
6
-
7
1
  Create a pull request for the current branch. Provider (Bitbucket / GitHub / GitLab) is
8
2
  auto-detected from the git remote; the matching binary (`bbpr` / `ghpr` / `glpr`) is invoked.
9
3
  Convention (title pattern, ticket regex, PR template) comes from `~/.config/pr/config`.
@@ -13,6 +7,9 @@ user to run the installer — it never guesses config (D8: hard fail, no hardcod
13
7
 
14
8
  If `$ARGUMENTS` starts with `help`/`-help`/`--help` (case-insensitive), print the **Help** block and stop.
15
9
 
10
+ If `$ARGUMENTS` starts with `update` (case-insensitive), run **Update mode** (below) and stop —
11
+ skip the create steps entirely.
12
+
16
13
  ## Help (`/pr help`)
17
14
 
18
15
  ```
@@ -20,6 +17,7 @@ If `$ARGUMENTS` starts with `help`/`-help`/`--help` (case-insensitive), print th
20
17
 
21
18
  USAGE
22
19
  /pr [base-branch] [draft] Create a PR for the current branch (default).
20
+ /pr update [PR#] [short] Rewrite an existing PR's description in place.
23
21
  /pr help Show this help.
24
22
 
25
23
  CREATE
@@ -27,9 +25,15 @@ CREATE
27
25
  remote branch (saved per-branch after first run).
28
26
  draft Create as draft. Otherwise opens ready (asks to confirm).
29
27
 
28
+ UPDATE
29
+ PR# PR number to rewrite. If omitted, resolved from the current branch's
30
+ open PR. Re-drafts the body per convention, then PATCHes it in place.
31
+ short Compress harder (caveman brevity): cut throat-clearing, restated ticket
32
+ goals, filler connectors. Keeps every file + technical fact.
33
+
30
34
  CONFIG ~/.config/pr/config (os, token_src, email, convention)
31
35
  PROVIDER auto from `git remote get-url origin` -> bbpr | ghpr | glpr
32
- REVIEWERS ~/.config/pr/reviewers/<workspace>-<slug>.txt (per-repo, global location)
36
+ REVIEWERS <main-repo-root>/.pr/reviewers.txt (per-repo, shared across worktrees, agent-agnostic)
33
37
  SETUP npx @nvice-r/pr-commands
34
38
  ```
35
39
 
@@ -138,14 +142,74 @@ per-repo reviewer roster automatically; `--no-reviewers` skips, `--reviewer <id>
138
142
  Reuse ticket key from Step 4. If none, skip. `guard_cmd jira-update`; if absent, just print URL + stop.
139
143
  If present, `AskUserQuestion`: sync now → `/jira-update <KEY>`, or skip (print reminder).
140
144
 
145
+ ## Update mode (`/pr update [PR#] [short]`)
146
+
147
+ Rewrites an existing open PR's **description** in place — no new branch, no new PR. Same Step 0
148
+ install check runs first (config + provider + creds). Provider PATCH is currently Bitbucket-only,
149
+ matching the create path; `ghpr`/`glpr` providers → STOP with the same "not yet supported" note.
150
+
151
+ **U1 — parse args.** Tokens after `update`: a bare integer = `PR#`; the word `short` (or
152
+ `caveman`) = compress-hard flag. Missing `PR#` → resolve from the current branch: query the
153
+ provider for the open PR whose source branch is `HEAD` (Bitbucket: `GET /pullrequests?q=source.branch.name="<CURR>"&state=OPEN`). Zero or multiple matches → ask.
154
+
155
+ **U2 — fetch current PR.** Resolve creds the same way the binary does — email from `$CFG`
156
+ (`email=`), token from the OS keychain (macOS service `bitbucket-api-token`, account = email).
157
+ `REPO` = workspace/slug from `git remote get-url origin`. Fetch title + description:
158
+ ```bash
159
+ curl -sS -u "$EMAIL:$TOKEN" \
160
+ "https://api.bitbucket.org/2.0/repositories/$REPO/pullrequests/$PRNUM"
161
+ ```
162
+
163
+ **U3 — re-draft the body.** Rebuild the description with the **Step 5** rules (trueid template
164
+ when `convention=trueid`): `What Happened` prose, `Insight` file detail, `Proof of Work`. Prefer
165
+ the diff/commits as the source of truth over the old text; carry forward Proof-of-Work links the
166
+ old body already had. Default output = terse, anti-slop normal English:
167
+ - Cut throat-clearing openers ("The X feature needs to…", "This adds…"), restated ticket goals,
168
+ and filler connectors ("as a consequence", "in order to").
169
+ - Keep every changed file, every behavioural fact, and any bug-fix detail — brevity, not omission.
170
+
171
+ With the `short` flag, compress harder toward caveman brevity (fragments, dropped articles) while
172
+ keeping the section headings and every technical fact. Title stays unchanged unless the user asks.
173
+
174
+ **U4 — confirm (diff-style).** Write the new body to `/tmp/pr-body.md`. Show a short before/after
175
+ summary (what was cut) + the full new body. WAIT — editing a live PR is user-visible; never silent.
176
+
177
+ **U5 — PATCH in place** (Bitbucket `PUT`, description only):
178
+ ```bash
179
+ python3 - "$REPO" "$PRNUM" "$EMAIL" "$TOKEN" /tmp/pr-body.md <<'PY'
180
+ import sys, json, base64, urllib.request, urllib.error
181
+ repo, num, email, token, bodyfile = sys.argv[1:6]
182
+ desc = open(bodyfile).read()
183
+ data = json.dumps({"description": desc}).encode()
184
+ url = f"https://api.bitbucket.org/2.0/repositories/{repo}/pullrequests/{num}"
185
+ req = urllib.request.Request(url, data=data, method="PUT", headers={"Content-Type": "application/json"})
186
+ req.add_header("Authorization", "Basic " + base64.b64encode(f"{email}:{token}".encode()).decode())
187
+ try:
188
+ r = urllib.request.urlopen(req)
189
+ print(r.status, json.loads(r.read())["links"]["html"]["href"])
190
+ except urllib.error.HTTPError as e:
191
+ print("ERR", e.code, e.read().decode()[:500])
192
+ PY
193
+ ```
194
+ On non-2xx, surface the raw response body. Reviewers/base/title are untouched — description only.
195
+
196
+ **U6 — print PR URL.** No Jira sync offer on update (status unchanged).
197
+
141
198
  ## Cross-command guard
142
- Sibling commands (`/pr-split`, `/jira-update`) are optional — a teammate may not have them. Before
143
- invoking any, confirm it exists; if absent, degrade gracefully (one-line note). Reusable check:
199
+ Sibling commands (`/pr-split`, `/jira-update`) are optional — a teammate may not have them, and
200
+ they may live under any agent's skills/commands dir. Before invoking any, confirm it exists; if
201
+ absent, degrade gracefully (one-line note). Reusable check (scans Claude command dir + every known
202
+ agent skills dir):
144
203
  ```bash
145
204
  guard_cmd() { # usage: guard_cmd pr-split → prints path or "no-<cmd>"
146
- ls "$PWD/.claude/commands/$1.md" "$HOME/.claude/commands/$1.md" \
147
- "$PWD/.claude/skills/$1/SKILL.md" "$HOME/.claude/skills/$1/SKILL.md" 2>/dev/null | head -1 \
148
- || echo "no-$1"
205
+ local c="$1"
206
+ ls "$PWD/.claude/commands/$c.md" "$HOME/.claude/commands/$c.md" \
207
+ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/$c/SKILL.md" \
208
+ "$HOME/.gemini/antigravity/skills/$c/SKILL.md" \
209
+ "${CODEX_HOME:-$HOME/.codex}/skills/$c/SKILL.md" \
210
+ "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/skills/$c/SKILL.md" \
211
+ "$PWD/.claude/skills/$c/SKILL.md" "$PWD/.agents/skills/$c/SKILL.md" 2>/dev/null | head -1 \
212
+ || echo "no-$c"
149
213
  }
150
214
  ```
151
215
 
@@ -154,4 +218,5 @@ guard_cmd() { # usage: guard_cmd pr-split → prints path or "no-<cmd>"
154
218
  - Provider/token/convention all from config + remote — NO hardcoded team values here (D3/D4).
155
219
  - Verify worktree before any split branch work (`git symbolic-ref --short HEAD` + `git worktree list`);
156
220
  background restacks can move branches mid-turn.
157
- - Caveman mode applies to replies — but PR title/body must be normal English.
221
+ - Caveman mode applies to replies — PR title/body default to normal English. Exception: `/pr update
222
+ … short` (a.k.a. `caveman`) is an explicit user opt-in to compress the body toward caveman brevity.
package/install.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // @nvice-r/pr-commands installer. Zero external deps (Node >=16 builtins only).
3
3
  // Wizard: detect OS + provider, pick reviewers + convention, write config/roster,
4
- // install binaries -> ~/.local/bin, copy /pr skill -> ~/.claude/commands.
4
+ // install binaries -> ~/.local/bin, install the portable `pr` SKILL.md into every
5
+ // detected agent (symlinked to one canonical copy) + Claude /pr slash-command.
5
6
  //
6
- // Flags: --dir <path> --provider <p> --all --yes --dry-run --help
7
+ // Flags: --dir <path> --provider <p> --agent <id> --all --yes --dry-run --help
7
8
 
8
9
  'use strict';
9
10
  const fs = require('fs');
@@ -35,17 +36,51 @@ const PRESET_DEFS = {
35
36
 
36
37
  // ---- arg parse ----
37
38
  const argv = process.argv.slice(2);
38
- const flags = { dir: null, provider: null, all: false, yes: false, dryRun: false };
39
+ const flags = { dir: null, provider: null, all: false, yes: false, dryRun: false, agentDir: null, agents: null };
39
40
  for (let i = 0; i < argv.length; i++) {
40
41
  const a = argv[i];
41
42
  if (a === '--help' || a === '-h') { printHelp(); process.exit(0); }
42
43
  else if (a === '--dir') flags.dir = argv[++i];
43
44
  else if (a === '--provider') flags.provider = argv[++i];
45
+ else if (a === '--agent-dir') flags.agentDir = argv[++i];
46
+ else if (a === '--agent') (flags.agents ||= []).push(argv[++i]);
44
47
  else if (a === '--all') flags.all = true;
45
48
  else if (a === '--yes' || a === '-y') flags.yes = true;
46
49
  else if (a === '--dry-run') flags.dryRun = true;
47
50
  else { console.error(`unknown flag: ${a}`); process.exit(1); }
48
51
  }
52
+ const AGENT_DIR = (flags.agentDir || '.claude').replace(/^\/+|\/+$/g, ''); // strip stray slashes
53
+
54
+ // ---- portable skill ----
55
+ // One canonical SKILL.md (~/.config/pr/skill/pr/); every detected agent's global skills
56
+ // dir gets a symlink `pr` -> that dir (skills-CLI single-source model). Claude additionally
57
+ // gets a /pr slash-command. Frontmatter differs per target; the body is shared (pr.body.md).
58
+ const SKILL_FM = `---
59
+ name: pr
60
+ description: Create a pull request for the current branch, with the title and body drafted in the team's convention and the provider (Bitbucket/GitHub/GitLab) auto-detected from the git remote. Use when the user asks to open, create, raise, or draft a pull request / PR / merge request for the current branch, or types /pr.
61
+ ---
62
+
63
+ `;
64
+ const CMD_FM = `---
65
+ description: Create a pull request for the current branch (provider auto-detected)
66
+ argument-hint: [base-branch] [draft]
67
+ model: sonnet
68
+ ---
69
+
70
+ `;
71
+ // base = probe dir deciding "installed"; skills = GLOBAL skills dir we link into.
72
+ // Env vars resolved the same way the agents themselves resolve them.
73
+ function agentTargets() {
74
+ const claudeBase = process.env.CLAUDE_CONFIG_DIR || path.join(HOME, '.claude');
75
+ const codexBase = process.env.CODEX_HOME || path.join(HOME, '.codex');
76
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(HOME, '.config');
77
+ return [
78
+ { id: 'claude-code', base: claudeBase, skills: path.join(claudeBase, 'skills'), claude: true },
79
+ { id: 'antigravity', base: path.join(HOME, '.gemini/antigravity'), skills: path.join(HOME, '.gemini/antigravity/skills') },
80
+ { id: 'codex', base: codexBase, skills: path.join(codexBase, 'skills') },
81
+ { id: 'opencode', base: path.join(xdg, 'opencode'), skills: path.join(xdg, 'opencode/skills') },
82
+ ];
83
+ }
49
84
 
50
85
  function printHelp() {
51
86
  console.log(`@nvice-r/pr-commands installer
@@ -54,6 +89,9 @@ function printHelp() {
54
89
 
55
90
  --dir <path> Repo to configure (default: cwd)
56
91
  --provider <p> Force bitbucket|github|gitlab (default: detect from remote)
92
+ --agent-dir <dir> (deprecated) Legacy roster folder; roster now lives in <root>/.pr/
93
+ --agent <id> Force skill install to this agent (repeatable). Ids: claude-code,
94
+ antigravity, codex, opencode. Default: auto-detect installed agents.
57
95
  --all Install all provider binaries (default: only the detected one)
58
96
  --yes Accept defaults, no prompts (reviewers=all, convention=trueid)
59
97
  --dry-run Print planned actions, write nothing
@@ -70,8 +108,8 @@ const ask = (q) => new Promise((res) => {
70
108
  });
71
109
  // Editable pre-filled default: edit the shown value or hit enter to keep it.
72
110
  // `:b` (or `:back`) returns BACK; empty keeps the default.
73
- const askPrefill = (q, def, allowBack = true) => new Promise((res) => {
74
- const r = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
111
+ const askPrefill = (q, def, allowBack = true, completer = undefined) => new Promise((res) => {
112
+ const r = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, completer });
75
113
  r.question(q, (a) => {
76
114
  r.close();
77
115
  const t = a.trim();
@@ -81,6 +119,22 @@ const askPrefill = (q, def, allowBack = true) => new Promise((res) => {
81
119
  });
82
120
  if (def && process.stdin.isTTY) r.write(def); // inject editable text
83
121
  });
122
+ // Tab-completion for filesystem paths (directories only — work dir is always a dir).
123
+ // Preserves the user's typed prefix style (~, relative, absolute); appends "/" to each match.
124
+ function pathCompleter(line) {
125
+ const cut = line.lastIndexOf('/');
126
+ const dirPart = cut >= 0 ? line.slice(0, cut + 1) : ''; // keeps trailing slash, e.g. "~/Doc" -> "~/"
127
+ const base = cut >= 0 ? line.slice(cut + 1) : line;
128
+ let realDir = dirPart || '.';
129
+ if (realDir.startsWith('~')) realDir = HOME + realDir.slice(1);
130
+ let ents;
131
+ try { ents = fs.readdirSync(realDir, { withFileTypes: true }); } catch { return [[], line]; }
132
+ const hits = ents
133
+ .filter((e) => e.isDirectory() && e.name.startsWith(base))
134
+ .map((e) => dirPart + e.name + '/')
135
+ .sort();
136
+ return [hits, line];
137
+ }
84
138
 
85
139
  // Interactive checklist (↑/↓ move · space toggle · a all · n none · enter confirm).
86
140
  // Zero-dep raw-mode TTY. Returns array of selected indices. items: [{label}].
@@ -209,6 +263,50 @@ function mainRepoRoot(wd) {
209
263
  const gcd = shSafe('git', ['-C', wd, 'rev-parse', '--git-common-dir']);
210
264
  return gcd ? path.resolve(wd, gcd, '..') : null;
211
265
  }
266
+ // Write the canonical SKILL.md, then link/copy it into each targeted agent. Claude also
267
+ // gets the /pr slash-command. --agent forces a set; otherwise install to every detected
268
+ // agent (claude-code is always included — it's this tool's native home).
269
+ // Agents whose config dir exists (claude-code always included — this tool's native home).
270
+ function detectedAgentIds() {
271
+ return agentTargets().filter((t) => t.id === 'claude-code' || fs.existsSync(t.base)).map((t) => t.id);
272
+ }
273
+ // Install the skill into exactly the agents in `ids`.
274
+ function installSkill(ids) {
275
+ const set = new Set(ids || []);
276
+ const body = fs.readFileSync(path.join(ASSETS, 'skill/pr.body.md'), 'utf8');
277
+ const canonDir = path.join(HOME, '.config/pr/skill/pr');
278
+ writeFile(path.join(canonDir, 'SKILL.md'), SKILL_FM + body);
279
+
280
+ const hits = [];
281
+ for (const t of agentTargets()) {
282
+ if (!set.has(t.id)) continue;
283
+ linkSkill(t, canonDir);
284
+ if (t.claude) writeFile(path.join(t.base, 'commands/pr.md'), CMD_FM + body); // dual: keep /pr
285
+ hits.push(t.id);
286
+ }
287
+ log(` skill targeted: ${hits.join(', ') || '(none)'}`);
288
+ }
289
+ // Symlink <agent>/skills/pr -> canonical dir. Replace only OUR symlink; never clobber a
290
+ // pre-existing real dir/file (could be the user's own `pr` skill). Copy if symlinks fail.
291
+ function linkSkill(t, canonDir) {
292
+ const dst = path.join(t.skills, 'pr');
293
+ if (flags.dryRun) { log(` [dry-run] link ${dst} -> ${canonDir}`); return; }
294
+ fs.mkdirSync(t.skills, { recursive: true });
295
+ let cur = null;
296
+ try { cur = fs.lstatSync(dst); } catch { /* absent */ }
297
+ if (cur) {
298
+ if (cur.isSymbolicLink()) fs.unlinkSync(dst);
299
+ else { log(` SKIP ${dst} — exists and is not our symlink; remove it and re-run`); return; }
300
+ }
301
+ try {
302
+ fs.symlinkSync(canonDir, dst, 'dir');
303
+ log(` linked ${dst} -> ${canonDir}`);
304
+ } catch (e) {
305
+ fs.mkdirSync(dst, { recursive: true });
306
+ fs.copyFileSync(path.join(canonDir, 'SKILL.md'), path.join(dst, 'SKILL.md'));
307
+ log(` copied ${dst}/SKILL.md (symlink unavailable: ${e.code})`);
308
+ }
309
+ }
212
310
  // Depth-limited scan for git repos beneath a non-repo dir. Stops descending once a
213
311
  // repo is found (no nested-repo noise); skips dotdirs + node_modules; caps results.
214
312
  function scanGitRepos(root, maxDepth = 3, cap = 50) {
@@ -313,6 +411,7 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
313
411
  }
314
412
  if (stage > 1 && st.chosen) L.push(` \x1b[32m✓\x1b[0m reviewers ${st.chosen.length} selected`);
315
413
  if (stage > 2 && st.convention) L.push(` \x1b[32m✓\x1b[0m convention ${st.convention}`);
414
+ if (stage > 3 && st.agents) L.push(` \x1b[32m✓\x1b[0m agents ${st.agents.join(', ') || '(none)'}`);
316
415
  return L;
317
416
  };
318
417
  // Full-screen redraw: wipe, reprint header + breadcrumb + active-step marker. No accumulation on back.
@@ -333,7 +432,7 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
333
432
  let wd;
334
433
  if (flags.dir || flags.yes) wd = path.resolve(flags.dir || process.cwd());
335
434
  else {
336
- const a = await askPrefill(' repo path: ', st.workDir || process.cwd());
435
+ const a = await askPrefill(' repo path: ', st.workDir || process.cwd(), true, pathCompleter);
337
436
  if (a === BACK) return BACK; // first step — driver just stays here
338
437
  wd = path.resolve(a);
339
438
  }
@@ -426,12 +525,32 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
426
525
  st.conv[key] = v;
427
526
  };
428
527
 
528
+ // Pick which agents get the /pr skill. --agent overrides (non-interactive); --yes auto-detects;
529
+ // else an interactive checklist with detected agents pre-checked.
530
+ const sAgents = async () => {
531
+ screen(3, 'Agents');
532
+ const targets = agentTargets();
533
+ if (flags.agents) { st.agents = flags.agents; log(` forced (--agent): ${st.agents.join(', ')}`); return; }
534
+ const detected = new Set(detectedAgentIds());
535
+ if (flags.yes) { st.agents = [...detected]; log(` auto-detected: ${st.agents.join(', ')}`); return; }
536
+ const preIdx = targets.map((t, i) => (detected.has(t.id) ? i : -1)).filter((i) => i >= 0);
537
+ const picked = await multiSelect(
538
+ 'Install the /pr skill into which agents? (detected pre-checked):',
539
+ targets.map((t) => ({ label: `${t.id}${detected.has(t.id) ? ' \x1b[2m(detected)\x1b[0m' : ''}` })),
540
+ false, preIdx,
541
+ );
542
+ if (picked === BACK) return BACK;
543
+ st.agents = picked.map((i) => targets[i].id);
544
+ log(` agents: ${st.agents.join(', ') || '(none)'}`);
545
+ };
546
+
429
547
  const steps = flags.yes
430
- ? [sWorkDir, sReviewers, sPreset]
548
+ ? [sWorkDir, sReviewers, sPreset, sAgents]
431
549
  : [sWorkDir, sReviewers, sPreset,
432
550
  editField('title_pattern: ', 'title_pattern'),
433
551
  editField('ticket_regex: ', 'ticket_regex'),
434
- editField('template_sections:', 'template_sections')];
552
+ editField('template_sections:', 'template_sections'),
553
+ sAgents];
435
554
 
436
555
  let i = 0;
437
556
  while (i < steps.length) {
@@ -441,18 +560,19 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
441
560
  }
442
561
 
443
562
  // --- install ---
444
- screen(3, 'Install');
563
+ screen(4, 'Install');
445
564
  const { slug, pcfg, email, convention, conv, chosen } = st;
446
- let cfg = `os=${cfgOs}\ntoken_src=${tokenSrc}\nemail=${email}\nconvention=${convention}\n`;
565
+ let cfg = `os=${cfgOs}\ntoken_src=${tokenSrc}\nemail=${email}\nconvention=${convention}\nagent_dir=${AGENT_DIR}\n`;
447
566
  if (conv.title_pattern) cfg += `title_pattern=${conv.title_pattern}\n`;
448
567
  if (conv.ticket_regex) cfg += `ticket_regex=${conv.ticket_regex}\n`;
449
568
  if (conv.template_sections) cfg += `template_sections=${conv.template_sections}\n`;
450
569
  writeFile(path.join(HOME, '.config/pr/config'), cfg, 0o600);
451
- // Roster lives in the work dir (main repo root — shared across worktrees). Editable
452
- // in place; pr-common.sh finds it first, falling back to the global path.
570
+ // Roster: neutral <main-repo-root>/.pr/reviewers.txt — agent-agnostic (bash reads it, not the
571
+ // agent), shared across worktrees, survives switching agents. pr-common.sh finds it first,
572
+ // falling back to the legacy agent-dir/root/global paths.
453
573
  const roster = chosen.map((r) => `${r.uuid} # ${r.name}`).join('\n') + (chosen.length ? '\n' : '');
454
574
  const header = `# pr reviewers for ${slug} — {uuid} # Name per line; blanks/# ignored. Edit freely.\n`;
455
- writeFile(path.join(st.mainRoot, '.pr-reviewers.txt'), header + (roster || '# (none selected)\n'));
575
+ writeFile(path.join(st.mainRoot, '.pr', 'reviewers.txt'), header + (roster || '# (none selected)\n'));
456
576
  const binDir = path.join(HOME, '.local/bin');
457
577
  const binsToInstall = flags.all
458
578
  ? Object.values(PROVIDERS).filter((p) => p.ready).map((p) => p.bin)
@@ -463,7 +583,7 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
463
583
  if (!fs.existsSync(src)) { log(` skip ${b} (asset not present yet)`); continue; }
464
584
  writeFile(path.join(binDir, b), fs.readFileSync(src), 0o755);
465
585
  }
466
- writeFile(path.join(HOME, '.claude/commands/pr.md'), fs.readFileSync(path.join(ASSETS, 'skill/pr.md')));
586
+ installSkill(st.agents || detectedAgentIds());
467
587
  if (!(process.env.PATH || '').split(':').includes(binDir)) {
468
588
  log(`\n WARN: ${binDir} not on PATH. Add to your shell rc:`);
469
589
  log(` export PATH="$HOME/.local/bin:$PATH"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nvice-r/pr-commands",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Provider-agnostic PR helper for Claude Code — npx installer + bbpr/ghpr/glpr binaries + /pr skill",
5
5
  "bin": {
6
6
  "pr-commands": "install.js"
@@ -15,7 +15,14 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "keywords": ["claude-code", "pull-request", "bitbucket", "github", "gitlab", "cli"],
18
+ "keywords": [
19
+ "claude-code",
20
+ "pull-request",
21
+ "bitbucket",
22
+ "github",
23
+ "gitlab",
24
+ "cli"
25
+ ],
19
26
  "author": "nvice.r",
20
27
  "license": "MIT"
21
28
  }