@nvice-r/pr-commands 0.1.3 → 0.1.5
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 +37 -51
- package/assets/bin/pr-commands +71 -0
- package/assets/{bin → share/pr-commands}/pr-common.sh +31 -12
- package/assets/share/pr-commands/providers/bitbucket.sh +77 -0
- package/assets/skill/pr.body.md +37 -38
- package/install.js +43 -22
- package/package.json +3 -3
- package/assets/bin/bbpr +0 -81
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Flags:
|
|
|
47
47
|
| `--agent <id>` | force skill install to this agent (repeatable): `claude-code`\|`antigravity`\|`codex`\|`opencode`. Default: auto-detect |
|
|
48
48
|
| `--all` | install every ready provider binary, not just the detected one |
|
|
49
49
|
| `--agent-dir <dir>` | repo folder for the reviewer roster (default: primary agent's — `.claude`/`.agent`/`.codex`/`.opencode`) |
|
|
50
|
-
| `--yes` | non-interactive: reviewers = existing roster if any, else all; convention = `
|
|
50
|
+
| `--yes` | non-interactive: reviewers = existing roster if any, else all; convention = `versioned` |
|
|
51
51
|
| `--reset-reviewers` | ignore an existing roster and rebuild it from the repo's default reviewers |
|
|
52
52
|
| `--dry-run` | print planned actions, write nothing |
|
|
53
53
|
| `--help` | usage |
|
|
@@ -78,8 +78,9 @@ identically from every clone and every git worktree, no repo-local symlinks.
|
|
|
78
78
|
|
|
79
79
|
| Artifact | Path |
|
|
80
80
|
|----------|------|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
81
|
+
| Command (only file on `PATH`) | `~/.local/bin/pr-commands` |
|
|
82
|
+
| Shared library | `~/.local/share/pr-commands/pr-common.sh` |
|
|
83
|
+
| Host provider | `~/.local/share/pr-commands/providers/bitbucket.sh` (`github.sh`/`gitlab.sh` later) |
|
|
83
84
|
| Config | `~/.config/pr/config` |
|
|
84
85
|
| Reviewer roster | `<main-repo-root>/<agent_dir>/pr-reviewers.txt` (primary agent's folder; shared across worktrees) |
|
|
85
86
|
| Canonical skill | `~/.config/pr/skill/pr/SKILL.md` (single source) |
|
|
@@ -97,7 +98,7 @@ In Claude Code, from any configured repo:
|
|
|
97
98
|
|
|
98
99
|
```
|
|
99
100
|
/pr # infer base, draft PR, confirm, create
|
|
100
|
-
/pr
|
|
101
|
+
/pr develop # explicit base branch
|
|
101
102
|
/pr draft # create as draft
|
|
102
103
|
/pr help # help
|
|
103
104
|
```
|
|
@@ -113,7 +114,7 @@ confirmation, then calls the detected binary. It never creates a PR silently.
|
|
|
113
114
|
os=macos # macos | linux
|
|
114
115
|
token_src=keychain # keychain | secret-tool | env
|
|
115
116
|
email=you@example.com
|
|
116
|
-
convention=
|
|
117
|
+
convention=versioned # see presets
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
Edit by hand any time. **A missing config is a hard error** — `/pr` and the binaries refuse to
|
|
@@ -127,7 +128,7 @@ with different conventions share the same skill:
|
|
|
127
128
|
|
|
128
129
|
```yaml
|
|
129
130
|
title_pattern: "[<ver>] Feature/<ver>/<ticket> <desc>"
|
|
130
|
-
ticket_regex: "
|
|
131
|
+
ticket_regex: "PROJ-\\d+"
|
|
131
132
|
template_sections: [What Happened, Insight, Proof of Work]
|
|
132
133
|
body_rules: |
|
|
133
134
|
- Free-text project rules the skill follows literally.
|
|
@@ -135,10 +136,10 @@ base: develop # default base proposal
|
|
|
135
136
|
draft: ask # ask | true | false
|
|
136
137
|
push: [origin] # remotes pushed before create
|
|
137
138
|
fork: # optional cross-repo mode (`/pr cross`)
|
|
138
|
-
remote:
|
|
139
|
-
repo: workspace/
|
|
139
|
+
remote: upstream
|
|
140
|
+
repo: workspace/repo
|
|
140
141
|
pre_pr: [] # commands run before confirm; non-zero exit stops /pr
|
|
141
|
-
post_pr: [
|
|
142
|
+
post_pr: [] # sibling skills offered after create
|
|
142
143
|
```
|
|
143
144
|
|
|
144
145
|
Without the file, the convention keys below (written by the wizard) are used.
|
|
@@ -147,7 +148,7 @@ Without the file, the convention keys below (written by the wizard) are used.
|
|
|
147
148
|
|
|
148
149
|
| preset | title pattern | ticket regex | template sections |
|
|
149
150
|
|--------|---------------|--------------|-------------------|
|
|
150
|
-
| `
|
|
151
|
+
| `versioned` | `[<ver>] Feature/<ver>/<ticket> <desc>` | `[A-Z]+-\d+` | What Happened / Insight / Proof of Work |
|
|
151
152
|
| `conventional` | `<type>(<scope>): <desc>` | `[A-Z]+-\d+` | Summary / Changes / Testing |
|
|
152
153
|
| `github-default` | `<desc> (#<issue>)` | `#\d+` | Summary / Test plan |
|
|
153
154
|
| `custom` | your `title_pattern=` | your `ticket_regex=` | your `template_sections=` (pipe-separated) |
|
|
@@ -175,7 +176,7 @@ roster:
|
|
|
175
176
|
|
|
176
177
|
Switching primary agent = re-run the installer; the old file still loads as a fallback.
|
|
177
178
|
|
|
178
|
-
You (the author) are excluded on install —
|
|
179
|
+
You (the author) are excluded on install — `pr-commands` also drops self at PR time. One entry per line,
|
|
179
180
|
trailing `# Name` comment optional:
|
|
180
181
|
|
|
181
182
|
```
|
|
@@ -184,22 +185,21 @@ trailing `# Name` comment optional:
|
|
|
184
185
|
```
|
|
185
186
|
|
|
186
187
|
It's plain data — edit freely, no reinstall. Consider gitignoring the roster unless the team wants
|
|
187
|
-
it committed.
|
|
188
|
-
|
|
189
|
-
legacy. Run-time overrides: `--no-reviewers` skips all, `--reviewer <uuid>` appends.
|
|
188
|
+
it committed. `$PR_REVIEWERS` (a file path) overrides the roster location. Run-time overrides:
|
|
189
|
+
`--no-reviewers` skips all, `--reviewer <uuid>` appends.
|
|
190
190
|
|
|
191
191
|
### Auth precedence (email)
|
|
192
192
|
|
|
193
|
-
`--email` flag → `$BB_EMAIL` → `git config
|
|
193
|
+
`--email` flag → `$BB_EMAIL` → `git config pr.email` → config `email` → keychain account.
|
|
194
194
|
The token is always read from the keychain (or `$PR_TOKEN` when `token_src=env`).
|
|
195
195
|
|
|
196
196
|
## Provider support
|
|
197
197
|
|
|
198
|
-
| provider |
|
|
199
|
-
|
|
200
|
-
| Bitbucket Cloud | `
|
|
201
|
-
| GitHub | `
|
|
202
|
-
| GitLab | `
|
|
198
|
+
| provider | file | status |
|
|
199
|
+
|----------|------|--------|
|
|
200
|
+
| Bitbucket Cloud | `providers/bitbucket.sh` | ✅ ready |
|
|
201
|
+
| GitHub | `providers/github.sh` | 🚧 not yet — `pr-commands` errors `github not yet supported` |
|
|
202
|
+
| GitLab | `providers/gitlab.sh` | 🚧 not yet |
|
|
203
203
|
|
|
204
204
|
| OS | token source | status |
|
|
205
205
|
|----|--------------|--------|
|
|
@@ -208,17 +208,21 @@ The token is always read from the keychain (or `$PR_TOKEN` when `token_src=env`)
|
|
|
208
208
|
|
|
209
209
|
## Architecture
|
|
210
210
|
|
|
211
|
-
- **
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
- **One command, one file per host.** `pr-commands` is the only thing on `PATH`. It reads
|
|
212
|
+
`git remote get-url origin` (override: `$PR_PROVIDER`) and loads `providers/<host>.sh`.
|
|
213
|
+
Each host keeps its own small file because the REST APIs genuinely differ. OS differences
|
|
214
|
+
(token retrieval) are a single `get_token()` function, not a separate file.
|
|
215
215
|
- **Shared logic in `pr-common.sh`** — git scan, base save, reviewer load, arg parse, token —
|
|
216
|
-
sourced
|
|
216
|
+
sourced before the provider. A provider file is ~60 lines: payload shape + endpoint + parse.
|
|
217
217
|
- **One generic `/pr` skill.** Convention lives in config, not the skill, so the skill is
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
host- and team-neutral and installs globally. It always calls `pr-commands create`, so adding
|
|
219
|
+
a host = one provider file + `ready: true` in `install.js`, zero skill changes.
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
```bash
|
|
222
|
+
pr-commands create --title "..." --body-file body.md --dest develop [--draft]
|
|
223
|
+
pr-commands check # which host, and is it supported?
|
|
224
|
+
pr-commands help
|
|
225
|
+
```
|
|
222
226
|
|
|
223
227
|
## Troubleshooting
|
|
224
228
|
|
|
@@ -227,39 +231,21 @@ Full rationale in [`DECISIONS.md`](./DECISIONS.md).
|
|
|
227
231
|
| `no ~/.config/pr/config` | run `npx @nvice-r/pr-commands` |
|
|
228
232
|
| reviewers not fetched | add the keychain token (see Prerequisites), re-run |
|
|
229
233
|
| `WARN ~/.local/bin not on PATH` | add the export line above to your shell rc |
|
|
230
|
-
|
|
|
231
|
-
| worktree runs old behavior | skill/
|
|
234
|
+
| `<provider> not yet supported` | GitHub/GitLab aren't implemented yet |
|
|
235
|
+
| worktree runs old behavior | skill/command are global, not repo-local; ensure no stale per-repo copy shadows them |
|
|
232
236
|
| 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 |
|
|
233
237
|
|
|
234
238
|
## Uninstall
|
|
235
239
|
|
|
236
240
|
```bash
|
|
237
|
-
rm ~/.local/bin/
|
|
241
|
+
rm ~/.local/bin/pr-commands
|
|
242
|
+
rm -rf ~/.local/share/pr-commands
|
|
238
243
|
rm -rf ~/.config/pr
|
|
239
244
|
rm ~/.claude/commands/pr.md
|
|
240
245
|
# per-agent skill symlinks (remove those you installed):
|
|
241
246
|
rm ~/.claude/skills/pr ~/.gemini/antigravity/skills/pr ~/.codex/skills/pr ~/.config/opencode/skills/pr
|
|
242
247
|
# roster lives in each repo — remove per repo if wanted:
|
|
243
|
-
rm
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
## Development
|
|
247
|
-
|
|
248
|
-
```
|
|
249
|
-
install.js npx entry (wizard)
|
|
250
|
-
assets/bin/* provider binaries + shared lib (source of truth; copied on install)
|
|
251
|
-
assets/skill/pr.body.md shared /pr skill body (frontmatter added per-target on install)
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
`assets/` is the single source of truth — both the installer (`install.js`)
|
|
255
|
-
and the published package (`files` in `package.json`) read from it. No duplicate
|
|
256
|
-
tree to keep in sync.
|
|
257
|
-
|
|
258
|
-
Publish (owner only):
|
|
259
|
-
|
|
260
|
-
```bash
|
|
261
|
-
npm login
|
|
262
|
-
npm publish --access public
|
|
248
|
+
rm <main-repo-root>/<agent_dir>/pr-reviewers.txt
|
|
263
249
|
```
|
|
264
250
|
|
|
265
251
|
## License
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pr-commands — create pull requests on any git host (one entry point).
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# pr-commands create --title "..." [--body|--body-file f] [--source b] [--dest b]
|
|
6
|
+
# [--repo owner/name] [--draft] [--reviewer id]... [--no-reviewers]
|
|
7
|
+
# [--email a] [--close-source] [--source-repo owner/fork]
|
|
8
|
+
# pr-commands check detect the host from the origin remote; exit 0 if supported
|
|
9
|
+
# pr-commands help this help
|
|
10
|
+
# pr-commands --version
|
|
11
|
+
#
|
|
12
|
+
# The host is detected from `git remote get-url origin` (override: $PR_PROVIDER).
|
|
13
|
+
# Host code lives in <share>/providers/<provider>.sh; shared code in <share>/pr-common.sh,
|
|
14
|
+
# where <share> = $PR_COMMANDS_HOME, else <this dir>/../share/pr-commands.
|
|
15
|
+
# pr-commands-version: 1
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
PR_CMD="pr-commands"
|
|
20
|
+
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
SHARE="${PR_COMMANDS_HOME:-$_here/../share/pr-commands}"
|
|
22
|
+
|
|
23
|
+
usage() { sed -n '2,14p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
24
|
+
fail() { echo "$PR_CMD: $*" >&2; exit 1; }
|
|
25
|
+
|
|
26
|
+
# Echo the provider name for the origin remote (or $PR_PROVIDER), or fail.
|
|
27
|
+
detect_provider() {
|
|
28
|
+
if [ -n "${PR_PROVIDER:-}" ]; then echo "$PR_PROVIDER"; return; fi
|
|
29
|
+
local url
|
|
30
|
+
url="$(git remote get-url origin 2>/dev/null)" || fail "no origin remote"
|
|
31
|
+
case "$url" in
|
|
32
|
+
*bitbucket.org*) echo bitbucket ;;
|
|
33
|
+
*github.com*) echo github ;;
|
|
34
|
+
*gitlab*) echo gitlab ;;
|
|
35
|
+
*) fail "unknown git host for remote: $url (set PR_PROVIDER to override)" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Source the shared lib + the provider file. Fails if the provider is not installed.
|
|
40
|
+
load_provider() {
|
|
41
|
+
PROVIDER="$(detect_provider)"
|
|
42
|
+
local pfile="$SHARE/providers/${PROVIDER}.sh"
|
|
43
|
+
[ -f "$SHARE/pr-common.sh" ] || fail "missing $SHARE/pr-common.sh — run: npx @nvice-r/pr-commands"
|
|
44
|
+
[ -f "$pfile" ] || fail "${PROVIDER} not yet supported (no $pfile)"
|
|
45
|
+
# shellcheck source=../share/pr-commands/pr-common.sh
|
|
46
|
+
. "$SHARE/pr-common.sh"
|
|
47
|
+
# shellcheck disable=SC1090
|
|
48
|
+
. "$pfile"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sub="${1:-help}"
|
|
52
|
+
case "$sub" in
|
|
53
|
+
-h|--help|--version) ;;
|
|
54
|
+
--*) set -- create "$@"; sub=create ;; # bare flags = create
|
|
55
|
+
esac
|
|
56
|
+
|
|
57
|
+
case "$sub" in
|
|
58
|
+
create)
|
|
59
|
+
shift
|
|
60
|
+
load_provider
|
|
61
|
+
declare -F provider_create >/dev/null || fail "${PROVIDER}: create not implemented"
|
|
62
|
+
provider_create "$@"
|
|
63
|
+
;;
|
|
64
|
+
check)
|
|
65
|
+
load_provider
|
|
66
|
+
echo "provider=${PROVIDER} ok"
|
|
67
|
+
;;
|
|
68
|
+
help|-h|--help) usage ;;
|
|
69
|
+
--version|version) sed -n 's/^# pr-commands-version: //p' "${BASH_SOURCE[0]}" ;;
|
|
70
|
+
*) fail "unknown command: $sub (try: $PR_CMD help)" ;;
|
|
71
|
+
esac
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# pr-common-version:
|
|
3
|
-
# pr-common.sh — shared library for
|
|
4
|
-
# NOT executable on its own — sourced by
|
|
5
|
-
#
|
|
2
|
+
# pr-common-version: 3
|
|
3
|
+
# pr-common.sh — shared library for the `pr-commands` entry point.
|
|
4
|
+
# NOT executable on its own — sourced by `pr-commands` before providers/<host>.sh, which
|
|
5
|
+
# builds the host-specific payload + POST. Everything here is host-AGNOSTIC.
|
|
6
6
|
#
|
|
7
7
|
# Provides (after `pr_common_init "$@"`): global vars
|
|
8
8
|
# TITLE BODY SOURCE DEST REPO SOURCE_REPO EMAIL TOKEN DRAFT CLOSE_SOURCE
|
|
@@ -82,7 +82,7 @@ pr_parse_args() {
|
|
|
82
82
|
--reviewer) EXTRA_REVIEWERS+=("$2"); shift 2 ;;
|
|
83
83
|
--no-reviewers) NO_REVIEWERS="true"; shift ;;
|
|
84
84
|
--email) EMAIL_ARG="$2"; shift 2 ;;
|
|
85
|
-
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
|
85
|
+
-h|--help) if declare -F usage >/dev/null; then usage; else sed -n '2,20p' "$0"; fi; exit 0 ;;
|
|
86
86
|
*) die "unknown flag: $1" ;;
|
|
87
87
|
esac
|
|
88
88
|
done
|
|
@@ -101,10 +101,11 @@ pr_git_defaults() {
|
|
|
101
101
|
[ "$SOURCE" = "HEAD" ] && die "detached HEAD — pass --source"
|
|
102
102
|
fi
|
|
103
103
|
if [ -z "$DEST" ]; then
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
pr_migrate_base
|
|
105
|
+
DEST="$(git config --get "branch.${SOURCE}.base" 2>/dev/null || true)"
|
|
106
|
+
[ -n "$DEST" ] && echo "${PR_CMD:-pr}: using saved base (branch.${SOURCE}.base = ${DEST})" >&2
|
|
106
107
|
fi
|
|
107
|
-
[ -n "$DEST" ] || die "--dest required (no saved branch.${SOURCE}.
|
|
108
|
+
[ -n "$DEST" ] || die "--dest required (no saved branch.${SOURCE}.base)"
|
|
108
109
|
if [ -z "$REPO" ]; then
|
|
109
110
|
local url
|
|
110
111
|
url="$(git remote get-url origin 2>/dev/null)" || die "no origin remote"
|
|
@@ -116,9 +117,11 @@ pr_git_defaults() {
|
|
|
116
117
|
|
|
117
118
|
# --- creds: email precedence + token via OS ---
|
|
118
119
|
pr_resolve_creds() {
|
|
119
|
-
# --email > $BB_EMAIL > git config
|
|
120
|
+
# --email > $BB_EMAIL > git config pr.email > config email > keychain acct
|
|
121
|
+
# (legacy git config bbpr.email still read as fallback after pr.email)
|
|
120
122
|
EMAIL="$EMAIL_ARG"
|
|
121
123
|
[ -n "$EMAIL" ] || EMAIL="${BB_EMAIL:-}"
|
|
124
|
+
[ -n "$EMAIL" ] || EMAIL="$(git config pr.email 2>/dev/null || true)"
|
|
122
125
|
[ -n "$EMAIL" ] || EMAIL="$(git config bbpr.email 2>/dev/null || true)"
|
|
123
126
|
[ -n "$EMAIL" ] || EMAIL="${CFG_EMAIL:-}"
|
|
124
127
|
[ -n "$EMAIL" ] || die "no email resolved (set config email or pass --email)"
|
|
@@ -128,7 +131,7 @@ pr_resolve_creds() {
|
|
|
128
131
|
# --- reviewer roster: work-dir file (shared across worktrees) then global fallbacks ---
|
|
129
132
|
pr_load_reviewers() {
|
|
130
133
|
DEFAULT_REVIEWERS=()
|
|
131
|
-
local file="${BBPR_REVIEWERS:-}"
|
|
134
|
+
local file="${PR_REVIEWERS:-${BBPR_REVIEWERS:-}}" # BBPR_REVIEWERS = legacy name
|
|
132
135
|
if [ -z "$file" ]; then
|
|
133
136
|
local slug; slug="$(echo "$REPO" | tr '/' '-')" # workspace/slug -> workspace-slug
|
|
134
137
|
# main repo root, shared across worktrees: git-common-dir/.. (empty if git call fails)
|
|
@@ -161,12 +164,28 @@ pr_load_reviewers() {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
# --- persist resolved base for next run ---
|
|
167
|
+
# branch.<b>.base is the shared, host-neutral key (also read by
|
|
168
|
+
# @nvice-r/git-commands git-restack).
|
|
164
169
|
pr_save_base() {
|
|
165
|
-
git config "branch.${SOURCE}.
|
|
166
|
-
&& echo "${PR_CMD:-pr}: saved branch.${SOURCE}.
|
|
170
|
+
git config "branch.${SOURCE}.base" "$DEST" 2>/dev/null \
|
|
171
|
+
&& echo "${PR_CMD:-pr}: saved branch.${SOURCE}.base = ${DEST}" >&2
|
|
167
172
|
return 0 # base-save is best-effort; never abort the binary on failure
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
# One-time migration: pr-commands <= 0.1.3 saved the base under the legacy key
|
|
176
|
+
# branch.<b>.bbprBase. Copy it to branch.<b>.base and unset the old key.
|
|
177
|
+
# Remove this function once no branch can still carry the legacy key.
|
|
178
|
+
pr_migrate_base() {
|
|
179
|
+
local old
|
|
180
|
+
git config --get "branch.${SOURCE}.base" >/dev/null 2>&1 && return 0
|
|
181
|
+
old="$(git config --get "branch.${SOURCE}.bbprBase" 2>/dev/null || true)"
|
|
182
|
+
[ -n "$old" ] || return 0
|
|
183
|
+
git config "branch.${SOURCE}.base" "$old" 2>/dev/null \
|
|
184
|
+
&& git config --unset "branch.${SOURCE}.bbprBase" 2>/dev/null \
|
|
185
|
+
&& echo "${PR_CMD:-pr}: migrated branch.${SOURCE}.bbprBase -> branch.${SOURCE}.base" >&2
|
|
186
|
+
return 0 # best-effort; never abort the binary on failure
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
# --- orchestrate everything a provider needs before its POST ---
|
|
171
190
|
pr_common_init() {
|
|
172
191
|
pr_load_config
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# bitbucket-provider-version: 10
|
|
2
|
+
# providers/bitbucket.sh — Bitbucket Cloud (REST API v2.0). Sourced by `pr-commands`
|
|
3
|
+
# after pr-common.sh; never run directly. Only Bitbucket-specific code lives here:
|
|
4
|
+
# payload shape, endpoint, self-as-reviewer rule, response parse.
|
|
5
|
+
|
|
6
|
+
KEYCHAIN_SERVICE="bitbucket-api-token"
|
|
7
|
+
|
|
8
|
+
provider_create() {
|
|
9
|
+
pr_common_init "$@"
|
|
10
|
+
|
|
11
|
+
# --- Bitbucket: drop the PR author's own UUID (BB rejects author-as-reviewer) ---
|
|
12
|
+
if [ "${#FINAL_REVIEWERS[@]}" -gt 0 ]; then
|
|
13
|
+
local SELF_UUID
|
|
14
|
+
SELF_UUID="$(curl -sS -u "${EMAIL}:${TOKEN}" "https://api.bitbucket.org/2.0/user" 2>/dev/null \
|
|
15
|
+
| python3 -c 'import json,sys
|
|
16
|
+
try: print(json.load(sys.stdin).get("uuid",""))
|
|
17
|
+
except Exception: print("")' 2>/dev/null || true)"
|
|
18
|
+
if [ -n "$SELF_UUID" ]; then
|
|
19
|
+
local FILTERED=() r
|
|
20
|
+
for r in "${FINAL_REVIEWERS[@]}"; do
|
|
21
|
+
[ "$r" = "$SELF_UUID" ] || FILTERED+=("$r")
|
|
22
|
+
done
|
|
23
|
+
FINAL_REVIEWERS=("${FILTERED[@]:-}")
|
|
24
|
+
[ -z "${FINAL_REVIEWERS[0]:-}" ] && FINAL_REVIEWERS=()
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# --- Bitbucket payload (source/destination keys, draft, reviewers[]) ---
|
|
29
|
+
local PAYLOAD
|
|
30
|
+
PAYLOAD="$(
|
|
31
|
+
TITLE="$TITLE" BODY="$BODY" SOURCE="$SOURCE" DEST="$DEST" SOURCE_REPO="$SOURCE_REPO" \
|
|
32
|
+
DRAFT="$DRAFT" CLOSE_SOURCE="$CLOSE_SOURCE" \
|
|
33
|
+
REVIEWERS_JSON="$(printf '%s\n' "${FINAL_REVIEWERS[@]:-}" | python3 -c 'import json,sys; print(json.dumps([{"uuid": l.strip()} for l in sys.stdin if l.strip()]))')" \
|
|
34
|
+
python3 -c '
|
|
35
|
+
import json, os
|
|
36
|
+
p = {
|
|
37
|
+
"title": os.environ["TITLE"],
|
|
38
|
+
"description": os.environ["BODY"],
|
|
39
|
+
"source": {"branch": {"name": os.environ["SOURCE"]}},
|
|
40
|
+
"destination": {"branch": {"name": os.environ["DEST"]}},
|
|
41
|
+
"draft": os.environ["DRAFT"] == "true",
|
|
42
|
+
"close_source_branch": os.environ["CLOSE_SOURCE"] == "true",
|
|
43
|
+
}
|
|
44
|
+
if os.environ["SOURCE_REPO"]:
|
|
45
|
+
p["source"]["repository"] = {"full_name": os.environ["SOURCE_REPO"]}
|
|
46
|
+
revs = json.loads(os.environ["REVIEWERS_JSON"])
|
|
47
|
+
if revs: p["reviewers"] = revs
|
|
48
|
+
print(json.dumps(p))
|
|
49
|
+
'
|
|
50
|
+
)"
|
|
51
|
+
|
|
52
|
+
local URL="https://api.bitbucket.org/2.0/repositories/${REPO}/pullrequests"
|
|
53
|
+
echo "POST $URL"
|
|
54
|
+
echo "source=${SOURCE_REPO:+$SOURCE_REPO:}$SOURCE dest=$DEST draft=$DRAFT reviewers=${#FINAL_REVIEWERS[@]}"
|
|
55
|
+
|
|
56
|
+
local RESP PR_INFO
|
|
57
|
+
RESP="$(curl -sS -X POST "$URL" -u "${EMAIL}:${TOKEN}" \
|
|
58
|
+
-H 'Content-Type: application/json' -d "$PAYLOAD")"
|
|
59
|
+
|
|
60
|
+
PR_INFO="$(echo "$RESP" | python3 -c '
|
|
61
|
+
import json, sys
|
|
62
|
+
try: d = json.loads(sys.stdin.read())
|
|
63
|
+
except Exception as e: print("non-JSON response", e); sys.exit(2)
|
|
64
|
+
if "error" in d: print("ERROR:", json.dumps(d["error"], indent=2)); sys.exit(1)
|
|
65
|
+
print(str(d.get("id")) + "\t" + str(d.get("links", {}).get("html", {}).get("href")))
|
|
66
|
+
')" || { echo "$PR_INFO" >&2; exit 1; } # surface the API error; set -e would swallow it
|
|
67
|
+
|
|
68
|
+
local NEW_PR_ID NEW_PR_URL ATTACHED
|
|
69
|
+
NEW_PR_ID="$(echo "$PR_INFO" | cut -f1)"
|
|
70
|
+
NEW_PR_URL="$(echo "$PR_INFO" | cut -f2)"
|
|
71
|
+
ATTACHED="$(echo "$RESP" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("reviewers", [])))')"
|
|
72
|
+
echo "created PR #${NEW_PR_ID} with ${ATTACHED} reviewers"
|
|
73
|
+
echo "$NEW_PR_URL"
|
|
74
|
+
|
|
75
|
+
[ -n "$NEW_PR_ID" ] && pr_save_base
|
|
76
|
+
return 0
|
|
77
|
+
}
|
package/assets/skill/pr.body.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Create a pull request for the current branch. Provider (Bitbucket / GitHub / GitLab) is
|
|
2
|
-
auto-detected from the git remote
|
|
2
|
+
auto-detected from the git remote by the `pr-commands` command, which runs that host's provider.
|
|
3
3
|
Convention (title pattern, ticket regex, PR template, body rules, push remotes, hooks) comes
|
|
4
4
|
from the repo's `.agents/config/pr.yaml` (one file per project, usually copied there by
|
|
5
5
|
`/landing`). Without that file, the convention keys in `~/.config/pr/config` are used.
|
|
@@ -36,8 +36,8 @@ UPDATE
|
|
|
36
36
|
goals, filler connectors. Keeps every file + technical fact.
|
|
37
37
|
|
|
38
38
|
CONVENTION <repo>/.agents/config/pr.yaml (per project; else convention keys in the user config)
|
|
39
|
-
CONFIG ~/.config/pr/config (os, token_src, email, agent_dir;
|
|
40
|
-
PROVIDER auto from `git remote get-url origin`
|
|
39
|
+
CONFIG ~/.config/pr/config (os, token_src, email, agent_dir; fallback convention keys)
|
|
40
|
+
PROVIDER auto from `git remote get-url origin` (run by `pr-commands`)
|
|
41
41
|
REVIEWERS <main-repo-root>/<agent_dir>/pr-reviewers.txt (per-repo, shared across worktrees; agent_dir from config)
|
|
42
42
|
SETUP npx @nvice-r/pr-commands
|
|
43
43
|
```
|
|
@@ -52,17 +52,11 @@ missing piece). Do NOT improvise config or reviewers.
|
|
|
52
52
|
CFG="${PR_CONFIG:-$HOME/.config/pr/config}"
|
|
53
53
|
[ -f "$CFG" ] || { echo "MISSING config: $CFG"; }
|
|
54
54
|
|
|
55
|
-
# b.
|
|
56
|
-
|
|
57
|
-
case "$URL" in
|
|
58
|
-
*bitbucket.org*) PROVIDER=bitbucket; BIN=bbpr ;;
|
|
59
|
-
*github.com*) PROVIDER=github; BIN=ghpr ;;
|
|
60
|
-
*gitlab.com*) PROVIDER=gitlab; BIN=glpr ;;
|
|
61
|
-
*) echo "unknown provider for remote: $URL" ;;
|
|
62
|
-
esac
|
|
55
|
+
# b. command installed?
|
|
56
|
+
command -v pr-commands >/dev/null 2>&1 || echo "MISSING command: pr-commands"
|
|
63
57
|
|
|
64
|
-
# c.
|
|
65
|
-
|
|
58
|
+
# c. host detected + supported? prints "provider=<name> ok", or the reason it is not
|
|
59
|
+
pr-commands check
|
|
66
60
|
|
|
67
61
|
# d. project convention file: this checkout first, then the main checkout (worktrees share it,
|
|
68
62
|
# because .agents/config/ is usually gitignored and only exists in the main checkout)
|
|
@@ -75,14 +69,14 @@ done
|
|
|
75
69
|
echo "convention: ${PR_YAML:-$CFG (no .agents/config/pr.yaml)}"
|
|
76
70
|
```
|
|
77
71
|
|
|
78
|
-
- config missing → STOP: "run `npx @nvice-r/pr-commands`".
|
|
79
|
-
-
|
|
80
|
-
|
|
72
|
+
- config or `pr-commands` missing → STOP: "run `npx @nvice-r/pr-commands`".
|
|
73
|
+
- `pr-commands check` fails → STOP and show its message (unknown host, or
|
|
74
|
+
"<provider> not yet supported" — only Bitbucket for now). (D6 stub.)
|
|
81
75
|
- all pass → continue.
|
|
82
76
|
|
|
83
77
|
## Step 1 — gather context (parallel)
|
|
84
78
|
- `git rev-parse --abbrev-ref HEAD` — source branch (CURR)
|
|
85
|
-
- `git config --get branch.<CURR>.
|
|
79
|
+
- `git config --get branch.<CURR>.base` — saved base, if any
|
|
86
80
|
- `git log --oneline -20`
|
|
87
81
|
- read the **convention source**: `$PR_YAML` if Step 0 found one, else the convention keys in `$CFG`.
|
|
88
82
|
Every later step that says "convention" means this source.
|
|
@@ -103,8 +97,8 @@ echo "convention: ${PR_YAML:-$CFG (no .agents/config/pr.yaml)}"
|
|
|
103
97
|
| `post_pr` | sibling skills offered after create (Step 10) | none |
|
|
104
98
|
|
|
105
99
|
## Step 2 — determine base branch (priority)
|
|
106
|
-
a.
|
|
107
|
-
b. else `branch.<CURR>.
|
|
100
|
+
a. an argument containing `/` → use it as the base.
|
|
101
|
+
b. else saved `branch.<CURR>.base` from git config → use silently.
|
|
108
102
|
c. else infer — smallest non-zero `<ref>..HEAD` distance where `<ref>` is an ancestor of HEAD:
|
|
109
103
|
```bash
|
|
110
104
|
for ref in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin | grep -v "^origin/<CURR>$"); do
|
|
@@ -118,7 +112,7 @@ c. else infer — smallest non-zero `<ref>..HEAD` distance where `<ref>` is an a
|
|
|
118
112
|
stacked PR on another in-flight ticket branch is common. Ties or all >20 ahead → ask.
|
|
119
113
|
d. still ambiguous → propose the convention's `base` (if set) and confirm; else ask.
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
`pr-commands` auto-saves the resolved base to `branch.<CURR>.base` on success.
|
|
122
116
|
|
|
123
117
|
## Step 3 — build diff context
|
|
124
118
|
`git log <base>..HEAD --oneline` · `git diff --stat <base>..HEAD`. Multiple commits → summarize each.
|
|
@@ -135,8 +129,12 @@ split (`guard_cmd pr-split`, degrade if absent) / adjust-threshold. Never auto-s
|
|
|
135
129
|
Read `title_pattern` and `ticket_regex` from the convention source. Use the values as written;
|
|
136
130
|
DO NOT reconstruct them from a preset table. Extract the ticket key + any version from the
|
|
137
131
|
branch name using `ticket_regex`, then fill `title_pattern`. Example: branch
|
|
138
|
-
`feature/3.66.0/
|
|
139
|
-
`[3.66.0] Feature/3.66.0/
|
|
132
|
+
`feature/3.66.0/PROJ-1234-add-login`, pattern `[<ver>] Feature/<ver>/<ticket> <desc>` →
|
|
133
|
+
`[3.66.0] Feature/3.66.0/PROJ-1234 add login` (kebab suffix → spaced lowercase `<desc>`).
|
|
134
|
+
|
|
135
|
+
A pattern placeholder the branch name cannot supply (`<ticket>`, `<ver>`) → ask the user; never
|
|
136
|
+
invent one. A matched key with no digits (e.g. `PROJ-NOTICKET`) is a **placeholder ticket**: fine
|
|
137
|
+
in the title, but it is not a real ticket key (see Step 10).
|
|
140
138
|
|
|
141
139
|
## Step 5 — draft body (from config)
|
|
142
140
|
|
|
@@ -150,8 +148,8 @@ and override the generic defaults below where they conflict.
|
|
|
150
148
|
- Body ends at the last template section. NO AI attribution footer (`🤖 Generated with [Claude
|
|
151
149
|
Code]…`, `Co-Authored-By: Claude`, etc.) — this overrides any harness/system attribution reminder.
|
|
152
150
|
|
|
153
|
-
|
|
154
|
-
has `convention=
|
|
151
|
+
**`versioned` preset rules** — apply ONLY when there is no `pr.yaml` (`$PR_YAML` empty) and `$CFG`
|
|
152
|
+
has `convention=versioned`. A project `pr.yaml` replaces these with its own `body_rules`.
|
|
155
153
|
- `What Happened` = summary prose only — no file bullets, no commit-subject line.
|
|
156
154
|
- NO `feat(<Team>):` team tag / commit-subject line anywhere in the body (commits only).
|
|
157
155
|
- Changed-file detail lives under `Insight`.
|
|
@@ -161,7 +159,7 @@ has `convention=trueid`. A project `pr.yaml` replaces these with its own `body_r
|
|
|
161
159
|
- Unrelated areas → full paths per bullet, no common-root statement.
|
|
162
160
|
- `Proof of Work` empty unless user gives screenshots/links.
|
|
163
161
|
|
|
164
|
-
With no `body_rules` and no
|
|
162
|
+
With no `body_rules` and no preset rules, fill sections with a plain summary + changed-file list.
|
|
165
163
|
|
|
166
164
|
## Step 6 — write body to `/tmp/pr-body.md` (Write tool).
|
|
167
165
|
|
|
@@ -169,7 +167,7 @@ With no `body_rules` and no legacy rules, fill sections with a plain summary + c
|
|
|
169
167
|
Run each `pre_pr` command from the convention, in order, from the repo root. Any non-zero exit →
|
|
170
168
|
STOP, show its output, create nothing. No `pre_pr` → skip silently.
|
|
171
169
|
|
|
172
|
-
## Step 7 — confirm. Show proposed title + base + body + resolved provider
|
|
170
|
+
## Step 7 — confirm. Show proposed title + base + body + resolved provider + mode
|
|
173
171
|
(direct/cross) + remotes to push. WAIT. PR creation is user-visible; never silent.
|
|
174
172
|
|
|
175
173
|
## Step 7.5 — mode + push
|
|
@@ -181,34 +179,35 @@ STOP, show its output, create nothing. No `pre_pr` → skip silently.
|
|
|
181
179
|
- Push failure → STOP and show the error. Never pass `--no-verify` (pre-push hooks may lint;
|
|
182
180
|
allow 30–60 s). No `push` key → do not push; warn if HEAD is ahead of its upstream.
|
|
183
181
|
|
|
184
|
-
## Step 8 — create. Run
|
|
182
|
+
## Step 8 — create. Run `pr-commands create` (append `--draft` only if draft chosen):
|
|
185
183
|
```bash
|
|
186
|
-
|
|
187
|
-
[--source-repo "<fork.repo>"]
|
|
184
|
+
pr-commands create --title "<title>" --body-file /tmp/pr-body.md --dest "<base>" [--draft] \
|
|
185
|
+
[--source-repo "<fork.repo>"] # cross mode only
|
|
188
186
|
```
|
|
189
187
|
Draft: `draft` arg → draft. Else convention `draft: true|false` → use it. Else (`ask` or unset)
|
|
190
188
|
ask open-vs-draft before creating. After a cross-mode success, run
|
|
191
|
-
`git config branch.<CURR>.prMode cross` so the next `/pr` reuses the mode.
|
|
189
|
+
`git config branch.<CURR>.prMode cross` so the next `/pr` reuses the mode. `pr-commands` attaches the
|
|
192
190
|
per-repo reviewer roster automatically; `--no-reviewers` skips, `--reviewer <id>` appends.
|
|
193
191
|
|
|
194
|
-
## Step 9 — print final PR URL + reviewer count from
|
|
192
|
+
## Step 9 — print final PR URL + reviewer count from `pr-commands` output. On error, surface raw response.
|
|
195
193
|
|
|
196
194
|
## Step 10 — post-PR skills (guarded, optional).
|
|
197
|
-
For each skill in the convention's `post_pr
|
|
195
|
+
For each skill in the convention's `post_pr`:
|
|
198
196
|
`guard_cmd <skill>`; absent → one-line note, skip. Present → `AskUserQuestion`: run now
|
|
199
|
-
(`/<skill> <ticket>` with the Step 4 ticket key) or skip. No ticket key
|
|
197
|
+
(`/<skill> <ticket>` with the Step 4 ticket key) or skip. No ticket key, or only a placeholder
|
|
198
|
+
ticket (no digits, e.g. `PROJ-NOTICKET`) → skip ticket-based skills with a one-line note.
|
|
200
199
|
|
|
201
200
|
## Update mode (`/pr update [PR#] [short]`)
|
|
202
201
|
|
|
203
202
|
Rewrites an existing open PR's **description** in place — no new branch, no new PR. Same Step 0
|
|
204
203
|
install check runs first (config + provider + creds). Provider PATCH is currently Bitbucket-only,
|
|
205
|
-
matching the create path;
|
|
204
|
+
matching the create path; any other host → STOP with the same "not yet supported" note.
|
|
206
205
|
|
|
207
206
|
**U1 — parse args.** Tokens after `update`: a bare integer = `PR#`; the word `short` (or
|
|
208
207
|
`caveman`) = compress-hard flag. Missing `PR#` → resolve from the current branch: query the
|
|
209
208
|
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.
|
|
210
209
|
|
|
211
|
-
**U2 — fetch current PR.** Resolve creds the same way
|
|
210
|
+
**U2 — fetch current PR.** Resolve creds the same way `pr-commands` does — email from `$CFG`
|
|
212
211
|
(`email=`), token from the OS keychain (macOS service `bitbucket-api-token`, account = email).
|
|
213
212
|
`REPO` = workspace/slug from `git remote get-url origin`. Fetch title + description:
|
|
214
213
|
```bash
|
|
@@ -216,8 +215,8 @@ curl -sS -u "$EMAIL:$TOKEN" \
|
|
|
216
215
|
"https://api.bitbucket.org/2.0/repositories/$REPO/pullrequests/$PRNUM"
|
|
217
216
|
```
|
|
218
217
|
|
|
219
|
-
**U3 — re-draft the body.** Rebuild the description with the **Step 5** rules (
|
|
220
|
-
when `convention=
|
|
218
|
+
**U3 — re-draft the body.** Rebuild the description with the **Step 5** rules (`versioned` template
|
|
219
|
+
when `convention=versioned`): `What Happened` prose, `Insight` file detail, `Proof of Work`. Prefer
|
|
221
220
|
the diff/commits as the source of truth over the old text; carry forward Proof-of-Work links the
|
|
222
221
|
old body already had. Default output = terse, anti-slop normal English:
|
|
223
222
|
- Cut throat-clearing openers ("The X feature needs to…", "This adds…"), restated ticket goals,
|
|
@@ -258,7 +257,7 @@ absent, degrade gracefully (one-line note). Reusable check (scans Claude command
|
|
|
258
257
|
agent skills dir):
|
|
259
258
|
```bash
|
|
260
259
|
guard_cmd() { # usage: guard_cmd pr-split → prints path or "no-<cmd>"
|
|
261
|
-
local c="$1"
|
|
260
|
+
local c="${@:1:1}" # first arg; avoids the positional form the skill loader rewrites
|
|
262
261
|
ls "$PWD/.claude/commands/$c.md" "$HOME/.claude/commands/$c.md" \
|
|
263
262
|
"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/$c/SKILL.md" \
|
|
264
263
|
"$HOME/.gemini/antigravity/skills/$c/SKILL.md" \
|
package/install.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
|
4
|
+
// install the `pr-commands` entry point -> ~/.local/bin (+ shared lib and host providers ->
|
|
5
|
+
// ~/.local/share/pr-commands), install the portable `pr` SKILL.md into every
|
|
5
6
|
// detected agent (symlinked to one canonical copy) + Claude /pr slash-command.
|
|
6
7
|
//
|
|
7
8
|
// Flags: --dir <path> --provider <p> --agent <id> --all --yes --dry-run --help
|
|
@@ -21,15 +22,16 @@ const ASSETS = path.join(PKG, 'assets');
|
|
|
21
22
|
const BACK = Symbol('back'); // returned by a prompt when the user wants the previous step
|
|
22
23
|
|
|
23
24
|
const PROVIDERS = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// provider code = assets/share/pr-commands/providers/<name>.sh
|
|
26
|
+
bitbucket: { host: 'bitbucket.org', keychainSvc: 'bitbucket-api-token', ready: true },
|
|
27
|
+
github: { host: 'github.com', keychainSvc: 'github-api-token', ready: false },
|
|
28
|
+
gitlab: { host: 'gitlab.com', keychainSvc: 'gitlab-api-token', ready: false },
|
|
27
29
|
};
|
|
28
|
-
const CONVENTIONS = ['
|
|
30
|
+
const CONVENTIONS = ['versioned', 'conventional', 'github-default', 'custom'];
|
|
29
31
|
// Concrete, editable defaults per preset. Whatever the user confirms is written to config,
|
|
30
32
|
// so `custom` is just "a preset with blank seeds" — same edit flow for every choice.
|
|
31
33
|
const PRESET_DEFS = {
|
|
32
|
-
|
|
34
|
+
versioned: { title_pattern: '[<ver>] Feature/<ver>/<ticket> <desc>', ticket_regex: '[A-Z]+-\\d+', template_sections: 'What Happened|Insight|Proof of Work' },
|
|
33
35
|
conventional: { title_pattern: '<type>(<scope>): <desc>', ticket_regex: '[A-Z]+-\\d+', template_sections: 'Summary|Changes|Testing' },
|
|
34
36
|
'github-default': { title_pattern: '<desc> (#<issue>)', ticket_regex: '#\\d+', template_sections: 'Summary|Test plan' },
|
|
35
37
|
custom: { title_pattern: '', ticket_regex: '', template_sections: '' },
|
|
@@ -96,8 +98,8 @@ function printHelp() {
|
|
|
96
98
|
folder — .claude / .agent / .codex / .opencode)
|
|
97
99
|
--agent <id> Force skill install to this agent (repeatable). Ids: claude-code,
|
|
98
100
|
antigravity, codex, opencode. Default: auto-detect installed agents.
|
|
99
|
-
--all Install
|
|
100
|
-
--yes Accept defaults, no prompts (reviewers=existing roster or all, convention=
|
|
101
|
+
--all Install every ready host provider (default: only the detected one)
|
|
102
|
+
--yes Accept defaults, no prompts (reviewers=existing roster or all, convention=versioned)
|
|
101
103
|
--reset-reviewers Ignore an existing roster and rebuild it from the repo's default reviewers
|
|
102
104
|
--dry-run Print planned actions, write nothing
|
|
103
105
|
--help This help`);
|
|
@@ -241,7 +243,7 @@ function printLegend() {
|
|
|
241
243
|
console.log(' Placeholders (used in title_pattern; filled by /pr from the branch):');
|
|
242
244
|
console.log(' \x1b[2m<ver> branch version e.g. 2.3.0\x1b[0m');
|
|
243
245
|
console.log(' \x1b[2m<desc> description from branch suffix / commits\x1b[0m');
|
|
244
|
-
console.log(' \x1b[2m<ticket> ticket key matched by ticket_regex, e.g.
|
|
246
|
+
console.log(' \x1b[2m<ticket> ticket key matched by ticket_regex, e.g. PROJ-123\x1b[0m');
|
|
245
247
|
console.log(' \x1b[2m<type> conventional type feat | fix | chore | ...\x1b[0m');
|
|
246
248
|
console.log(' \x1b[2m<scope> conventional scope e.g. auth\x1b[0m');
|
|
247
249
|
console.log(' \x1b[2m<issue> issue number e.g. 42\x1b[0m');
|
|
@@ -345,6 +347,20 @@ function scanGitRepos(root, maxDepth = 3, cap = 50) {
|
|
|
345
347
|
return found;
|
|
346
348
|
}
|
|
347
349
|
|
|
350
|
+
// Remove binaries from <= 0.1.3 (bbpr + pr-common.sh in ~/.local/bin). Only files that carry
|
|
351
|
+
// our own version marker are deleted, so an unrelated file with the same name is left alone.
|
|
352
|
+
function removeLegacyBins(binDir) {
|
|
353
|
+
for (const [name, marker] of [['bbpr', 'bbpr-version'], ['pr-common.sh', 'pr-common-version']]) {
|
|
354
|
+
const p = path.join(binDir, name);
|
|
355
|
+
let txt;
|
|
356
|
+
try { txt = fs.readFileSync(p, 'utf8'); } catch { continue; }
|
|
357
|
+
if (!txt.includes(marker)) { log(` kept ${p} (not ours)`); continue; }
|
|
358
|
+
if (flags.dryRun) { log(` [dry-run] remove legacy ${p}`); continue; }
|
|
359
|
+
fs.unlinkSync(p);
|
|
360
|
+
log(` removed legacy ${p}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
348
364
|
// ---- provider detection ----
|
|
349
365
|
function detectProvider(remoteUrl) {
|
|
350
366
|
if (flags.provider) return flags.provider;
|
|
@@ -426,7 +442,7 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
|
|
|
426
442
|
const L = [];
|
|
427
443
|
if (stage > 0 && st.workDir) {
|
|
428
444
|
L.push(` \x1b[32m✓\x1b[0m work dir ${st.workDir}`);
|
|
429
|
-
L.push(` \x1b[32m✓\x1b[0m provider ${st.provider} (${st.slug})
|
|
445
|
+
L.push(` \x1b[32m✓\x1b[0m provider ${st.provider} (${st.slug})`);
|
|
430
446
|
L.push(` \x1b[32m✓\x1b[0m auth ${st.email || '(none)'}${st.token ? ' · token ok' : ' · no token'}`);
|
|
431
447
|
}
|
|
432
448
|
if (stage > 1 && st.chosen) L.push(` \x1b[32m✓\x1b[0m reviewers ${st.chosen.length} selected`);
|
|
@@ -481,7 +497,7 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
|
|
|
481
497
|
if (!provider) { console.error(` ERROR: could not detect provider from ${remoteUrl}`); process.exit(1); }
|
|
482
498
|
const pcfg = PROVIDERS[provider];
|
|
483
499
|
const slug = parseSlug(remoteUrl);
|
|
484
|
-
log(` remote: ${remoteUrl}\n provider=${provider} slug=${slug}
|
|
500
|
+
log(` remote: ${remoteUrl}\n provider=${provider} slug=${slug}`);
|
|
485
501
|
if (!pcfg.ready) { console.error(` ERROR: ${provider} not yet supported (macOS+Bitbucket only). Aborting.`); process.exit(1); }
|
|
486
502
|
// identity + token (derived from provider)
|
|
487
503
|
let email = '';
|
|
@@ -534,11 +550,11 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
|
|
|
534
550
|
const sPreset = async () => {
|
|
535
551
|
screen(2, 'Convention — preset');
|
|
536
552
|
if (flags.yes) {
|
|
537
|
-
st.convention = st.convention
|
|
553
|
+
st.convention = PRESET_DEFS[st.convention] ? st.convention : 'versioned';
|
|
538
554
|
st.conv = { ...PRESET_DEFS[st.convention] };
|
|
539
555
|
return;
|
|
540
556
|
}
|
|
541
|
-
const idx = Math.max(0, CONVENTIONS.indexOf(st.convention || '
|
|
557
|
+
const idx = Math.max(0, CONVENTIONS.indexOf(st.convention || 'versioned'));
|
|
542
558
|
const pick = await multiSelectOne('Convention preset:', CONVENTIONS, idx);
|
|
543
559
|
if (pick === BACK) return BACK;
|
|
544
560
|
const chosenConv = CONVENTIONS[pick];
|
|
@@ -592,7 +608,7 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
|
|
|
592
608
|
|
|
593
609
|
// --- install ---
|
|
594
610
|
screen(4, 'Install');
|
|
595
|
-
const { slug,
|
|
611
|
+
const { slug, email, convention, conv, chosen } = st;
|
|
596
612
|
// Roster folder = --agent-dir, else the first chosen agent's repo folder, else .claude.
|
|
597
613
|
const agentIds = st.agents || detectedAgentIds();
|
|
598
614
|
const primary = agentTargets().find((t) => t.id === agentIds[0]);
|
|
@@ -615,16 +631,21 @@ async function fetchBitbucketReviewers(slug, email, token, selfUuid) {
|
|
|
615
631
|
const header = `# pr reviewers for ${slug} — {uuid} # Name per line; blanks/# ignored. Edit freely.\n`;
|
|
616
632
|
writeFile(rosterFile, header + (roster || '# (none selected)\n'));
|
|
617
633
|
}
|
|
634
|
+
// One entry point on PATH; shared lib + host providers under ~/.local/share/pr-commands.
|
|
618
635
|
const binDir = path.join(HOME, '.local/bin');
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
writeFile(path.join(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
636
|
+
const shareDir = path.join(HOME, '.local/share/pr-commands');
|
|
637
|
+
const shareSrc = path.join(ASSETS, 'share/pr-commands');
|
|
638
|
+
writeFile(path.join(binDir, 'pr-commands'), fs.readFileSync(path.join(ASSETS, 'bin/pr-commands')), 0o755);
|
|
639
|
+
writeFile(path.join(shareDir, 'pr-common.sh'), fs.readFileSync(path.join(shareSrc, 'pr-common.sh')), 0o644);
|
|
640
|
+
const provNames = flags.all
|
|
641
|
+
? Object.keys(PROVIDERS).filter((n) => PROVIDERS[n].ready)
|
|
642
|
+
: [st.provider];
|
|
643
|
+
for (const n of provNames) {
|
|
644
|
+
const src = path.join(shareSrc, 'providers', `${n}.sh`);
|
|
645
|
+
if (!fs.existsSync(src)) { log(` skip ${n} (provider not present yet)`); continue; }
|
|
646
|
+
writeFile(path.join(shareDir, 'providers', `${n}.sh`), fs.readFileSync(src), 0o644);
|
|
627
647
|
}
|
|
648
|
+
removeLegacyBins(binDir);
|
|
628
649
|
installSkill(agentIds);
|
|
629
650
|
if (!(process.env.PATH || '').split(':').includes(binDir)) {
|
|
630
651
|
log(`\n WARN: ${binDir} not on PATH. Add to your shell rc:`);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nvice-r/pr-commands",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Provider-agnostic PR helper for Claude Code — npx installer +
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Provider-agnostic PR helper for Claude Code — npx installer + one `pr-commands` command (per-host providers) + /pr skill",
|
|
5
5
|
"bin": {
|
|
6
|
-
"pr-commands": "install.js"
|
|
6
|
+
"pr-commands-setup": "install.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"install.js",
|
package/assets/bin/bbpr
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# bbpr-version: 9
|
|
3
|
-
# bbpr — create a Bitbucket Cloud pull request (REST API v2.0).
|
|
4
|
-
# Provider-specific layer; all git/config/creds/reviewer logic lives in pr-common.sh.
|
|
5
|
-
# Usage: bbpr --title "..." [--body|--body-file] [--source b] [--dest b] [--repo w/s]
|
|
6
|
-
# [--draft] [--reviewer uuid]... [--no-reviewers] [--email a] [--close-source]
|
|
7
|
-
# [--source-repo ws/fork] (cross-repo: PR opens in --repo/origin, from the fork)
|
|
8
|
-
|
|
9
|
-
set -euo pipefail
|
|
10
|
-
|
|
11
|
-
PR_CMD="bbpr"
|
|
12
|
-
KEYCHAIN_SERVICE="bitbucket-api-token"
|
|
13
|
-
|
|
14
|
-
# locate + source the shared lib (same dir as this binary)
|
|
15
|
-
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
-
# shellcheck source=pr-common.sh
|
|
17
|
-
. "$_here/pr-common.sh"
|
|
18
|
-
|
|
19
|
-
pr_common_init "$@"
|
|
20
|
-
|
|
21
|
-
# --- Bitbucket: drop the PR author's own UUID (BB rejects author-as-reviewer) ---
|
|
22
|
-
if [ "${#FINAL_REVIEWERS[@]}" -gt 0 ]; then
|
|
23
|
-
SELF_UUID="$(curl -sS -u "${EMAIL}:${TOKEN}" "https://api.bitbucket.org/2.0/user" 2>/dev/null \
|
|
24
|
-
| python3 -c 'import json,sys
|
|
25
|
-
try: print(json.load(sys.stdin).get("uuid",""))
|
|
26
|
-
except Exception: print("")' 2>/dev/null || true)"
|
|
27
|
-
if [ -n "$SELF_UUID" ]; then
|
|
28
|
-
FILTERED=()
|
|
29
|
-
for r in "${FINAL_REVIEWERS[@]}"; do
|
|
30
|
-
[ "$r" = "$SELF_UUID" ] || FILTERED+=("$r")
|
|
31
|
-
done
|
|
32
|
-
FINAL_REVIEWERS=("${FILTERED[@]:-}")
|
|
33
|
-
[ -z "${FINAL_REVIEWERS[0]:-}" ] && FINAL_REVIEWERS=()
|
|
34
|
-
fi
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# --- Bitbucket payload (source/destination keys, draft, reviewers[]) ---
|
|
38
|
-
PAYLOAD="$(
|
|
39
|
-
TITLE="$TITLE" BODY="$BODY" SOURCE="$SOURCE" DEST="$DEST" SOURCE_REPO="$SOURCE_REPO" \
|
|
40
|
-
DRAFT="$DRAFT" CLOSE_SOURCE="$CLOSE_SOURCE" \
|
|
41
|
-
REVIEWERS_JSON="$(printf '%s\n' "${FINAL_REVIEWERS[@]:-}" | python3 -c 'import json,sys; print(json.dumps([{"uuid": l.strip()} for l in sys.stdin if l.strip()]))')" \
|
|
42
|
-
python3 -c '
|
|
43
|
-
import json, os
|
|
44
|
-
p = {
|
|
45
|
-
"title": os.environ["TITLE"],
|
|
46
|
-
"description": os.environ["BODY"],
|
|
47
|
-
"source": {"branch": {"name": os.environ["SOURCE"]}},
|
|
48
|
-
"destination": {"branch": {"name": os.environ["DEST"]}},
|
|
49
|
-
"draft": os.environ["DRAFT"] == "true",
|
|
50
|
-
"close_source_branch": os.environ["CLOSE_SOURCE"] == "true",
|
|
51
|
-
}
|
|
52
|
-
if os.environ["SOURCE_REPO"]:
|
|
53
|
-
p["source"]["repository"] = {"full_name": os.environ["SOURCE_REPO"]}
|
|
54
|
-
revs = json.loads(os.environ["REVIEWERS_JSON"])
|
|
55
|
-
if revs: p["reviewers"] = revs
|
|
56
|
-
print(json.dumps(p))
|
|
57
|
-
'
|
|
58
|
-
)"
|
|
59
|
-
|
|
60
|
-
URL="https://api.bitbucket.org/2.0/repositories/${REPO}/pullrequests"
|
|
61
|
-
echo "POST $URL"
|
|
62
|
-
echo "source=${SOURCE_REPO:+$SOURCE_REPO:}$SOURCE dest=$DEST draft=$DRAFT reviewers=${#FINAL_REVIEWERS[@]}"
|
|
63
|
-
|
|
64
|
-
RESP="$(curl -sS -X POST "$URL" -u "${EMAIL}:${TOKEN}" \
|
|
65
|
-
-H 'Content-Type: application/json' -d "$PAYLOAD")"
|
|
66
|
-
|
|
67
|
-
PR_INFO="$(echo "$RESP" | python3 -c '
|
|
68
|
-
import json, sys
|
|
69
|
-
try: d = json.loads(sys.stdin.read())
|
|
70
|
-
except Exception as e: print("non-JSON response", e); sys.exit(2)
|
|
71
|
-
if "error" in d: print("ERROR:", json.dumps(d["error"], indent=2)); sys.exit(1)
|
|
72
|
-
print(str(d.get("id")) + "\t" + str(d.get("links", {}).get("html", {}).get("href")))
|
|
73
|
-
')" || { echo "$PR_INFO" >&2; exit 1; } # surface the API error; set -e would swallow it
|
|
74
|
-
|
|
75
|
-
NEW_PR_ID="$(echo "$PR_INFO" | cut -f1)"
|
|
76
|
-
NEW_PR_URL="$(echo "$PR_INFO" | cut -f2)"
|
|
77
|
-
ATTACHED="$(echo "$RESP" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("reviewers", [])))')"
|
|
78
|
-
echo "created PR #${NEW_PR_ID} with ${ATTACHED} reviewers"
|
|
79
|
-
echo "$NEW_PR_URL"
|
|
80
|
-
|
|
81
|
-
[ -n "$NEW_PR_ID" ] && pr_save_base
|