@tempo-ai/visual-review 0.0.1

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.
@@ -0,0 +1,146 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+ import {
3
+ DEFAULT_VISUAL_REVIEW_INGEST_URL
4
+ } from "./chunk-QLAJNWA2.js";
5
+ import {
6
+ githubPullRequestContext
7
+ } from "./chunk-W7PBID4V.js";
8
+
9
+ // src/claim.ts
10
+ import { randomUUID } from "crypto";
11
+ import { appendFile } from "fs/promises";
12
+ var PENDING_ENVIRONMENT = "pending";
13
+ var OIDC_AUDIENCE = "tempo-visual-review";
14
+ async function mintOidcToken(options) {
15
+ const separator = options.oidcRequestUrl.includes("?") ? "&" : "?";
16
+ const tokenResponse = await options.fetchImpl(
17
+ `${options.oidcRequestUrl}${separator}audience=${OIDC_AUDIENCE}`,
18
+ { headers: { Authorization: `Bearer ${options.oidcRequestToken}` } }
19
+ );
20
+ if (!tokenResponse.ok) {
21
+ throw new Error(`OIDC token request failed: ${tokenResponse.status}`);
22
+ }
23
+ const { value } = await tokenResponse.json();
24
+ if (!value) throw new Error("OIDC token response had no value");
25
+ return value;
26
+ }
27
+ async function postClaimRequest(options) {
28
+ const response = await options.fetchImpl(
29
+ `${options.ingestUrl.replace(/\/$/, "")}/visual-review/baseline/${options.path}`,
30
+ {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ "x-visual-review-oidc": options.oidcToken
35
+ },
36
+ body: JSON.stringify(options.body)
37
+ }
38
+ );
39
+ if (!response.ok) {
40
+ throw new Error(
41
+ `capture ${options.path} failed: ${response.status} ${(await response.text()).slice(0, 200)}`
42
+ );
43
+ }
44
+ return await response.json();
45
+ }
46
+ async function announceCapture(options) {
47
+ const ownerToken = options.ownerToken ?? randomUUID();
48
+ const fetchImpl = options.fetchImpl ?? fetch;
49
+ const oidcToken = await mintOidcToken({ ...options, fetchImpl });
50
+ const result = await postClaimRequest({
51
+ ingestUrl: options.ingestUrl,
52
+ oidcToken,
53
+ path: "claim",
54
+ body: {
55
+ repoKey: `github.com/${options.repository.toLowerCase()}`,
56
+ commitSha: options.headSha,
57
+ envFingerprint: PENDING_ENVIRONMENT,
58
+ ownerToken
59
+ },
60
+ fetchImpl
61
+ });
62
+ const status = typeof result.status === "string" ? result.status : void 0;
63
+ return {
64
+ ownerToken: status === "claimed" ? ownerToken : void 0,
65
+ status
66
+ };
67
+ }
68
+ async function releaseCaptureClaim(options) {
69
+ const fetchImpl = options.fetchImpl ?? fetch;
70
+ const oidcToken = await mintOidcToken({ ...options, fetchImpl });
71
+ const result = await postClaimRequest({
72
+ ingestUrl: options.ingestUrl,
73
+ oidcToken,
74
+ path: "fail",
75
+ body: {
76
+ repoKey: `github.com/${options.repository.toLowerCase()}`,
77
+ commitSha: options.headSha,
78
+ ownerToken: options.ownerToken,
79
+ error: "workflow ended before the capture claim completed"
80
+ },
81
+ fetchImpl
82
+ });
83
+ return { released: result.released === true };
84
+ }
85
+ function parseArgs(argv) {
86
+ if (argv.some((arg) => arg !== "--release")) {
87
+ throw new Error("claim accepts only the optional --release flag");
88
+ }
89
+ return { release: argv.includes("--release") };
90
+ }
91
+ async function main(argv = process.argv.slice(2)) {
92
+ const { release } = parseArgs(argv);
93
+ const pullRequest = githubPullRequestContext();
94
+ const required = {
95
+ ingestUrl: process.env.VISUAL_REVIEW_INGEST_URL ?? DEFAULT_VISUAL_REVIEW_INGEST_URL,
96
+ repository: process.env.GITHUB_REPOSITORY,
97
+ // GITHUB_SHA is a merge ref on pull_request runs; the event payload is
98
+ // the only standard environment source for the actual PR head SHA.
99
+ headSha: process.env.CAPTURE_HEAD_SHA ?? pullRequest?.headSha ?? process.env.GITHUB_SHA,
100
+ oidcRequestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
101
+ oidcRequestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
102
+ ...release ? { ownerToken: process.env.VISUAL_REVIEW_CAPTURE_OWNER_TOKEN } : {}
103
+ };
104
+ const missing = Object.entries(required).filter(([, value]) => !value).map(([name]) => name);
105
+ if (missing.length > 0) {
106
+ console.warn(
107
+ `capture announcement skipped; missing ${missing.join(", ")} (capture will claim after setup)`
108
+ );
109
+ return 0;
110
+ }
111
+ try {
112
+ if (release) {
113
+ const result2 = await releaseCaptureClaim(
114
+ required
115
+ );
116
+ console.log(
117
+ `capture claim release: ${result2.released ? "released" : "already complete or transferred"}`
118
+ );
119
+ return 0;
120
+ }
121
+ const result = await announceCapture(
122
+ required
123
+ );
124
+ console.log(`capture announcement: ${result.status}`);
125
+ if (result.ownerToken && process.env.GITHUB_ENV) {
126
+ await appendFile(
127
+ process.env.GITHUB_ENV,
128
+ `VISUAL_REVIEW_CAPTURE_OWNER_TOKEN=${result.ownerToken}
129
+ `
130
+ );
131
+ }
132
+ return 0;
133
+ } catch (error) {
134
+ console.warn(
135
+ `capture announcement failed; deferring to capture: ${error instanceof Error ? error.message : error}`
136
+ );
137
+ return 0;
138
+ }
139
+ }
140
+
141
+ export {
142
+ announceCapture,
143
+ releaseCaptureClaim,
144
+ main
145
+ };
146
+ //# sourceMappingURL=chunk-DJGBIRN5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/claim.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { appendFile } from \"node:fs/promises\";\n\nimport { DEFAULT_VISUAL_REVIEW_INGEST_URL } from \"./constants\";\nimport { githubPullRequestContext } from \"./github-actions\";\n\nconst PENDING_ENVIRONMENT = \"pending\";\nconst OIDC_AUDIENCE = \"tempo-visual-review\";\n\n/** The slice of `fetch` the claim protocol uses. A plain `(url, init)` mock\n * satisfies it without impersonating the overloaded global. */\ntype FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nasync function mintOidcToken(options: {\n oidcRequestUrl: string;\n oidcRequestToken: string;\n fetchImpl: FetchLike;\n}): Promise<string> {\n const separator = options.oidcRequestUrl.includes(\"?\") ? \"&\" : \"?\";\n const tokenResponse = await options.fetchImpl(\n `${options.oidcRequestUrl}${separator}audience=${OIDC_AUDIENCE}`,\n { headers: { Authorization: `Bearer ${options.oidcRequestToken}` } },\n );\n if (!tokenResponse.ok) {\n throw new Error(`OIDC token request failed: ${tokenResponse.status}`);\n }\n const { value } = (await tokenResponse.json()) as { value?: string };\n if (!value) throw new Error(\"OIDC token response had no value\");\n return value;\n}\n\nasync function postClaimRequest(options: {\n ingestUrl: string;\n oidcToken: string;\n path: \"claim\" | \"fail\";\n body: Record<string, string>;\n fetchImpl: FetchLike;\n}): Promise<Record<string, unknown>> {\n const response = await options.fetchImpl(\n `${options.ingestUrl.replace(/\\/$/, \"\")}/visual-review/baseline/${options.path}`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-visual-review-oidc\": options.oidcToken,\n },\n body: JSON.stringify(options.body),\n },\n );\n if (!response.ok) {\n throw new Error(\n `capture ${options.path} failed: ${response.status} ${(await response.text()).slice(0, 200)}`,\n );\n }\n return (await response.json()) as Record<string, unknown>;\n}\n\nexport async function announceCapture(options: {\n ingestUrl: string;\n repository: string;\n headSha: string;\n oidcRequestUrl: string;\n oidcRequestToken: string;\n ownerToken?: string;\n fetchImpl?: FetchLike;\n}): Promise<{ ownerToken?: string; status?: string }> {\n const ownerToken = options.ownerToken ?? randomUUID();\n const fetchImpl = options.fetchImpl ?? fetch;\n const oidcToken = await mintOidcToken({ ...options, fetchImpl });\n const result = await postClaimRequest({\n ingestUrl: options.ingestUrl,\n oidcToken,\n path: \"claim\",\n body: {\n repoKey: `github.com/${options.repository.toLowerCase()}`,\n commitSha: options.headSha,\n envFingerprint: PENDING_ENVIRONMENT,\n ownerToken,\n },\n fetchImpl,\n });\n const status = typeof result.status === \"string\" ? result.status : undefined;\n return {\n ownerToken: status === \"claimed\" ? ownerToken : undefined,\n status,\n };\n}\n\nexport async function releaseCaptureClaim(options: {\n ingestUrl: string;\n repository: string;\n headSha: string;\n ownerToken: string;\n oidcRequestUrl: string;\n oidcRequestToken: string;\n fetchImpl?: FetchLike;\n}): Promise<{ released?: boolean }> {\n const fetchImpl = options.fetchImpl ?? fetch;\n const oidcToken = await mintOidcToken({ ...options, fetchImpl });\n const result = await postClaimRequest({\n ingestUrl: options.ingestUrl,\n oidcToken,\n path: \"fail\",\n body: {\n repoKey: `github.com/${options.repository.toLowerCase()}`,\n commitSha: options.headSha,\n ownerToken: options.ownerToken,\n error: \"workflow ended before the capture claim completed\",\n },\n fetchImpl,\n });\n return { released: result.released === true };\n}\n\nfunction parseArgs(argv: string[]) {\n if (argv.some((arg) => arg !== \"--release\")) {\n throw new Error(\"claim accepts only the optional --release flag\");\n }\n return { release: argv.includes(\"--release\") };\n}\n\nexport async function main(argv = process.argv.slice(2)): Promise<number> {\n const { release } = parseArgs(argv);\n const pullRequest = githubPullRequestContext();\n const required = {\n ingestUrl:\n process.env.VISUAL_REVIEW_INGEST_URL ?? DEFAULT_VISUAL_REVIEW_INGEST_URL,\n repository: process.env.GITHUB_REPOSITORY,\n // GITHUB_SHA is a merge ref on pull_request runs; the event payload is\n // the only standard environment source for the actual PR head SHA.\n headSha:\n process.env.CAPTURE_HEAD_SHA ??\n pullRequest?.headSha ??\n process.env.GITHUB_SHA,\n oidcRequestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,\n oidcRequestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,\n ...(release\n ? { ownerToken: process.env.VISUAL_REVIEW_CAPTURE_OWNER_TOKEN }\n : {}),\n };\n const missing = Object.entries(required)\n .filter(([, value]) => !value)\n .map(([name]) => name);\n if (missing.length > 0) {\n console.warn(\n `capture announcement skipped; missing ${missing.join(\", \")} (capture will claim after setup)`,\n );\n return 0;\n }\n\n try {\n if (release) {\n const result = await releaseCaptureClaim(\n required as Parameters<typeof releaseCaptureClaim>[0],\n );\n console.log(\n `capture claim release: ${result.released ? \"released\" : \"already complete or transferred\"}`,\n );\n return 0;\n }\n const result = await announceCapture(\n required as Parameters<typeof announceCapture>[0],\n );\n console.log(`capture announcement: ${result.status}`);\n if (result.ownerToken && process.env.GITHUB_ENV) {\n await appendFile(\n process.env.GITHUB_ENV,\n `VISUAL_REVIEW_CAPTURE_OWNER_TOKEN=${result.ownerToken}\\n`,\n );\n }\n return 0;\n } catch (error) {\n // The capture command repeats the atomic claim with the real environment\n // fingerprint; an early announcement outage must not suppress coverage.\n console.warn(\n `capture announcement failed; deferring to capture: ${error instanceof Error ? error.message : error}`,\n );\n return 0;\n }\n}\n"],"mappings":"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","names":["result"]}
@@ -0,0 +1,42 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+
3
+ // src/install-browsers.ts
4
+ import { spawn } from "child_process";
5
+ import { createRequire } from "module";
6
+ import path from "path";
7
+ async function main(argv = process.argv.slice(2)) {
8
+ if (argv.length > 0) {
9
+ throw new Error("install-browsers accepts no arguments");
10
+ }
11
+ const require2 = createRequire(import.meta.url);
12
+ const cliPath = path.join(
13
+ path.dirname(require2.resolve("playwright/package.json")),
14
+ "cli.js"
15
+ );
16
+ const installArgs = ["install"];
17
+ if (process.platform === "linux") installArgs.push("--with-deps");
18
+ installArgs.push("chromium");
19
+ console.log(`[visual-review] playwright ${installArgs.join(" ")}`);
20
+ return await new Promise((resolve, reject) => {
21
+ const child = spawn(process.execPath, [cliPath, ...installArgs], {
22
+ stdio: "inherit",
23
+ windowsHide: true
24
+ });
25
+ child.once("error", reject);
26
+ child.once("exit", (code, signal) => {
27
+ if (code === 0) resolve(0);
28
+ else {
29
+ reject(
30
+ new Error(
31
+ `playwright ${installArgs.join(" ")} exited ${code ?? `from signal ${signal ?? "unknown"}`}`
32
+ )
33
+ );
34
+ }
35
+ });
36
+ });
37
+ }
38
+
39
+ export {
40
+ main
41
+ };
42
+ //# sourceMappingURL=chunk-EYRAT56C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/install-browsers.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\n/**\n * `tempo-visual-review install-browsers` — install the Chromium build that\n * matches the playwright THIS package depends on.\n *\n * A bare `npx playwright install` resolves registry-latest playwright and can\n * fetch a browser revision the pinned driver cannot launch; resolving the\n * installer from our own dependency tree removes that drift for every\n * consumer of the generated workflow. Linux adds `--with-deps` (Chromium's\n * shared libraries via apt) because CI VMs are ephemeral; other platforms\n * already carry those libraries.\n */\nexport async function main(argv = process.argv.slice(2)): Promise<number> {\n if (argv.length > 0) {\n throw new Error(\"install-browsers accepts no arguments\");\n }\n const require = createRequire(import.meta.url);\n const cliPath = path.join(\n path.dirname(require.resolve(\"playwright/package.json\")),\n \"cli.js\",\n );\n const installArgs = [\"install\"];\n if (process.platform === \"linux\") installArgs.push(\"--with-deps\");\n installArgs.push(\"chromium\");\n console.log(`[visual-review] playwright ${installArgs.join(\" \")}`);\n return await new Promise<number>((resolve, reject) => {\n const child = spawn(process.execPath, [cliPath, ...installArgs], {\n stdio: \"inherit\",\n windowsHide: true,\n });\n child.once(\"error\", reject);\n child.once(\"exit\", (code, signal) => {\n if (code === 0) resolve(0);\n else {\n reject(\n new Error(\n `playwright ${installArgs.join(\" \")} exited ${code ?? `from signal ${signal ?? \"unknown\"}`}`,\n ),\n );\n }\n });\n });\n}\n"],"mappings":";;;AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AAajB,eAAsB,KAAK,OAAO,QAAQ,KAAK,MAAM,CAAC,GAAoB;AACxE,MAAI,KAAK,SAAS,GAAG;AACnB,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AACA,QAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,QAAM,UAAU,KAAK;AAAA,IACnB,KAAK,QAAQA,SAAQ,QAAQ,yBAAyB,CAAC;AAAA,IACvD;AAAA,EACF;AACA,QAAM,cAAc,CAAC,SAAS;AAC9B,MAAI,QAAQ,aAAa,QAAS,aAAY,KAAK,aAAa;AAChE,cAAY,KAAK,UAAU;AAC3B,UAAQ,IAAI,8BAA8B,YAAY,KAAK,GAAG,CAAC,EAAE;AACjE,SAAO,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AACpD,UAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,SAAS,GAAG,WAAW,GAAG;AAAA,MAC/D,OAAO;AAAA,MACP,aAAa;AAAA,IACf,CAAC;AACD,UAAM,KAAK,SAAS,MAAM;AAC1B,UAAM,KAAK,QAAQ,CAAC,MAAM,WAAW;AACnC,UAAI,SAAS,EAAG,SAAQ,CAAC;AAAA,WACpB;AACH;AAAA,UACE,IAAI;AAAA,YACF,cAAc,YAAY,KAAK,GAAG,CAAC,WAAW,QAAQ,eAAe,UAAU,SAAS,EAAE;AAAA,UAC5F;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;","names":["require"]}
@@ -0,0 +1,288 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+ import {
3
+ visualReviewPrDeclaration
4
+ } from "./chunk-W7PBID4V.js";
5
+
6
+ // src/report.ts
7
+ import { execFileSync } from "child_process";
8
+ var STICKY_MARKER = "<!-- tempo-visual-review -->";
9
+ var STATUS_CONTEXT = "visual-review";
10
+ function parseArgs(argv) {
11
+ const args = {
12
+ pollTimeoutS: 900,
13
+ bypass: false,
14
+ noVisualChange: false,
15
+ fallbackFailure: false
16
+ };
17
+ for (let index = 0; index < argv.length; index += 1) {
18
+ const flag = argv[index];
19
+ const next = () => {
20
+ const value = argv[++index];
21
+ if (value === void 0) throw new Error(`${flag} needs a value`);
22
+ return value;
23
+ };
24
+ if (flag === "--status-url") args.statusUrl = next();
25
+ else if (flag === "--pr") args.pr = Number(next());
26
+ else if (flag === "--sha") args.sha = next();
27
+ else if (flag === "--poll-timeout-s") args.pollTimeoutS = Number(next());
28
+ else if (flag === "--bypass") args.bypass = true;
29
+ else if (flag === "--no-visual-change") args.noVisualChange = true;
30
+ else if (flag === "--fallback-failure") args.fallbackFailure = true;
31
+ else throw new Error(`unknown flag ${flag}`);
32
+ }
33
+ if (!args.sha) throw new Error("--sha is required");
34
+ return args;
35
+ }
36
+ function gh(...ghArgs) {
37
+ return execFileSync("gh", ghArgs, { encoding: "utf-8" });
38
+ }
39
+ function postCommitStatus(repo, sha, state, description, targetUrl) {
40
+ gh(
41
+ "api",
42
+ `repos/${repo}/statuses/${sha}`,
43
+ "-f",
44
+ `state=${state}`,
45
+ "-f",
46
+ `context=${STATUS_CONTEXT}`,
47
+ "-f",
48
+ `description=${description.slice(0, 140)}`,
49
+ ...targetUrl ? ["-f", `target_url=${targetUrl}`] : []
50
+ );
51
+ }
52
+ function upsertStickyComment(repo, pr, body) {
53
+ const comments = JSON.parse(
54
+ gh("api", `repos/${repo}/issues/${pr}/comments`, "--paginate", "--slurp")
55
+ ).flat();
56
+ const existing = comments.find(
57
+ (comment) => typeof comment.body === "string" && comment.body.includes(STICKY_MARKER)
58
+ );
59
+ if (existing) {
60
+ gh(
61
+ "api",
62
+ "--method",
63
+ "PATCH",
64
+ `repos/${repo}/issues/comments/${existing.id}`,
65
+ "-f",
66
+ `body=${body}`
67
+ );
68
+ } else {
69
+ gh("api", `repos/${repo}/issues/${pr}/comments`, "-f", `body=${body}`);
70
+ }
71
+ }
72
+ function describeGate(gate, state) {
73
+ if (state === "error") {
74
+ return {
75
+ state: "failure",
76
+ description: "visual review run failed \u2014 see report"
77
+ };
78
+ }
79
+ if (state !== "complete") {
80
+ return {
81
+ state: "pending",
82
+ description: "capturing & comparing storyboards\u2026"
83
+ };
84
+ }
85
+ if (gate.satisfied) {
86
+ return {
87
+ state: "success",
88
+ description: gate.required === 0 ? "no visual changes" : `${gate.required} visual change${gate.required === 1 ? "" : "s"} approved`
89
+ };
90
+ }
91
+ const parts = [];
92
+ if ((gate.approvalsNeeded ?? 0) > 0) {
93
+ parts.push(`${gate.approvalsNeeded} of ${gate.required} need approval`);
94
+ }
95
+ if ((gate.rejected ?? 0) > 0) parts.push(`${gate.rejected} rejected`);
96
+ if ((gate.blockingErrors ?? 0) > 0) {
97
+ parts.push(
98
+ `${gate.blockingErrors} render error${gate.blockingErrors === 1 ? "" : "s"}`
99
+ );
100
+ }
101
+ return {
102
+ state: "failure",
103
+ description: `visual review: ${parts.join(", ")} \u2014 see report`
104
+ };
105
+ }
106
+ function buildCommentBody(status) {
107
+ const counts = status.counts ?? {};
108
+ const gateLine = status.gate?.satisfied ? "\u2705 **Check passing** \u2014 every visual change is approved." : `\u{1F534} **Check failing** \u2014 ${status.gate?.approvalsNeeded ?? "?"} of ${status.gate?.required ?? "?"} storyboards need approval${status.gate?.blockingErrors ? `, ${status.gate.blockingErrors} render error(s)` : ""}${status.gate?.rejected ? `, ${status.gate.rejected} rejected` : ""}. Approving in the report flips the check instantly \u2014 no CI re-run.`;
109
+ const summary = `| tracked | changed | new | removed | render errors | unchanged |
110
+ | --- | --- | --- | --- | --- | --- |
111
+ | ${status.trackedTotal ?? 0} | ${counts.changed ?? 0} | ${counts.added ?? 0} | ${counts.removed ?? 0} | ${counts.errors ?? 0} | ${counts.unchanged ?? 0} |`;
112
+ const thumbs = (status.topChanged ?? []).slice(0, 6).map((board) => {
113
+ const title = board.name ?? board.storyboardId;
114
+ const meta = board.diffRatio !== void 0 && board.diffRatio !== null ? ` \xB7 ${(board.diffRatio * 100).toFixed(2)}%` : "";
115
+ if (board.verdict === "render-error") {
116
+ return `**${title}** \u2014 render error${board.error ? `: \`${board.error.slice(0, 120)}\`` : ""}`;
117
+ }
118
+ const cell = (hash, label) => hash && status.blobUrlTemplate ? `<img src="${status.blobUrlTemplate}${hash}" alt="${label}" width="360" />` : "\u2014";
119
+ return `<details><summary><b>${title}</b> \u2014 ${board.verdict}${meta}</summary>
120
+
121
+ | baseline | candidate |
122
+ | --- | --- |
123
+ | ${cell(board.baselineHash, "baseline")} | ${cell(board.candidateHash, "candidate")} |
124
+
125
+ </details>`;
126
+ }).join("\n");
127
+ const notes = [];
128
+ if (status.approximateBase) {
129
+ notes.push(
130
+ "\u26A0 exact merge-base had no visual run \u2014 compared against the nearest captured ancestor."
131
+ );
132
+ }
133
+ if (status.freshEpoch) {
134
+ notes.push(
135
+ "\u2139 fresh baseline epoch (capture environment changed) \u2014 everything reports as new."
136
+ );
137
+ }
138
+ return `${STICKY_MARKER}
139
+ ## \u{1F441} Visual review
140
+
141
+ ${gateLine}
142
+
143
+ ${summary}
144
+
145
+ ${notes.length > 0 ? `${notes.join("\n")}
146
+ ` : ""}${thumbs}
147
+
148
+ \u{1F4F7} **[Open the visual review report](${status.reportUrl})** \u2014 side-by-side, onion-skin and diff views; approve, reject, and comment there.`;
149
+ }
150
+ function buildNoVisualChangeComment() {
151
+ return `${STICKY_MARKER}
152
+ ## \u{1F441} Visual review
153
+
154
+ \u2705 **Not applicable \u2014 no storyboard changes expected.** Storyboard capture was skipped from the PR declaration.`;
155
+ }
156
+ function latestContextState(repo, sha) {
157
+ const statuses = JSON.parse(
158
+ gh(
159
+ "api",
160
+ `repos/${repo}/commits/${sha}/statuses`,
161
+ "--paginate",
162
+ "--slurp"
163
+ )
164
+ ).flat();
165
+ return statuses.find((status) => status.context === STATUS_CONTEXT)?.state ?? null;
166
+ }
167
+ function isPermanentError(error) {
168
+ return error instanceof Error && "permanent" in error && error.permanent === true;
169
+ }
170
+ async function main(argv = process.argv.slice(2)) {
171
+ const args = parseArgs(argv);
172
+ const repo = process.env.GITHUB_REPOSITORY;
173
+ if (!repo) throw new Error("GITHUB_REPOSITORY is not set");
174
+ const sha = args.sha;
175
+ if (!sha) throw new Error("--sha is required");
176
+ const declaration = visualReviewPrDeclaration();
177
+ if (args.bypass || declaration.bypass) {
178
+ postCommitStatus(
179
+ repo,
180
+ sha,
181
+ "success",
182
+ "bypassed via visual-review:bypass label"
183
+ );
184
+ console.log("bypass label present \u2014 posted success and skipped the report");
185
+ return 0;
186
+ }
187
+ if (args.noVisualChange || declaration.noVisualChange) {
188
+ postCommitStatus(
189
+ repo,
190
+ sha,
191
+ "success",
192
+ "not applicable \u2014 no storyboard changes expected"
193
+ );
194
+ if (args.pr)
195
+ upsertStickyComment(repo, args.pr, buildNoVisualChangeComment());
196
+ console.log(
197
+ "no-visual-change declaration present \u2014 posted success and skipped capture"
198
+ );
199
+ return 0;
200
+ }
201
+ if (args.fallbackFailure) {
202
+ const current = latestContextState(repo, sha);
203
+ if (current === null || current === "pending") {
204
+ postCommitStatus(
205
+ repo,
206
+ sha,
207
+ "failure",
208
+ "visual review reporting failed \u2014 see workflow logs"
209
+ );
210
+ console.log(
211
+ `reporting fallback: latest state was ${current ?? "absent"} \u2014 posted failure`
212
+ );
213
+ } else {
214
+ console.log(
215
+ `reporting fallback: latest '${STATUS_CONTEXT}' state is '${current}' \u2014 leaving it`
216
+ );
217
+ }
218
+ return 0;
219
+ }
220
+ const statusUrl = args.statusUrl;
221
+ if (!statusUrl) throw new Error("--status-url is required");
222
+ const deadline = Date.now() + args.pollTimeoutS * 1e3;
223
+ let status;
224
+ for (; ; ) {
225
+ try {
226
+ const response = await fetch(statusUrl);
227
+ if (response.status === 404) {
228
+ const error = new Error("status poll 404 \u2014 wrong report key");
229
+ Object.assign(error, { permanent: true });
230
+ throw error;
231
+ }
232
+ if (!response.ok)
233
+ throw new Error(`status poll failed: ${response.status}`);
234
+ status = await response.json();
235
+ } catch (error) {
236
+ if (isPermanentError(error)) throw error;
237
+ console.warn(
238
+ `transient status poll failure, retrying: ${error instanceof Error ? error.message : error}`
239
+ );
240
+ }
241
+ if (status && (status.state === "complete" || status.state === "error")) {
242
+ break;
243
+ }
244
+ if (Date.now() > deadline) {
245
+ postCommitStatus(
246
+ repo,
247
+ sha,
248
+ "failure",
249
+ "visual review timed out waiting for compare \u2014 see report",
250
+ status?.reportUrl
251
+ );
252
+ throw new Error("timed out waiting for the run to complete");
253
+ }
254
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
255
+ }
256
+ const verdict = describeGate(status.gate ?? {}, status.state);
257
+ const publishError = status.lastPublishError;
258
+ const publishFailing = publishError !== void 0 && publishError.revision >= (status.lastPublish?.revision ?? 0) && status.statusPublishing === false;
259
+ if (status.publisherConfigured && !publishFailing) {
260
+ console.log(
261
+ `verdict (${verdict.state}) owned by the Convex publisher \u2014 skipping CI status write`
262
+ );
263
+ } else {
264
+ if (publishFailing) {
265
+ console.log(
266
+ `Convex publisher reported a delivery failure (${publishError.error}) \u2014 writing the status from CI as a fallback`
267
+ );
268
+ }
269
+ postCommitStatus(
270
+ repo,
271
+ sha,
272
+ verdict.state,
273
+ verdict.description,
274
+ status.reportUrl
275
+ );
276
+ console.log(`commit status: ${verdict.state} \u2014 ${verdict.description}`);
277
+ }
278
+ if (args.pr) {
279
+ upsertStickyComment(repo, args.pr, buildCommentBody(status));
280
+ console.log(`sticky comment upserted on #${args.pr}`);
281
+ }
282
+ return 0;
283
+ }
284
+
285
+ export {
286
+ main
287
+ };
288
+ //# sourceMappingURL=chunk-HMPQK5ZG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/report.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\n\nimport { visualReviewPrDeclaration } from \"./github-actions\";\n\nconst STICKY_MARKER = \"<!-- tempo-visual-review -->\";\nconst STATUS_CONTEXT = \"visual-review\";\n\ntype ReportArgs = {\n pollTimeoutS: number;\n bypass: boolean;\n noVisualChange: boolean;\n fallbackFailure: boolean;\n statusUrl?: string;\n pr?: number;\n sha?: string;\n};\n\ntype Gate = {\n satisfied?: boolean;\n required?: number;\n approvalsNeeded?: number;\n rejected?: number;\n blockingErrors?: number;\n};\n\ntype ReportStatus = {\n state?: \"complete\" | \"error\" | string;\n reportUrl?: string;\n trackedTotal?: number;\n counts?: Record<string, number | undefined>;\n gate?: Gate;\n topChanged?: Array<{\n name?: string;\n storyboardId: string;\n diffRatio?: number | null;\n verdict: string;\n error?: string;\n baselineHash?: string;\n candidateHash?: string;\n }>;\n blobUrlTemplate?: string;\n approximateBase?: boolean;\n freshEpoch?: boolean;\n publisherConfigured?: boolean;\n lastPublishError?: { revision: number; error: string };\n lastPublish?: { revision: number };\n statusPublishing?: boolean;\n};\n\nfunction parseArgs(argv: string[]): ReportArgs {\n const args: ReportArgs = {\n pollTimeoutS: 900,\n bypass: false,\n noVisualChange: false,\n fallbackFailure: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index];\n const next = () => {\n const value = argv[++index];\n if (value === undefined) throw new Error(`${flag} needs a value`);\n return value;\n };\n if (flag === \"--status-url\") args.statusUrl = next();\n else if (flag === \"--pr\") args.pr = Number(next());\n else if (flag === \"--sha\") args.sha = next();\n else if (flag === \"--poll-timeout-s\") args.pollTimeoutS = Number(next());\n else if (flag === \"--bypass\") args.bypass = true;\n else if (flag === \"--no-visual-change\") args.noVisualChange = true;\n else if (flag === \"--fallback-failure\") args.fallbackFailure = true;\n else throw new Error(`unknown flag ${flag}`);\n }\n if (!args.sha) throw new Error(\"--sha is required\");\n return args;\n}\n\nfunction gh(...ghArgs: string[]): string {\n return execFileSync(\"gh\", ghArgs, { encoding: \"utf-8\" });\n}\n\nfunction postCommitStatus(\n repo: string,\n sha: string,\n state: \"success\" | \"failure\" | \"pending\",\n description: string,\n targetUrl?: string,\n): void {\n gh(\n \"api\",\n `repos/${repo}/statuses/${sha}`,\n \"-f\",\n `state=${state}`,\n \"-f\",\n `context=${STATUS_CONTEXT}`,\n \"-f\",\n `description=${description.slice(0, 140)}`,\n ...(targetUrl ? [\"-f\", `target_url=${targetUrl}`] : []),\n );\n}\n\nfunction upsertStickyComment(repo: string, pr: number, body: string): void {\n const comments = (\n JSON.parse(\n gh(\"api\", `repos/${repo}/issues/${pr}/comments`, \"--paginate\", \"--slurp\"),\n ) as Array<Array<{ id: number; body?: string }>>\n ).flat();\n const existing = comments.find(\n (comment) =>\n typeof comment.body === \"string\" && comment.body.includes(STICKY_MARKER),\n );\n if (existing) {\n gh(\n \"api\",\n \"--method\",\n \"PATCH\",\n `repos/${repo}/issues/comments/${existing.id}`,\n \"-f\",\n `body=${body}`,\n );\n } else {\n gh(\"api\", `repos/${repo}/issues/${pr}/comments`, \"-f\", `body=${body}`);\n }\n}\n\nfunction describeGate(\n gate: Gate,\n state: ReportStatus[\"state\"],\n): { state: \"success\" | \"failure\" | \"pending\"; description: string } {\n if (state === \"error\") {\n return {\n state: \"failure\",\n description: \"visual review run failed — see report\",\n };\n }\n if (state !== \"complete\") {\n return {\n state: \"pending\",\n description: \"capturing & comparing storyboards…\",\n };\n }\n if (gate.satisfied) {\n return {\n state: \"success\",\n description:\n gate.required === 0\n ? \"no visual changes\"\n : `${gate.required} visual change${gate.required === 1 ? \"\" : \"s\"} approved`,\n };\n }\n const parts: string[] = [];\n if ((gate.approvalsNeeded ?? 0) > 0) {\n parts.push(`${gate.approvalsNeeded} of ${gate.required} need approval`);\n }\n if ((gate.rejected ?? 0) > 0) parts.push(`${gate.rejected} rejected`);\n if ((gate.blockingErrors ?? 0) > 0) {\n parts.push(\n `${gate.blockingErrors} render error${gate.blockingErrors === 1 ? \"\" : \"s\"}`,\n );\n }\n return {\n state: \"failure\",\n description: `visual review: ${parts.join(\", \")} — see report`,\n };\n}\n\nfunction buildCommentBody(status: ReportStatus): string {\n const counts = status.counts ?? {};\n const gateLine = status.gate?.satisfied\n ? \"✅ **Check passing** — every visual change is approved.\"\n : `🔴 **Check failing** — ${status.gate?.approvalsNeeded ?? \"?\"} of ${status.gate?.required ?? \"?\"} storyboards need approval${status.gate?.blockingErrors ? `, ${status.gate.blockingErrors} render error(s)` : \"\"}${status.gate?.rejected ? `, ${status.gate.rejected} rejected` : \"\"}. Approving in the report flips the check instantly — no CI re-run.`;\n const summary = `| tracked | changed | new | removed | render errors | unchanged |\n| --- | --- | --- | --- | --- | --- |\n| ${status.trackedTotal ?? 0} | ${counts.changed ?? 0} | ${counts.added ?? 0} | ${counts.removed ?? 0} | ${counts.errors ?? 0} | ${counts.unchanged ?? 0} |`;\n\n const thumbs = (status.topChanged ?? [])\n .slice(0, 6)\n .map((board) => {\n const title = board.name ?? board.storyboardId;\n const meta =\n board.diffRatio !== undefined && board.diffRatio !== null\n ? ` · ${(board.diffRatio * 100).toFixed(2)}%`\n : \"\";\n if (board.verdict === \"render-error\") {\n return `**${title}** — render error${board.error ? `: \\`${board.error.slice(0, 120)}\\`` : \"\"}`;\n }\n const cell = (hash: string | undefined, label: string) =>\n hash && status.blobUrlTemplate\n ? `<img src=\"${status.blobUrlTemplate}${hash}\" alt=\"${label}\" width=\"360\" />`\n : \"—\";\n return `<details><summary><b>${title}</b> — ${board.verdict}${meta}</summary>\n\n| baseline | candidate |\n| --- | --- |\n| ${cell(board.baselineHash, \"baseline\")} | ${cell(board.candidateHash, \"candidate\")} |\n\n</details>`;\n })\n .join(\"\\n\");\n\n const notes: string[] = [];\n if (status.approximateBase) {\n notes.push(\n \"⚠ exact merge-base had no visual run — compared against the nearest captured ancestor.\",\n );\n }\n if (status.freshEpoch) {\n notes.push(\n \"ℹ fresh baseline epoch (capture environment changed) — everything reports as new.\",\n );\n }\n\n return `${STICKY_MARKER}\n## 👁 Visual review\n\n${gateLine}\n\n${summary}\n\n${notes.length > 0 ? `${notes.join(\"\\n\")}\\n` : \"\"}${thumbs}\n\n📷 **[Open the visual review report](${status.reportUrl})** — side-by-side, onion-skin and diff views; approve, reject, and comment there.`;\n}\n\nfunction buildNoVisualChangeComment(): string {\n return `${STICKY_MARKER}\n## 👁 Visual review\n\n✅ **Not applicable — no storyboard changes expected.** Storyboard capture was skipped from the PR declaration.`;\n}\n\nfunction latestContextState(repo: string, sha: string): string | null {\n const statuses = (\n JSON.parse(\n gh(\n \"api\",\n `repos/${repo}/commits/${sha}/statuses`,\n \"--paginate\",\n \"--slurp\",\n ),\n ) as Array<Array<{ context?: string; state?: string }>>\n ).flat();\n return (\n statuses.find((status) => status.context === STATUS_CONTEXT)?.state ?? null\n );\n}\n\nfunction isPermanentError(error: unknown): boolean {\n return (\n error instanceof Error && \"permanent\" in error && error.permanent === true\n );\n}\n\nexport async function main(argv = process.argv.slice(2)): Promise<number> {\n const args = parseArgs(argv);\n const repo = process.env.GITHUB_REPOSITORY;\n if (!repo) throw new Error(\"GITHUB_REPOSITORY is not set\");\n const sha = args.sha;\n if (!sha) throw new Error(\"--sha is required\");\n const declaration = visualReviewPrDeclaration();\n\n if (args.bypass || declaration.bypass) {\n postCommitStatus(\n repo,\n sha,\n \"success\",\n \"bypassed via visual-review:bypass label\",\n );\n console.log(\"bypass label present — posted success and skipped the report\");\n return 0;\n }\n\n if (args.noVisualChange || declaration.noVisualChange) {\n postCommitStatus(\n repo,\n sha,\n \"success\",\n \"not applicable — no storyboard changes expected\",\n );\n if (args.pr)\n upsertStickyComment(repo, args.pr, buildNoVisualChangeComment());\n console.log(\n \"no-visual-change declaration present — posted success and skipped capture\",\n );\n return 0;\n }\n\n if (args.fallbackFailure) {\n const current = latestContextState(repo, sha);\n if (current === null || current === \"pending\") {\n postCommitStatus(\n repo,\n sha,\n \"failure\",\n \"visual review reporting failed — see workflow logs\",\n );\n console.log(\n `reporting fallback: latest state was ${current ?? \"absent\"} — posted failure`,\n );\n } else {\n console.log(\n `reporting fallback: latest '${STATUS_CONTEXT}' state is '${current}' — leaving it`,\n );\n }\n return 0;\n }\n\n const statusUrl = args.statusUrl;\n if (!statusUrl) throw new Error(\"--status-url is required\");\n const deadline = Date.now() + args.pollTimeoutS * 1_000;\n let status: ReportStatus | undefined;\n for (;;) {\n try {\n const response = await fetch(statusUrl);\n if (response.status === 404) {\n const error = new Error(\"status poll 404 — wrong report key\");\n Object.assign(error, { permanent: true });\n throw error;\n }\n if (!response.ok)\n throw new Error(`status poll failed: ${response.status}`);\n status = (await response.json()) as ReportStatus;\n } catch (error) {\n if (isPermanentError(error)) throw error;\n console.warn(\n `transient status poll failure, retrying: ${error instanceof Error ? error.message : error}`,\n );\n }\n if (status && (status.state === \"complete\" || status.state === \"error\")) {\n break;\n }\n if (Date.now() > deadline) {\n postCommitStatus(\n repo,\n sha,\n \"failure\",\n \"visual review timed out waiting for compare — see report\",\n status?.reportUrl,\n );\n throw new Error(\"timed out waiting for the run to complete\");\n }\n await new Promise((resolve) => setTimeout(resolve, 5_000));\n }\n\n const verdict = describeGate(status.gate ?? {}, status.state);\n const publishError = status.lastPublishError;\n const publishFailing =\n publishError !== undefined &&\n publishError.revision >= (status.lastPublish?.revision ?? 0) &&\n status.statusPublishing === false;\n if (status.publisherConfigured && !publishFailing) {\n console.log(\n `verdict (${verdict.state}) owned by the Convex publisher — skipping CI status write`,\n );\n } else {\n if (publishFailing) {\n console.log(\n `Convex publisher reported a delivery failure (${publishError.error}) — writing the status from CI as a fallback`,\n );\n }\n postCommitStatus(\n repo,\n sha,\n verdict.state,\n verdict.description,\n status.reportUrl,\n );\n console.log(`commit status: ${verdict.state} — ${verdict.description}`);\n }\n\n if (args.pr) {\n upsertStickyComment(repo, args.pr, buildCommentBody(status));\n console.log(`sticky comment upserted on #${args.pr}`);\n }\n return 0;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,9 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+
3
+ // src/constants.ts
4
+ var DEFAULT_VISUAL_REVIEW_INGEST_URL = "https://greedy-jackal-526.convex.site";
5
+
6
+ export {
7
+ DEFAULT_VISUAL_REVIEW_INGEST_URL
8
+ };
9
+ //# sourceMappingURL=chunk-QLAJNWA2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/** The production Convex HTTP origin customers ingest visual-review runs to. */\nexport const DEFAULT_VISUAL_REVIEW_INGEST_URL =\n \"https://greedy-jackal-526.convex.site\";\n"],"mappings":";;;AACO,IAAM,mCACX;","names":[]}
@@ -0,0 +1,38 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+
3
+ // src/github-actions.ts
4
+ import { readFileSync } from "fs";
5
+ function readGithubActionsEvent(eventPath = process.env.GITHUB_EVENT_PATH) {
6
+ if (!eventPath) return null;
7
+ try {
8
+ return JSON.parse(readFileSync(eventPath, "utf8"));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ function githubPullRequestContext(event = readGithubActionsEvent()) {
14
+ const pullRequest = event?.pull_request;
15
+ if (!pullRequest) return null;
16
+ return {
17
+ number: pullRequest.number ?? event?.number,
18
+ headRef: pullRequest.head?.ref,
19
+ headSha: pullRequest.head?.sha,
20
+ baseRef: pullRequest.base?.ref,
21
+ baseSha: pullRequest.base?.sha
22
+ };
23
+ }
24
+ function visualReviewPrDeclaration(event = readGithubActionsEvent()) {
25
+ const pullRequest = event?.pull_request;
26
+ return {
27
+ bypass: pullRequest?.labels?.some(
28
+ (label) => label.name === "visual-review:bypass"
29
+ ) ?? false,
30
+ noVisualChange: pullRequest?.body?.includes("<!-- tempo-no-visual-change -->") ?? false
31
+ };
32
+ }
33
+
34
+ export {
35
+ githubPullRequestContext,
36
+ visualReviewPrDeclaration
37
+ };
38
+ //# sourceMappingURL=chunk-W7PBID4V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/github-actions.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\n\nexport type GithubActionsEvent = {\n number?: number;\n pull_request?: {\n number?: number;\n body?: string | null;\n labels?: Array<{ name?: string | null }>;\n head?: { ref?: string; sha?: string };\n base?: { ref?: string; sha?: string };\n };\n};\n\n/** Read Actions' event payload when it is available, without making a local\n * invocation depend on it. */\nexport function readGithubActionsEvent(\n eventPath = process.env.GITHUB_EVENT_PATH,\n): GithubActionsEvent | null {\n if (!eventPath) return null;\n try {\n return JSON.parse(readFileSync(eventPath, \"utf8\")) as GithubActionsEvent;\n } catch {\n return null;\n }\n}\n\nexport function githubPullRequestContext(event = readGithubActionsEvent()) {\n const pullRequest = event?.pull_request;\n if (!pullRequest) return null;\n return {\n number: pullRequest.number ?? event?.number,\n headRef: pullRequest.head?.ref,\n headSha: pullRequest.head?.sha,\n baseRef: pullRequest.base?.ref,\n baseSha: pullRequest.base?.sha,\n };\n}\n\n/** These PR-owned declarations are handled by the portable `report` command,\n * keeping customer workflow files free of dogfood-specific bypass jobs. */\nexport function visualReviewPrDeclaration(\n event: GithubActionsEvent | null = readGithubActionsEvent(),\n): { bypass: boolean; noVisualChange: boolean } {\n const pullRequest = event?.pull_request;\n return {\n bypass:\n pullRequest?.labels?.some(\n (label) => label.name === \"visual-review:bypass\",\n ) ?? false,\n noVisualChange:\n pullRequest?.body?.includes(\"<!-- tempo-no-visual-change -->\") ?? false,\n };\n}\n"],"mappings":";;;AAAA,SAAS,oBAAoB;AAetB,SAAS,uBACd,YAAY,QAAQ,IAAI,mBACG;AAC3B,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,WAAW,MAAM,CAAC;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,yBAAyB,QAAQ,uBAAuB,GAAG;AACzE,QAAM,cAAc,OAAO;AAC3B,MAAI,CAAC,YAAa,QAAO;AACzB,SAAO;AAAA,IACL,QAAQ,YAAY,UAAU,OAAO;AAAA,IACrC,SAAS,YAAY,MAAM;AAAA,IAC3B,SAAS,YAAY,MAAM;AAAA,IAC3B,SAAS,YAAY,MAAM;AAAA,IAC3B,SAAS,YAAY,MAAM;AAAA,EAC7B;AACF;AAIO,SAAS,0BACd,QAAmC,uBAAuB,GACZ;AAC9C,QAAM,cAAc,OAAO;AAC3B,SAAO;AAAA,IACL,QACE,aAAa,QAAQ;AAAA,MACnB,CAAC,UAAU,MAAM,SAAS;AAAA,IAC5B,KAAK;AAAA,IACP,gBACE,aAAa,MAAM,SAAS,iCAAiC,KAAK;AAAA,EACtE;AACF;","names":[]}
@@ -0,0 +1,14 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+ import {
3
+ announceCapture,
4
+ main,
5
+ releaseCaptureClaim
6
+ } from "./chunk-DJGBIRN5.js";
7
+ import "./chunk-QLAJNWA2.js";
8
+ import "./chunk-W7PBID4V.js";
9
+ export {
10
+ announceCapture,
11
+ main,
12
+ releaseCaptureClaim
13
+ };
14
+ //# sourceMappingURL=claim-WI2ZVIDS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,16 @@
1
+ import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
2
+ import {
3
+ VISUAL_REVIEW_OIDC_AUDIENCE,
4
+ buildBaselineCaptureArgs,
5
+ main,
6
+ resolveOrClaimBaseline
7
+ } from "./chunk-4QAZFBC3.js";
8
+ import "./chunk-QLAJNWA2.js";
9
+ import "./chunk-W7PBID4V.js";
10
+ export {
11
+ VISUAL_REVIEW_OIDC_AUDIENCE,
12
+ buildBaselineCaptureArgs,
13
+ main,
14
+ resolveOrClaimBaseline
15
+ };
16
+ //# sourceMappingURL=cli-3HH2BY7Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}