@henryqw/pi-pr 2.0.3 → 2.1.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 +4 -2
- package/extensions/pr-ui.ts +7 -3
- package/extensions/pr.ts +22 -6
- package/package.json +1 -1
- package/skills/pi-pr-update-branch/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -23,7 +23,9 @@ Run `/pr` without arguments in a GitHub checkout. It reads the current branch pu
|
|
|
23
23
|
| Surface | Type | Purpose |
|
|
24
24
|
| --- | --- | --- |
|
|
25
25
|
| Footer | ui | Show a linked `PR #number` and one plain-language status. |
|
|
26
|
-
| Widget
|
|
26
|
+
| Widget | ui | Show one actionable icon-prefixed `Run /pr to …` hint. |
|
|
27
|
+
|
|
28
|
+
The footer already shows the pull request and status. Actionable widgets omit duplicate identity and status. Each uses one semantic status icon, a space, and a plain `Run /pr to …` route. `✗` marks errors, `!` warnings, `✓` success, and `●` accent or neutral routes. In TUI, only the icon uses a theme color. RPC and non-TUI output use the same plain text without ANSI.
|
|
27
29
|
|
|
28
30
|
## Flow
|
|
29
31
|
|
|
@@ -65,7 +67,7 @@ Ordinary conversation comments do not trigger a route or block a merge. Changes
|
|
|
65
67
|
|
|
66
68
|
The footer and widget load at session start. They refresh after local commits, PR creation, pushes, and each dispatched workflow settles. They also poll every 30 seconds. Polling updates presentation only and may be stale.
|
|
67
69
|
|
|
68
|
-
The create
|
|
70
|
+
The create widget stays hidden until the local branch has a commit beyond its creation point. Any displayed widget clears as soon as `/pr` starts. A dispatched workflow keeps it hidden until the agent settles. A direct merge, no-action route, or failed command refreshes the widget when the handler finishes.
|
|
69
71
|
|
|
70
72
|
Presentation uses route priority, so draft appears before running CI. `/pr` reads fresh state before routing or merging. The command is authoritative for actions.
|
|
71
73
|
|
package/extensions/pr-ui.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCapabilities, hyperlink } from "@earendil-works/pi-tui";
|
|
1
|
+
import { getCapabilities, hyperlink, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import {
|
|
3
3
|
deriveNextStep,
|
|
4
4
|
type NextStep,
|
|
@@ -93,6 +93,10 @@ export function formatPrFooter(display: PrDisplay, theme: PrTheme): string | und
|
|
|
93
93
|
return `${link} · ${theme.fg(display.footer.color, display.footer.text)}`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export function formatPrWidget(display: PrDisplay): string | undefined {
|
|
97
|
-
|
|
96
|
+
export function formatPrWidget(display: PrDisplay, theme?: PrTheme, width?: number): string[] | undefined {
|
|
97
|
+
if (display.widget === undefined) return undefined;
|
|
98
|
+
const color = display.footer?.color ?? "accent";
|
|
99
|
+
const icon = color === "error" ? "✗" : color === "warning" ? "!" : color === "success" ? "✓" : "●";
|
|
100
|
+
const line = `${theme ? theme.fg(color, icon) : icon} ${display.widget}`;
|
|
101
|
+
return [width === undefined ? line : truncateToWidth(line, Math.max(1, width))];
|
|
98
102
|
}
|
package/extensions/pr.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
formatPrFooter,
|
|
13
13
|
formatPrWidget,
|
|
14
14
|
projectPrDisplay,
|
|
15
|
+
type PrDisplay,
|
|
15
16
|
} from "./pr-ui.ts";
|
|
16
17
|
|
|
17
18
|
const POLL_INTERVAL_MS = 30_000;
|
|
@@ -39,10 +40,25 @@ export default function pullRequestExtension(
|
|
|
39
40
|
let active: AbortController | undefined;
|
|
40
41
|
let queued = false;
|
|
41
42
|
let refreshFailureReported = false;
|
|
42
|
-
let displayedWidget:
|
|
43
|
+
let displayedWidget: PrDisplay | undefined;
|
|
43
44
|
let commandGeneration = 0;
|
|
44
45
|
const activeInvocations = new Map<number, "routing" | "workflow">();
|
|
45
46
|
|
|
47
|
+
const setWidget = (ctx: ExtensionContext, display: PrDisplay | undefined): void => {
|
|
48
|
+
if (display?.widget === undefined) {
|
|
49
|
+
ctx.ui.setWidget(UI_KEY, undefined);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (ctx.mode === "tui") {
|
|
53
|
+
ctx.ui.setWidget(UI_KEY, (_tui, theme) => ({
|
|
54
|
+
invalidate() {},
|
|
55
|
+
render: (width) => formatPrWidget(display, theme, width)!,
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
ctx.ui.setWidget(UI_KEY, formatPrWidget(display));
|
|
60
|
+
};
|
|
61
|
+
|
|
46
62
|
const render = (
|
|
47
63
|
ctx: ExtensionContext,
|
|
48
64
|
pullRequest: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
|
|
@@ -53,10 +69,10 @@ export default function pullRequestExtension(
|
|
|
53
69
|
if (pullRequest !== null && footer === undefined) {
|
|
54
70
|
throw new Error("Current pull request display is missing a footer");
|
|
55
71
|
}
|
|
56
|
-
displayedWidget =
|
|
72
|
+
displayedWidget = display.widget === undefined ? undefined : display;
|
|
57
73
|
const widget = activeInvocations.size > 0 ? undefined : displayedWidget;
|
|
58
74
|
ctx.ui.setStatus(UI_KEY, footer);
|
|
59
|
-
|
|
75
|
+
setWidget(ctx, widget);
|
|
60
76
|
};
|
|
61
77
|
|
|
62
78
|
const stop = (): void => {
|
|
@@ -172,7 +188,7 @@ export default function pullRequestExtension(
|
|
|
172
188
|
const generation = sessionGeneration;
|
|
173
189
|
const invocation = ++commandGeneration;
|
|
174
190
|
activeInvocations.set(invocation, "routing");
|
|
175
|
-
if (displayedWidget !== undefined)
|
|
191
|
+
if (displayedWidget !== undefined) setWidget(ctx, undefined);
|
|
176
192
|
let nextStep: Awaited<ReturnType<typeof commandHandler>>;
|
|
177
193
|
try {
|
|
178
194
|
nextStep = await commandHandler(args, ctx);
|
|
@@ -181,7 +197,7 @@ export default function pullRequestExtension(
|
|
|
181
197
|
cancelRefresh();
|
|
182
198
|
activeInvocations.delete(invocation);
|
|
183
199
|
if (!activeInvocations.size) {
|
|
184
|
-
|
|
200
|
+
setWidget(ctx, displayedWidget);
|
|
185
201
|
}
|
|
186
202
|
refreshInBackground();
|
|
187
203
|
}
|
|
@@ -192,7 +208,7 @@ export default function pullRequestExtension(
|
|
|
192
208
|
if (nextStep !== "none" && nextStep !== "merge") {
|
|
193
209
|
activeInvocations.set(invocation, "workflow");
|
|
194
210
|
if (nextStep === "create") ctx.ui.setStatus(UI_KEY, undefined);
|
|
195
|
-
|
|
211
|
+
setWidget(ctx, undefined);
|
|
196
212
|
} else {
|
|
197
213
|
activeInvocations.delete(invocation);
|
|
198
214
|
refreshInBackground();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-pr",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Run /pr to automatically choose the current pull request's next safe lifecycle step: create, update, address feedback, fix CI, or merge when ready.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -15,7 +15,7 @@ Before changing anything:
|
|
|
15
15
|
2. Read `LOCAL_BRANCH`'s validated `%(push:short)` with `git for-each-ref` and enumerate configured remote names. Match an exact `<remote>/` prefix, choosing the unique longest match so remote names containing `/` work. Save that remote and the remaining ref as `PUSH_REMOTE` and `PUSH_REF`, then validate `PUSH_REF` with `git check-ref-format --branch`. Require one push URL and validate its GitHub host and owner/repository. Do not use `%(push:remoteref)` or fall back to `LOCAL_BRANCH`.
|
|
16
16
|
3. Set `PR_FIELDS=number,url,state,baseRefName,headRepository,headRefName,headRefOid,mergeStateStatus,mergeable`. On the push target's host, search open pull requests by the exact push owner and `PUSH_REF`. Inspect every candidate by its URL with exactly `PR_FIELDS`; stop for incomplete, capped, duplicate, or ambiguous results. Never use branch-default `gh pr view` or retry with `LOCAL_BRANCH`.
|
|
17
17
|
4. Require exactly one open PR. Require its HTTPS URL to be exactly `HOST/OWNER/REPOSITORY/pull/NUMBER`, with no credentials, port, query, or fragment, and require its number to match. The URL gives the base host and repository. Require `headRepository.nameWithOwner` and `headRefName` to match the recorded push repository and `PUSH_REF`. Require local `HEAD` to equal `headRefOid`. Record the PR and its base/head repositories and refs. Set `EXPECTED_HEAD_SHA` from the initial head OID; never replace it. A fork head and upstream base are normal.
|
|
18
|
-
5. Validate `refs/heads/<BASE_REF>` with `git check-ref-format`, then split the validated base repository into its exact owner and name. Query `repository
|
|
18
|
+
5. Validate `refs/heads/<BASE_REF>` with `git check-ref-format`, then split the validated base repository into its exact owner and name. Query `repository { nameWithOwner ref(qualifiedName: "refs/heads/<BASE_REF>") { name target { oid } } }` with `gh api graphql --hostname "$PR_HOST"`. Require no GraphQL errors, `repository.nameWithOwner` to exactly equal the recorded `BASE_REPOSITORY`, the exact recorded ref, and one full hexadecimal target OID. Record that target as `BASE_SHA`; never replace it. Do not read `baseRefOid`, REST `pull.base.sha`, a local remote-tracking ref, or `git ls-remote` as a substitute.
|
|
19
19
|
|
|
20
20
|
## Fetch, pin, and merge
|
|
21
21
|
|
|
@@ -49,7 +49,7 @@ printf 'Fetched base %s %s at %s\n' "$BASE_REPOSITORY" "$BASE_REF" "$BASE_SHA"
|
|
|
49
49
|
|
|
50
50
|
If protocol lookup, API lookup, URL validation, fetch, or object verification fails, stop. The HTTPS helper is command-local and receives credentials only through Git's credential protocol. Never print a token, run `gh auth setup-git`, or change persistent Git config. The recorded `BASE_SHA` is authoritative for this run. Do not fall back to another URL, ref, protocol, or credential source.
|
|
51
51
|
|
|
52
|
-
Before merging, require the attached branch to remain `LOCAL_BRANCH` and the tree to remain clean. Re-resolve its configured push target exactly as above and require the saved remote, `PUSH_REF`, sole push URL, host, and repository. Re-read the PR by its recorded URL with exactly `PR_FIELDS`; require its number, URL, open state, base repository/ref, head repository, `PUSH_REF`, and head OID to remain unchanged. Re-run the exact live base-ref GraphQL query and require its
|
|
52
|
+
Before merging, require the attached branch to remain `LOCAL_BRANCH` and the tree to remain clean. Re-resolve its configured push target exactly as above and require the saved remote, `PUSH_REF`, sole push URL, host, and repository. Re-read the PR by its recorded URL with exactly `PR_FIELDS`; require its number, URL, open state, base repository/ref, head repository, `PUSH_REF`, and head OID to remain unchanged. Re-run the exact live base-ref GraphQL query, including `repository.nameWithOwner`, and require `repository.nameWithOwner` to exactly equal the recorded `BASE_REPOSITORY`; require its ref and target OID to match the recorded values and `BASE_SHA`. Immediately before merging, require local `HEAD` to equal `EXPECTED_HEAD_SHA`:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
LOCAL_HEAD="$(git rev-parse --verify HEAD)"
|
|
@@ -107,7 +107,7 @@ After the merge completes:
|
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
Stop on either failure. Record the validated full local `HEAD` as `MERGED_HEAD`. Do not substitute a newer ref or another SHA.
|
|
110
|
-
3. Immediately before the single push, repeat the complete branch, configured push target, sole push URL, recorded-URL PR guard, and live base-ref GraphQL query
|
|
110
|
+
3. Immediately before the single push, repeat the complete branch, configured push target, sole push URL, recorded-URL PR guard, and the same live base-ref GraphQL query, including `repository.nameWithOwner`. Require all saved identities and PR fields to remain exact, `repository.nameWithOwner` to exactly equal the recorded `BASE_REPOSITORY`, the live base target to remain `BASE_SHA`, the tree to be clean, and local `HEAD` to equal `MERGED_HEAD`. Stop on any change.
|
|
111
111
|
4. Push once to the saved configured push ref, without force or retry:
|
|
112
112
|
|
|
113
113
|
```bash
|