@henryqw/pi-pr 2.0.1 → 2.0.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
|
@@ -70,7 +70,11 @@ Ordinary conversation comments do not trigger a route or block a merge. Changes
|
|
|
70
70
|
|
|
71
71
|
## Refresh
|
|
72
72
|
|
|
73
|
-
The footer and widget load at session start. They refresh after local commits, PR creation,
|
|
73
|
+
The footer and widget load at session start. They refresh after local commits, PR creation, pushes, and the creation workflow. They also poll every 30 seconds. Polling updates presentation only and may be stale.
|
|
74
|
+
|
|
75
|
+
The create hint stays hidden until the local branch has a commit beyond its creation point. It clears as soon as `/pr` starts creation. It returns if the workflow finishes without a pull request.
|
|
76
|
+
|
|
77
|
+
Presentation uses route priority, so draft appears before running CI. `/pr` reads fresh state before routing or merging. The command is authoritative for actions.
|
|
74
78
|
|
|
75
79
|
## Safety limits
|
|
76
80
|
|
package/extensions/pr-command.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionCommandContext,
|
|
4
|
-
RegisteredCommand,
|
|
5
4
|
} from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import {
|
|
7
6
|
executeGitHubMerge,
|
|
@@ -26,7 +25,7 @@ const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
|
|
29
|
-
export type PrCommandHandler =
|
|
28
|
+
export type PrCommandHandler = (args: string, ctx: ExtensionCommandContext) => Promise<NextStep>;
|
|
30
29
|
|
|
31
30
|
function dispatchWorkflow(pi: PrCommandPi, ctx: ExtensionCommandContext, commandName: string): void {
|
|
32
31
|
const command = pi.getCommands().find((candidate) =>
|
|
@@ -141,14 +140,15 @@ export function createPrCommandHandler(pi: PrCommandPi): PrCommandHandler {
|
|
|
141
140
|
const notification = noActionNotification(current);
|
|
142
141
|
ctx.ui.notify(notification.message, notification.type);
|
|
143
142
|
}
|
|
144
|
-
return;
|
|
143
|
+
return nextStep;
|
|
145
144
|
}
|
|
146
145
|
if (nextStep === "merge") {
|
|
147
146
|
if (!current) throw new Error("/pr merge failed: pull request is unavailable");
|
|
148
147
|
await mergePullRequest(pi, ctx, current);
|
|
149
|
-
return;
|
|
148
|
+
return nextStep;
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
dispatchWorkflow(pi, ctx, WORKFLOWS[nextStep]);
|
|
152
|
+
return nextStep;
|
|
153
153
|
};
|
|
154
154
|
}
|
package/extensions/pr-github.ts
CHANGED
|
@@ -877,7 +877,6 @@ export async function loadCurrentPullRequest(
|
|
|
877
877
|
): Promise<CurrentPullRequest | null> {
|
|
878
878
|
const pushTarget = await readPushTarget(pi, context);
|
|
879
879
|
if (pushTarget === null) return null;
|
|
880
|
-
const [headOwner] = pushTarget.repository.nameWithOwner.split("/");
|
|
881
880
|
const search = await execute(pi, context, "Find pull requests", "gh", [
|
|
882
881
|
"api",
|
|
883
882
|
"search/issues",
|
|
@@ -888,7 +887,7 @@ export async function loadCurrentPullRequest(
|
|
|
888
887
|
"-X",
|
|
889
888
|
"GET",
|
|
890
889
|
"-f",
|
|
891
|
-
`q=is:pr head:${
|
|
890
|
+
`q=is:pr head:${pushTarget.ref}${pushTarget.remoteHeadOid === null ? "" : ` ${pushTarget.remoteHeadOid}`}`,
|
|
892
891
|
"-f",
|
|
893
892
|
`per_page=${PR_LIST_LIMIT}`,
|
|
894
893
|
]);
|
package/extensions/pr.ts
CHANGED
|
@@ -34,10 +34,15 @@ export default function pullRequestExtension(
|
|
|
34
34
|
const detectLocalCommit = dependencies.hasLocalCommit ?? hasLocalCommit;
|
|
35
35
|
const createCommandHandler = dependencies.createPrCommandHandler ?? createPrCommandHandler;
|
|
36
36
|
let context: ExtensionContext | undefined;
|
|
37
|
+
let sessionGeneration = 0;
|
|
37
38
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
38
39
|
let active: AbortController | undefined;
|
|
39
40
|
let queued = false;
|
|
40
41
|
let refreshFailureReported = false;
|
|
42
|
+
let displayedNextStep: ReturnType<typeof projectPrDisplay>["nextStep"] | undefined;
|
|
43
|
+
let displayedWidget: string | undefined;
|
|
44
|
+
let commandGeneration = 0;
|
|
45
|
+
const pendingCreations = new Set<number>();
|
|
41
46
|
|
|
42
47
|
const render = (
|
|
43
48
|
ctx: ExtensionContext,
|
|
@@ -49,15 +54,23 @@ export default function pullRequestExtension(
|
|
|
49
54
|
if (pullRequest !== null && footer === undefined) {
|
|
50
55
|
throw new Error("Current pull request display is missing a footer");
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
if (pullRequest !== null) pendingCreations.clear();
|
|
58
|
+
displayedWidget = formatPrWidget(display);
|
|
59
|
+
const widget = pendingCreations.size > 0 && display.nextStep === "create" ? undefined : displayedWidget;
|
|
60
|
+
displayedNextStep = display.nextStep;
|
|
53
61
|
ctx.ui.setStatus(UI_KEY, footer);
|
|
54
62
|
ctx.ui.setWidget(UI_KEY, widget === undefined ? undefined : [widget]);
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
const stop = (): void => {
|
|
66
|
+
sessionGeneration += 1;
|
|
58
67
|
context = undefined;
|
|
59
68
|
queued = false;
|
|
60
69
|
refreshFailureReported = false;
|
|
70
|
+
displayedNextStep = undefined;
|
|
71
|
+
displayedWidget = undefined;
|
|
72
|
+
commandGeneration = 0;
|
|
73
|
+
pendingCreations.clear();
|
|
61
74
|
if (timer !== undefined) clearInterval(timer);
|
|
62
75
|
timer = undefined;
|
|
63
76
|
active?.abort();
|
|
@@ -79,6 +92,7 @@ export default function pullRequestExtension(
|
|
|
79
92
|
const refresh = async (): Promise<void> => {
|
|
80
93
|
const ctx = context;
|
|
81
94
|
if (!ctx) return;
|
|
95
|
+
const generation = sessionGeneration;
|
|
82
96
|
if (active) {
|
|
83
97
|
queued = true;
|
|
84
98
|
return;
|
|
@@ -92,13 +106,14 @@ export default function pullRequestExtension(
|
|
|
92
106
|
let localCommit = false;
|
|
93
107
|
try {
|
|
94
108
|
pullRequest = await load(pi, loadContext);
|
|
109
|
+
if (controller.signal.aborted || sessionGeneration !== generation) return;
|
|
95
110
|
if (pullRequest === null) localCommit = await detectLocalCommit(pi, loadContext);
|
|
96
111
|
} catch (error) {
|
|
97
112
|
// Keep the last known display when lookup is unavailable.
|
|
98
|
-
if (!controller.signal.aborted &&
|
|
113
|
+
if (!controller.signal.aborted && sessionGeneration === generation) reportRefreshFailure(error);
|
|
99
114
|
return;
|
|
100
115
|
}
|
|
101
|
-
if (controller.signal.aborted ||
|
|
116
|
+
if (controller.signal.aborted || sessionGeneration !== generation) return;
|
|
102
117
|
render(ctx, pullRequest, localCommit);
|
|
103
118
|
refreshFailureReported = false;
|
|
104
119
|
} finally {
|
|
@@ -115,16 +130,30 @@ export default function pullRequestExtension(
|
|
|
115
130
|
void refresh().catch(reportRefreshFailure);
|
|
116
131
|
};
|
|
117
132
|
|
|
133
|
+
const cancelRefresh = (): void => {
|
|
134
|
+
active?.abort();
|
|
135
|
+
active = undefined;
|
|
136
|
+
queued = false;
|
|
137
|
+
};
|
|
138
|
+
|
|
118
139
|
pi.on("session_start", async (_event, ctx) => {
|
|
119
140
|
stop();
|
|
141
|
+
const generation = sessionGeneration;
|
|
120
142
|
if (!ctx.hasUI) return;
|
|
121
143
|
context = ctx;
|
|
122
144
|
await refresh();
|
|
123
|
-
if (
|
|
145
|
+
if (sessionGeneration === generation) timer = setInterval(refreshInBackground, POLL_INTERVAL_MS);
|
|
124
146
|
});
|
|
125
147
|
|
|
126
148
|
pi.on("session_shutdown", stop);
|
|
127
149
|
|
|
150
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
151
|
+
if (!ctx.hasUI || !ctx.isIdle() || !context || !pendingCreations.size) return;
|
|
152
|
+
cancelRefresh();
|
|
153
|
+
pendingCreations.clear();
|
|
154
|
+
await refresh().catch(reportRefreshFailure);
|
|
155
|
+
});
|
|
156
|
+
|
|
128
157
|
pi.on("tool_result", async (event, ctx) => {
|
|
129
158
|
if (!ctx.hasUI || event.isError || !isBashToolResult(event)) return;
|
|
130
159
|
const command = event.input.command;
|
|
@@ -137,10 +166,38 @@ export default function pullRequestExtension(
|
|
|
137
166
|
pi.registerCommand("pr", {
|
|
138
167
|
description: "Run the current branch pull request next step",
|
|
139
168
|
handler: async (args, ctx) => {
|
|
140
|
-
if (!ctx.hasUI) return;
|
|
169
|
+
if (!ctx.hasUI || !context) return;
|
|
170
|
+
const generation = sessionGeneration;
|
|
171
|
+
const invocation = ++commandGeneration;
|
|
172
|
+
const displayedCreation = displayedNextStep === "create";
|
|
173
|
+
if (displayedCreation) {
|
|
174
|
+
pendingCreations.add(invocation);
|
|
175
|
+
ctx.ui.setWidget(UI_KEY, undefined);
|
|
176
|
+
}
|
|
177
|
+
let nextStep: Awaited<ReturnType<typeof commandHandler>>;
|
|
141
178
|
try {
|
|
142
|
-
await commandHandler(args, ctx);
|
|
143
|
-
}
|
|
179
|
+
nextStep = await commandHandler(args, ctx);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (sessionGeneration === generation) {
|
|
182
|
+
cancelRefresh();
|
|
183
|
+
pendingCreations.delete(invocation);
|
|
184
|
+
if (!pendingCreations.size) {
|
|
185
|
+
ctx.ui.setWidget(UI_KEY, displayedNextStep === "create" && displayedWidget !== undefined
|
|
186
|
+
? [displayedWidget]
|
|
187
|
+
: undefined);
|
|
188
|
+
}
|
|
189
|
+
refreshInBackground();
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
if (sessionGeneration !== generation) return;
|
|
194
|
+
cancelRefresh();
|
|
195
|
+
if (nextStep === "create") {
|
|
196
|
+
pendingCreations.add(invocation);
|
|
197
|
+
ctx.ui.setStatus(UI_KEY, undefined);
|
|
198
|
+
ctx.ui.setWidget(UI_KEY, undefined);
|
|
199
|
+
} else {
|
|
200
|
+
pendingCreations.delete(invocation);
|
|
144
201
|
refreshInBackground();
|
|
145
202
|
}
|
|
146
203
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-pr",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
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",
|
|
@@ -383,10 +383,9 @@ function candidatePrUrls(pages, hostname) {
|
|
|
383
383
|
|
|
384
384
|
function discoverOpenPr(localHead) {
|
|
385
385
|
const push = configuredPushTarget();
|
|
386
|
-
const [owner] = push.repository.split("/", 2);
|
|
387
386
|
const response = readJsonValueCommand("gh", [
|
|
388
387
|
"api", "search/issues", "--hostname", push.hostname, "--paginate", "--slurp", "-X", "GET",
|
|
389
|
-
"-f", `q=is:pr is:open head:${
|
|
388
|
+
"-f", `q=is:pr is:open head:${push.ref} ${localHead}`,
|
|
390
389
|
"-f", `per_page=${PR_LIST_LIMIT}`,
|
|
391
390
|
]);
|
|
392
391
|
const candidates = candidatePrUrls(response, push.hostname).map((url) => {
|
|
@@ -1151,7 +1150,7 @@ if [ "$*" = "repo view ${enterpriseHost}/Owner/Repo --json nameWithOwner,url" ];
|
|
|
1151
1150
|
echo '{"nameWithOwner":"Owner/Repo","url":"https://${enterpriseHost}/Owner/Repo"}'
|
|
1152
1151
|
exit 0
|
|
1153
1152
|
fi
|
|
1154
|
-
if [ "$*" = "api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:
|
|
1153
|
+
if [ "$*" = "api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:feature/pr ${discoveryMetadata.headRefOid} -f per_page=100" ]; then
|
|
1155
1154
|
echo '[{"total_count":1,"incomplete_results":false,"items":[{"html_url":"https://${enterpriseHost}/Upstream/Project/pull/1"}]}]'
|
|
1156
1155
|
exit 0
|
|
1157
1156
|
fi
|
|
@@ -1198,7 +1197,7 @@ exit 1
|
|
|
1198
1197
|
const discoveryCommands = readFileSync(discoveryCalls, "utf8").trim().split("\n");
|
|
1199
1198
|
assert.equal(discoveryCommands[0], `repo view ${enterpriseHost}/Owner/Repo --json nameWithOwner,url`);
|
|
1200
1199
|
assert(!discoveryCommands.some((command) => command.includes("account") || command.includes("secret")));
|
|
1201
|
-
assert.equal(discoveryCommands[1], `api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:
|
|
1200
|
+
assert.equal(discoveryCommands[1], `api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:feature/pr ${discoveryMetadata.headRefOid} -f per_page=100`);
|
|
1202
1201
|
assert.match(discoveryCommands[2], new RegExp(`^pr view https://${enterpriseHost}/Upstream/Project/pull/1 `));
|
|
1203
1202
|
assert(!discoveryCommands.some((command) => command.startsWith("pr view --json")));
|
|
1204
1203
|
|
|
@@ -1215,7 +1214,7 @@ if [ "$*" = "repo view ${enterpriseHost}/Owner/Repo --json nameWithOwner,url" ];
|
|
|
1215
1214
|
echo '{"nameWithOwner":"Owner/Repo","url":"https://${enterpriseHost}/Owner/Repo"}'
|
|
1216
1215
|
exit 0
|
|
1217
1216
|
fi
|
|
1218
|
-
if [ "$*" = "api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:
|
|
1217
|
+
if [ "$*" = "api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:feature/pr ${discoveryMetadata.headRefOid} -f per_page=100" ]; then
|
|
1219
1218
|
echo '[{"total_count":1,"incomplete_results":false,"items":[{"html_url":"https://${enterpriseHost}/Owner/Repo/pull/1"}]}]'
|
|
1220
1219
|
exit 0
|
|
1221
1220
|
fi
|
|
@@ -1277,7 +1276,7 @@ exit 1
|
|
|
1277
1276
|
assert.equal(readFileSync(resolved, "utf8"), "first\n");
|
|
1278
1277
|
const resolutionCommands = readFileSync(resolutionCalls, "utf8").trim().split("\n");
|
|
1279
1278
|
assert.equal(resolutionCommands[0], `repo view ${enterpriseHost}/Owner/Repo --json nameWithOwner,url`);
|
|
1280
|
-
assert.equal(resolutionCommands[1], `api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:
|
|
1279
|
+
assert.equal(resolutionCommands[1], `api search/issues --hostname ${enterpriseHost} --paginate --slurp -X GET -f q=is:pr is:open head:feature/pr ${discoveryMetadata.headRefOid} -f per_page=100`);
|
|
1281
1280
|
assert.match(resolutionCommands[2], new RegExp(`^pr view https://${enterpriseHost}/Owner/Repo/pull/1 `));
|
|
1282
1281
|
assert(!resolutionCommands.some((command) => command.startsWith("pr view --json")));
|
|
1283
1282
|
|