@timurproko/a1 0.2.0 → 0.2.1-dev.590
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/bin/ui.js +1 -1
- package/dist/features/owned-ui/project-trust-dialog.d.ts +2 -0
- package/dist/features/owned-ui/project-trust-dialog.js +55 -0
- package/dist/features/owned-ui/project-trust-prompt.d.ts +3 -5
- package/dist/features/owned-ui/project-trust-prompt.js +9 -9
- package/dist/integrations/pi/engine/project-trust-preflight.d.ts +1 -2
- package/dist/integrations/pi/engine/project-trust-preflight.js +1 -5
- package/dist/integrations/pi/engine/repository-pr-rest.d.ts +21 -0
- package/dist/integrations/pi/engine/repository-pr-rest.js +155 -0
- package/dist/integrations/pi/engine/repository-pr.d.ts +10 -2
- package/dist/integrations/pi/engine/repository-pr.js +22 -5
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/package.json +1 -1
package/bin/ui.js
CHANGED
|
@@ -53,7 +53,7 @@ Promise.resolve().then(() => {
|
|
|
53
53
|
cwd: process.cwd(),
|
|
54
54
|
profileId,
|
|
55
55
|
ownedSurfaces,
|
|
56
|
-
projectTrustPrompt: createConsoleProjectTrustPrompt(),
|
|
56
|
+
projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
|
|
57
57
|
sessionForkPrompt: createConsoleSessionForkPrompt(),
|
|
58
58
|
...(sessionSelection === undefined ? {} : { sessionSelection }),
|
|
59
59
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const ACCENT = "\u001b[38;2;138;190;183m";
|
|
2
|
+
const BORDER = "\u001b[38;2;95;135;255m";
|
|
3
|
+
const MUTED = "\u001b[38;2;128;128;128m";
|
|
4
|
+
const DIM = "\u001b[38;2;102;102;102m";
|
|
5
|
+
const RESET_FG = "\u001b[39m";
|
|
6
|
+
const BOLD = "\u001b[1m";
|
|
7
|
+
const RESET_BOLD = "\u001b[22m";
|
|
8
|
+
const EXPLANATION = "This allows to load project settings and resources, install missing project packages, and execute project extensions.";
|
|
9
|
+
/** Fixed startup-safe rendering only; project-derived presentation is forbidden before trust resolves. */
|
|
10
|
+
export function renderProjectTrustDialog(cwd, selected, terminalWidth, terminalRows) {
|
|
11
|
+
const width = Math.max(1, terminalWidth);
|
|
12
|
+
const rule = `${BORDER}${"─".repeat(width)}${RESET_FG}`;
|
|
13
|
+
const title = ` ${BOLD}${ACCENT}Trust project folder?${RESET_FG}${RESET_BOLD}`;
|
|
14
|
+
const path = ` ${MUTED}${sanitize(cwd)}${RESET_FG}`;
|
|
15
|
+
const choices = [choice("Trust", selected === 0), choice("Do not trust", selected === 1)];
|
|
16
|
+
const hint = ` ${DIM}↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to cancel${RESET_FG}`;
|
|
17
|
+
const explanation = wrap(EXPLANATION, Math.max(1, width - 2)).map(line => ` ${line}`);
|
|
18
|
+
const preferred = [rule, title, path, "", ...explanation, "", ...choices, "", hint, rule];
|
|
19
|
+
if (preferred.length <= terminalRows)
|
|
20
|
+
return preferred;
|
|
21
|
+
const ruled = [rule, title, path, ...choices, hint, rule];
|
|
22
|
+
if (ruled.length <= terminalRows)
|
|
23
|
+
return ruled;
|
|
24
|
+
const compact = [title, path, ...choices, hint];
|
|
25
|
+
if (compact.length <= terminalRows)
|
|
26
|
+
return compact;
|
|
27
|
+
const essential = [title, ...choices, hint];
|
|
28
|
+
if (essential.length <= terminalRows)
|
|
29
|
+
return essential;
|
|
30
|
+
if (terminalRows >= 3)
|
|
31
|
+
return [...choices, hint];
|
|
32
|
+
if (terminalRows === 2)
|
|
33
|
+
return choices;
|
|
34
|
+
return [choices[selected] ?? choices[0] ?? ""];
|
|
35
|
+
}
|
|
36
|
+
function choice(label, selected) {
|
|
37
|
+
return selected ? ` ${ACCENT}→ ${label}${RESET_FG}` : ` ${label}`;
|
|
38
|
+
}
|
|
39
|
+
function wrap(text, width) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
let line = "";
|
|
42
|
+
for (const word of text.split(/\s+/u)) {
|
|
43
|
+
if (line && line.length + word.length + 1 > width) {
|
|
44
|
+
lines.push(line);
|
|
45
|
+
line = "";
|
|
46
|
+
}
|
|
47
|
+
line += `${line ? " " : ""}${word}`;
|
|
48
|
+
}
|
|
49
|
+
if (line)
|
|
50
|
+
lines.push(line);
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
function sanitize(text) {
|
|
54
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "�");
|
|
55
|
+
}
|
|
@@ -14,12 +14,10 @@ export interface ConsoleProjectTrustPromptOptions {
|
|
|
14
14
|
readonly output?: Writable & {
|
|
15
15
|
readonly isTTY?: boolean;
|
|
16
16
|
readonly columns?: number;
|
|
17
|
+
readonly rows?: number;
|
|
17
18
|
};
|
|
19
|
+
readonly presentation?: "bare" | "comparison";
|
|
18
20
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Pre-resource startup selector. It uses only fixed product wording and terminal
|
|
21
|
-
* controls: no project setting, theme, extension, prompt, package, or skill is
|
|
22
|
-
* consulted before the trust result exists.
|
|
23
|
-
*/
|
|
21
|
+
/** Fixed pre-resource selector; no project resource is available. */
|
|
24
22
|
export declare function createConsoleProjectTrustPrompt(options?: ConsoleProjectTrustPromptOptions): OwnedProjectTrustPrompt;
|
|
25
23
|
export {};
|
|
@@ -9,11 +9,7 @@ const DIM = "\u001b[38;2;102;102;102m";
|
|
|
9
9
|
const RESET_FG = "\u001b[39m";
|
|
10
10
|
const BOLD = "\u001b[1m";
|
|
11
11
|
const RESET_BOLD = "\u001b[22m";
|
|
12
|
-
/**
|
|
13
|
-
* Pre-resource startup selector. It uses only fixed product wording and terminal
|
|
14
|
-
* controls: no project setting, theme, extension, prompt, package, or skill is
|
|
15
|
-
* consulted before the trust result exists.
|
|
16
|
-
*/
|
|
12
|
+
/** Fixed pre-resource selector; no project resource is available. */
|
|
17
13
|
export function createConsoleProjectTrustPrompt(options = {}) {
|
|
18
14
|
const input = options.input ?? process.stdin;
|
|
19
15
|
const output = options.output ?? process.stdout;
|
|
@@ -21,6 +17,8 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
21
17
|
if (input.isTTY !== true || output.isTTY !== true) {
|
|
22
18
|
throw new Error("an interactive terminal is unavailable");
|
|
23
19
|
}
|
|
20
|
+
const renderBareDialog = options.presentation === "comparison"
|
|
21
|
+
? undefined : (await import("./project-trust-dialog.js")).renderProjectTrustDialog;
|
|
24
22
|
const wasRaw = input.isRaw === true;
|
|
25
23
|
let selected = 0;
|
|
26
24
|
let restored = false;
|
|
@@ -32,8 +30,9 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
32
30
|
output.write(`${CLEAR_HOME}${SHOW_CURSOR}${LEAVE_ALTERNATE_SCREEN}`);
|
|
33
31
|
};
|
|
34
32
|
const render = () => {
|
|
35
|
-
const width = Math.max(20, output.columns ?? 80);
|
|
36
|
-
const
|
|
33
|
+
const width = Math.max(renderBareDialog ? 1 : 20, output.columns ?? 80);
|
|
34
|
+
const rows = Math.max(1, output.rows ?? 24);
|
|
35
|
+
const lines = (renderBareDialog?.(cwd, selected, width, rows) ?? [
|
|
37
36
|
`${BOLD}${ACCENT}Trust project folder?${RESET_FG}${RESET_BOLD}`,
|
|
38
37
|
cwd,
|
|
39
38
|
"",
|
|
@@ -43,8 +42,9 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
43
42
|
optionRow("Do not trust", selected === 1),
|
|
44
43
|
"",
|
|
45
44
|
`${DIM}↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to cancel${RESET_FG}`,
|
|
46
|
-
];
|
|
47
|
-
|
|
45
|
+
]).map(line => clipAnsiSafe(line, width));
|
|
46
|
+
const padding = renderBareDialog === undefined ? "" : "\n".repeat(Math.max(0, rows - lines.length));
|
|
47
|
+
output.write(`${CLEAR_HOME}${padding}${lines.join("\n")}`);
|
|
48
48
|
};
|
|
49
49
|
try {
|
|
50
50
|
output.write(`${ENTER_ALTERNATE_SCREEN}${HIDE_CURSOR}`);
|
|
@@ -5,14 +5,13 @@ export interface PiProjectTrustPreflightRequest {
|
|
|
5
5
|
export type PiProjectTrustPreflightPrompt = (request: PiProjectTrustPreflightRequest) => Promise<boolean | null>;
|
|
6
6
|
export interface PiProjectTrustPreflightResult {
|
|
7
7
|
readonly trusted: boolean;
|
|
8
|
-
readonly source: "
|
|
8
|
+
readonly source: "saved" | "default" | "interactive" | "fail-closed";
|
|
9
9
|
readonly diagnostic: string | null;
|
|
10
10
|
}
|
|
11
11
|
export interface ResolvePiProjectTrustPreflightOptions {
|
|
12
12
|
readonly cwd: string;
|
|
13
13
|
readonly agentDir: string;
|
|
14
14
|
readonly prompt?: PiProjectTrustPreflightPrompt;
|
|
15
|
-
readonly hasProjectResources?: (cwd: string) => boolean;
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
18
17
|
* Resolves trust from global state only. It never constructs a project-trusted
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProjectTrustStore, SettingsManager, } from "../startup-public.js";
|
|
2
2
|
/**
|
|
3
3
|
* Resolves trust from global state only. It never constructs a project-trusted
|
|
4
4
|
* settings manager or resource loader; callers may do that only after this
|
|
5
5
|
* result exists.
|
|
6
6
|
*/
|
|
7
7
|
export async function resolvePiProjectTrustPreflight(options) {
|
|
8
|
-
const hasProjectResources = options.hasProjectResources ?? hasTrustRequiringProjectResources;
|
|
9
|
-
if (!hasProjectResources(options.cwd)) {
|
|
10
|
-
return { trusted: true, source: "no-project-resources", diagnostic: null };
|
|
11
|
-
}
|
|
12
8
|
const trustStore = new ProjectTrustStore(options.agentDir);
|
|
13
9
|
const saved = trustStore.get(options.cwd);
|
|
14
10
|
if (saved !== null)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface PiPullRequestIdentity {
|
|
2
|
+
readonly number: number;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
}
|
|
5
|
+
export interface PiGitHubRepositoryIdentity {
|
|
6
|
+
readonly owner: string;
|
|
7
|
+
readonly repository: string;
|
|
8
|
+
}
|
|
9
|
+
type PiGitHubFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
10
|
+
type PiCommandReader = (cwd: string, signal: AbortSignal) => Promise<string | null>;
|
|
11
|
+
export interface PiPullRequestRestDependencies {
|
|
12
|
+
readonly readOriginUrl?: PiCommandReader;
|
|
13
|
+
readonly fetch?: PiGitHubFetch;
|
|
14
|
+
}
|
|
15
|
+
/** Discover one exact branch pull request through GitHub's read-only REST API. */
|
|
16
|
+
export declare function readPullRequestFromGitHub(cwd: string, branch: string, signal: AbortSignal, environment?: NodeJS.ProcessEnv, dependencies?: PiPullRequestRestDependencies): Promise<PiPullRequestIdentity | null>;
|
|
17
|
+
/** Parse a supported GitHub origin URL without accepting credentials or non-GitHub hosts. */
|
|
18
|
+
export declare function parseGitHubRepositoryRemote(remote: string): PiGitHubRepositoryIdentity | null;
|
|
19
|
+
/** Validate a GitHub REST pull list as one exact branch association. */
|
|
20
|
+
export declare function parseGitHubPullRequests(value: unknown, expectedRepository: PiGitHubRepositoryIdentity, branch: string): PiPullRequestIdentity | null;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
3
|
+
const COMMAND_MAX_BUFFER_BYTES = 64 * 1024;
|
|
4
|
+
const RESPONSE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
5
|
+
const GITHUB_API_ORIGIN = "https://api.github.com";
|
|
6
|
+
/** Discover one exact branch pull request through GitHub's read-only REST API. */
|
|
7
|
+
export async function readPullRequestFromGitHub(cwd, branch, signal, environment = process.env, dependencies = {}) {
|
|
8
|
+
if (branch.length === 0 || signal.aborted)
|
|
9
|
+
return null;
|
|
10
|
+
const remote = await (dependencies.readOriginUrl ?? executeGitOriginUrl)(cwd, signal);
|
|
11
|
+
if (remote === null || signal.aborted)
|
|
12
|
+
return null;
|
|
13
|
+
const repository = parseGitHubRepositoryRemote(remote);
|
|
14
|
+
if (repository === null)
|
|
15
|
+
return null;
|
|
16
|
+
const endpoint = new URL(`/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repository)}/pulls`, GITHUB_API_ORIGIN);
|
|
17
|
+
endpoint.searchParams.set("state", "all");
|
|
18
|
+
endpoint.searchParams.set("head", `${repository.owner}:${branch}`);
|
|
19
|
+
endpoint.searchParams.set("per_page", "100");
|
|
20
|
+
const headers = {
|
|
21
|
+
accept: "application/vnd.github+json",
|
|
22
|
+
"user-agent": "a1-pr-footer",
|
|
23
|
+
"x-github-api-version": "2022-11-28",
|
|
24
|
+
};
|
|
25
|
+
const token = environment.GH_TOKEN || environment.GITHUB_TOKEN;
|
|
26
|
+
if (token !== undefined && token.trim().length > 0)
|
|
27
|
+
headers.authorization = `Bearer ${token}`;
|
|
28
|
+
try {
|
|
29
|
+
const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(PROBE_TIMEOUT_MS)]);
|
|
30
|
+
const response = await (dependencies.fetch ?? fetch)(endpoint, { headers, signal: requestSignal });
|
|
31
|
+
if (!response.ok || response.redirected)
|
|
32
|
+
return null;
|
|
33
|
+
const length = response.headers.get("content-length");
|
|
34
|
+
if (length !== null && (!/^\d+$/u.test(length) || Number(length) > RESPONSE_MAX_BUFFER_BYTES))
|
|
35
|
+
return null;
|
|
36
|
+
const bytes = await response.arrayBuffer();
|
|
37
|
+
if (bytes.byteLength > RESPONSE_MAX_BUFFER_BYTES || signal.aborted)
|
|
38
|
+
return null;
|
|
39
|
+
let value;
|
|
40
|
+
try {
|
|
41
|
+
value = JSON.parse(new TextDecoder().decode(bytes));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return parseGitHubPullRequests(value, repository, branch);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Parse a supported GitHub origin URL without accepting credentials or non-GitHub hosts. */
|
|
53
|
+
export function parseGitHubRepositoryRemote(remote) {
|
|
54
|
+
const value = remote.trim();
|
|
55
|
+
if (value.length === 0 || /[\r\n]/u.test(value))
|
|
56
|
+
return null;
|
|
57
|
+
const scp = /^git@github\.com:([^/]+)\/([^/]+)$/iu.exec(value);
|
|
58
|
+
if (scp !== null)
|
|
59
|
+
return normalizeRepository(scp[1], scp[2]);
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(value);
|
|
62
|
+
if ((url.protocol !== "https:" && url.protocol !== "ssh:")
|
|
63
|
+
|| url.hostname.toLowerCase() !== "github.com"
|
|
64
|
+
|| url.password !== ""
|
|
65
|
+
|| url.port !== ""
|
|
66
|
+
|| url.search !== ""
|
|
67
|
+
|| url.hash !== ""
|
|
68
|
+
|| (url.protocol === "https:" && url.username !== "")
|
|
69
|
+
|| (url.protocol === "ssh:" && url.username !== "git"))
|
|
70
|
+
return null;
|
|
71
|
+
const path = /^\/([^/]+)\/([^/]+)$/u.exec(url.pathname);
|
|
72
|
+
return path === null ? null : normalizeRepository(path[1], path[2]);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Validate a GitHub REST pull list as one exact branch association. */
|
|
79
|
+
export function parseGitHubPullRequests(value, expectedRepository, branch) {
|
|
80
|
+
if (!Array.isArray(value))
|
|
81
|
+
return null;
|
|
82
|
+
const fullName = `${expectedRepository.owner}/${expectedRepository.repository}`;
|
|
83
|
+
const matches = [];
|
|
84
|
+
for (const candidate of value) {
|
|
85
|
+
if (!isRecord(candidate)
|
|
86
|
+
|| !Number.isSafeInteger(candidate.number)
|
|
87
|
+
|| candidate.number <= 0
|
|
88
|
+
|| typeof candidate.html_url !== "string"
|
|
89
|
+
|| !isRecord(candidate.head)
|
|
90
|
+
|| candidate.head.ref !== branch
|
|
91
|
+
|| !isRecord(candidate.head.repo)
|
|
92
|
+
|| typeof candidate.head.repo.full_name !== "string"
|
|
93
|
+
|| candidate.head.repo.full_name.toLowerCase() !== fullName.toLowerCase()
|
|
94
|
+
|| !isRecord(candidate.base)
|
|
95
|
+
|| !isRecord(candidate.base.repo)
|
|
96
|
+
|| typeof candidate.base.repo.full_name !== "string"
|
|
97
|
+
|| candidate.base.repo.full_name.toLowerCase() !== fullName.toLowerCase())
|
|
98
|
+
continue;
|
|
99
|
+
const eligible = candidate.state === "open"
|
|
100
|
+
|| (candidate.state === "closed" && typeof candidate.merged_at === "string" && Number.isFinite(Date.parse(candidate.merged_at)));
|
|
101
|
+
if (!eligible)
|
|
102
|
+
continue;
|
|
103
|
+
const pullRequest = normalizePullRequest(candidate.number, candidate.html_url, expectedRepository);
|
|
104
|
+
if (pullRequest !== null)
|
|
105
|
+
matches.push(pullRequest);
|
|
106
|
+
}
|
|
107
|
+
return matches.length === 1 ? matches[0] : null;
|
|
108
|
+
}
|
|
109
|
+
function normalizePullRequest(number, value, expectedRepository) {
|
|
110
|
+
try {
|
|
111
|
+
const url = new URL(value);
|
|
112
|
+
if (url.protocol !== "https:"
|
|
113
|
+
|| url.hostname.toLowerCase() !== "github.com"
|
|
114
|
+
|| url.username !== ""
|
|
115
|
+
|| url.password !== ""
|
|
116
|
+
|| url.port !== ""
|
|
117
|
+
|| url.search !== ""
|
|
118
|
+
|| url.hash !== "")
|
|
119
|
+
return null;
|
|
120
|
+
const path = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/u.exec(url.pathname);
|
|
121
|
+
if (path === null
|
|
122
|
+
|| Number(path[3]) !== number
|
|
123
|
+
|| path[1].toLowerCase() !== expectedRepository.owner.toLowerCase()
|
|
124
|
+
|| path[2].toLowerCase() !== expectedRepository.repository.toLowerCase())
|
|
125
|
+
return null;
|
|
126
|
+
return { number, url: url.href.replace(/\/$/u, "") };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function normalizeRepository(ownerValue, repositoryValue) {
|
|
133
|
+
if (ownerValue === undefined || repositoryValue === undefined)
|
|
134
|
+
return null;
|
|
135
|
+
const repository = repositoryValue.replace(/\.git$/iu, "");
|
|
136
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/u.test(ownerValue)
|
|
137
|
+
|| !/^[A-Za-z0-9_.-]+$/u.test(repository)
|
|
138
|
+
|| repository === "."
|
|
139
|
+
|| repository === "..")
|
|
140
|
+
return null;
|
|
141
|
+
return { owner: ownerValue, repository };
|
|
142
|
+
}
|
|
143
|
+
function executeGitOriginUrl(cwd, signal) {
|
|
144
|
+
return new Promise(resolve => {
|
|
145
|
+
try {
|
|
146
|
+
execFile("git", ["config", "--get", "remote.origin.url"], { cwd, windowsHide: true, timeout: PROBE_TIMEOUT_MS, maxBuffer: COMMAND_MAX_BUFFER_BYTES, signal, encoding: "utf8" }, (error, stdout) => { resolve(error === null && typeof stdout === "string" ? stdout : null); });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
resolve(null);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function isRecord(value) {
|
|
154
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155
|
+
}
|
|
@@ -3,6 +3,14 @@ export interface PiPullRequestIdentity {
|
|
|
3
3
|
readonly url: string;
|
|
4
4
|
}
|
|
5
5
|
export type PiPullRequestProbe = (cwd: string, branch: string, signal: AbortSignal) => Promise<PiPullRequestIdentity | null>;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
type PiCommandReader = (cwd: string, signal: AbortSignal) => Promise<string | null>;
|
|
7
|
+
type PiPullRequestRestProbe = (cwd: string, branch: string, signal: AbortSignal, environment: NodeJS.ProcessEnv) => Promise<PiPullRequestIdentity | null>;
|
|
8
|
+
export interface PiPullRequestDiscoveryDependencies {
|
|
9
|
+
readonly executeGh?: PiCommandReader;
|
|
10
|
+
readonly restProbe?: PiPullRequestRestProbe;
|
|
11
|
+
}
|
|
12
|
+
/** Read an exact branch's open or merged PR, preferring GitHub CLI before the REST fallback. */
|
|
13
|
+
export declare function readPullRequest(cwd: string, branch: string, signal: AbortSignal, environment?: NodeJS.ProcessEnv, dependencies?: PiPullRequestDiscoveryDependencies): Promise<PiPullRequestIdentity | null>;
|
|
14
|
+
/** Parse and validate the normalized payload emitted by `gh pr view`. */
|
|
8
15
|
export declare function parsePullRequest(stdout: string, branch: string): PiPullRequestIdentity | null;
|
|
16
|
+
export {};
|
|
@@ -2,18 +2,24 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { PRODUCT_IDENTITY } from "../../../product-identity.js";
|
|
3
3
|
const GH_TIMEOUT_MS = 5_000;
|
|
4
4
|
const GH_MAX_BUFFER_BYTES = 64 * 1024;
|
|
5
|
-
/** Read an exact branch's open or merged PR. */
|
|
6
|
-
export async function readPullRequest(cwd, branch, signal, environment = process.env) {
|
|
5
|
+
/** Read an exact branch's open or merged PR, preferring GitHub CLI before the REST fallback. */
|
|
6
|
+
export async function readPullRequest(cwd, branch, signal, environment = process.env, dependencies = {}) {
|
|
7
7
|
if (branch.length === 0 || signal.aborted)
|
|
8
8
|
return null;
|
|
9
9
|
const preview = environment[PRODUCT_IDENTITY.environment.prFooterPreview];
|
|
10
10
|
if (preview !== undefined)
|
|
11
11
|
return parsePullRequest(preview, branch);
|
|
12
|
-
const stdout = await executeGh(cwd, signal);
|
|
13
|
-
if (stdout
|
|
12
|
+
const stdout = await (dependencies.executeGh ?? executeGh)(cwd, signal);
|
|
13
|
+
if (stdout !== null) {
|
|
14
|
+
const pullRequest = parsePullRequest(stdout, branch);
|
|
15
|
+
if (pullRequest !== null)
|
|
16
|
+
return pullRequest;
|
|
17
|
+
}
|
|
18
|
+
if (signal.aborted)
|
|
14
19
|
return null;
|
|
15
|
-
return
|
|
20
|
+
return (dependencies.restProbe ?? readPullRequestFromRest)(cwd, branch, signal, environment);
|
|
16
21
|
}
|
|
22
|
+
/** Parse and validate the normalized payload emitted by `gh pr view`. */
|
|
17
23
|
export function parsePullRequest(stdout, branch) {
|
|
18
24
|
let value;
|
|
19
25
|
try {
|
|
@@ -47,6 +53,17 @@ export function parsePullRequest(stdout, branch) {
|
|
|
47
53
|
return null;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
56
|
+
async function readPullRequestFromRest(cwd, branch, signal, environment) {
|
|
57
|
+
try {
|
|
58
|
+
const fallback = await import("./repository-pr-rest.js");
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
return null;
|
|
61
|
+
return fallback.readPullRequestFromGitHub(cwd, branch, signal, environment);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
50
67
|
function executeGh(cwd, signal) {
|
|
51
68
|
return new Promise(resolve => {
|
|
52
69
|
try {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-27T09:22:05.237Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-27T09:22:09.867Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-27T09:22:55.355Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "68d285c38302670a03c8b58d0e387f17614070bcdcfdb5d3cead3a47d1d19763",
|
|
12
12
|
"size": 178176
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
package/package.json
CHANGED