@impeccable/reviews 0.1.17 → 0.1.18

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Vercel deploys previews of the pull requests: impeccable_hosted_previews lists a successful Preview deployment that Vercel made (`vercel: true`) of a pull request, with its address. The checkout often shows it too (`vercel.json`, `.vercel/project.json`, a `vercel` dependency), but GitHub's record decides. Vercel builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
4
4
 
5
- It applies as well **before the first pull request**: Vercel deploys the repository already (impeccable_hosted_previews lists deployments Vercel made, production ones or of pushes, and none of a pull request; on a repository with several apps, those of the project that builds the app, named in `project`), so its previews start with the test drive's push. Then there is nothing to prove in step 1 yet: impeccable_ask with `vercel-setup` is refused, since there is no sign-in, project or connection to make; go on to pull-requests' step 4, which pushes the test drive, waits for Vercel's deployment of the pushed branch (impeccable_hosted_previews with `branch`, and the app's `appKey`), proves the test drive's page there, past Vercel's login as preview-protection says, and then step 2 saves the method.
5
+ It applies as well **before the first pull request**: Vercel deploys the repository already (impeccable_hosted_previews lists deployments Vercel made, production ones or of pushes, and none of a pull request; on a repository with several apps, those of the project that builds the app, named in `project`), so its previews start with the test drive's push. Then there is nothing to prove in step 1 yet: impeccable_ask with `vercel-setup` is refused, since there is no sign-in, project or connection to make; go on to pull-requests' step 4, which asks for Vercel's bypass secret where the project's previews are protected, pushes the test drive, waits for Vercel's deployment of the pushed branch (impeccable_hosted_previews with `branch`, and the app's `appKey`), proves the test drive's page there, past Vercel's login as preview-protection says, and then step 2 saves the method.
6
6
 
7
7
  ### Steps
8
8
 
@@ -14,8 +14,8 @@ It applies as well **before the first pull request**: Vercel deploys the reposit
14
14
  - **The live app**: with the checkout linked to the project (`.vercel/project.json`) and a production deployment of it on GitHub's record, the evidence answers the project's own domain as `production`; live-app-address says how it becomes the before, in step 2's write. Unlinked, GitHub's record names no address of it that is open.
15
15
  - **The aim** holds here: the CLI does the work, and the person is asked only what the playbook's aim allows.
16
16
  - **Behind Vercel's login** (`gate: "vercel-protection"`): Deployment Protection stands in front of the previews. Get past it as preview-protection says, which proves the preview, and go on with step 2; when nothing got past it, that case says how the run ends.
17
- - **`pullRequestPreviews` in impeccable_hosted_previews' answer**: what vercel.json and the linked project say of the test drive's branch, read with the evidence and never changed by the run, since the setting is the repository's own or has no command (impeccable_enable_previews takes no Vercel). `enabled: false` (`git.deploymentEnabled` in vercel.json turns the branch off, or Vercel records no deployments on GitHub, where Impeccable finds its previews): push no test drive on Vercel. Set the preview up as server-previews or static-site says, and say in one line under "What was skipped, and why" that Vercel builds no preview of the pull requests, with the `reason`, and the exact change for the person: the vercel.json rule to lift, or Vercel GitHub deployments under the project's Settings → Git. `enabled: 'unknown'` (an Ignored Build Step decides commit by commit, or the CLI could not read the project): go on, and say so in one line under "What is not verified".
18
- - **Another app, or no answer from any of the three**: Vercel's previews do not show this app. Set the preview up as server-previews or static-site says instead, and say under "What is not verified" what the Vercel address answered.
17
+ - **`pullRequestPreviews` in impeccable_hosted_previews' answer**: what vercel.json and the linked project say of the test drive's branch, read with the evidence and never changed by the run, since the setting is the repository's own or has no command (impeccable_enable_previews takes no Vercel). `enabled: false` (`git.deploymentEnabled` in vercel.json turns the branch off, or Vercel records no deployments on GitHub, where Impeccable finds its previews): push no test drive on Vercel. Set the preview up as server-previews says, and say in one line under "What was skipped, and why" that Vercel builds no preview of the pull requests, with the `reason`, and the exact change for the person: the vercel.json rule to lift, or Vercel GitHub deployments under the project's Settings → Git. `enabled: 'unknown'` (an Ignored Build Step decides commit by commit, or the CLI could not read the project): go on, and say so in one line under "What is not verified".
18
+ - **Another app, or no answer from any of the three**: Vercel's previews do not show this app. Set the preview up as server-previews says instead, and say under "What is not verified" what the Vercel address answered.
19
19
  - **The pull requests**: no setup pull request; the test drive branches from the default branch, and Vercel builds its preview from the push, which pull-requests' step 4 waits for and proves the test drive's page on before the pull request opens. In place of pull-requests' step 3, build the test drive branch once with the install and build Vercel runs (`vercel.json`'s `installCommand` and `buildCommand`, else the package's), as plain commands (`pnpm install`, then `pnpm run build`) and not with impeccable_verify_preview, which is for a preview Impeccable starts itself, so a page that does not build is never pushed.
20
20
  - The evidence is GitHub's: link, deploy and change nothing on Vercel, and save no address pattern, since Vercel reports each preview's address to GitHub itself.
21
21
  - The report names the address that answered with this app under "What we found", and puts the test drive's own preview under "What is not verified".
@@ -8,7 +8,7 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
8
8
  - **`cli: "missing"`**: install the CLI with `npm install -g vercel@62.7.0`, exactly so: the run installs it with npm alone, pinned to that version, and refuses another package manager, version or tag; then ask again: the screen shows no second time.
9
9
  - **`teams` in the answer instead of `team`** (several teams, none current): ask once which owns the project, `kind: "single"`, `id: "vercel-team"`, `options` the teams as given (the tool words the rest). The pick is the team.
10
10
  - **Neither `team` nor `teams`**: the account has no team, and `vercel link --yes` without `--team` links in the person's own scope.
11
- - **Skip**: set the preview up as server-previews or static-site says, and say under "What was skipped, and why" that Vercel was found and the sign-in was skipped. The steps below do not apply.
11
+ - **Skip**: set the preview up as server-previews says, and say under "What was skipped, and why" that Vercel was found and the sign-in was skipped. The steps below do not apply.
12
12
  2. **Create the project, then link it** (run each Vercel command plain, on its own: its answer is short, a pipe to `tail` is refused, and a command chained after it leaves the project unrecorded): `vercel project add <the folder's name> --team <the team>` creates the project in that team, under the name `vercel link` gives a project (the checkout folder's name, lowercase); then `vercel link --yes --team <the team>`, in the checkout's root (never with `--cwd` or after a `cd`), links the folder to the project of that name, which is now the run's. The CLI names no team on its own without a terminal, so the option is given whenever there is a team. Done when the link prints the linked project.
13
13
  - **`vercel project add` fails** (the name is in use: the project is the person's, not this run's): run no link; the Vercel setup ends there, and with the checkout linked to no project its CLI stops and the hold on pushes goes. Go on as "A step fails before the test drive is pushed" says, naming the project under "What was skipped, and why". When `.vercel/project.json` was in the checkout before the run, that link is not this run's, and the hold on pushes stays, by design: no test drive, as "No preview for the pushed test drive" says, and the report says under "What was skipped, and why" that the person removes `.vercel/project.json` or links the project themselves and runs the setup again.
14
14
  3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, when that project is the one step 2 created or one connected to this repository already, asks the person for Vercel's GitHub app's access when the app cannot reach the repository, and then, on a project with no production deployment, puts Vercel's login in front of every address of the project, its own included. Vercel makes a new project's first deployment Production whatever branch it builds and serves it at the project's own address, and the test drive's push is that first deployment. When Vercel refuses the lock, the tool asks the person on one screen to turn it on in the project's settings and waits for it. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
@@ -19,7 +19,7 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
19
19
 
20
20
  - **The live app**: a project created here has none yet, which the report says; one that deploys production already (`production: "exists"`) answers its address as `productionUrl`, and live-app-address says how it becomes the before.
21
21
  - **The aim** holds here: the CLI does the work, and the person is asked only what the playbook's aim allows.
22
- - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the team skipped, the project not created, the repository not connected, or step 3 answering that the linked project is not this run's: `vercel link` linked one of the person's projects, named like the folder, which is left as it is): the Vercel setup ends there. Set the preview up as server-previews or static-site says, say under "What was skipped, and why" what stopped it, and put the person's step in `left` when there is one, such as "Give Vercel's GitHub app access to the repository", with the `access` page in the report.
22
+ - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the team skipped, the project not created, the repository not connected, or step 3 answering that the linked project is not this run's: `vercel link` linked one of the person's projects, named like the folder, which is left as it is): the Vercel setup ends there. Set the preview up as server-previews says, say under "What was skipped, and why" what stopped it, and put the person's step in `left` when there is one, such as "Give Vercel's GitHub app access to the repository", with the `access` page in the report.
23
23
  - **`pullRequestPreviews` in impeccable_hosted_previews' answer**: what vercel.json and the linked project say of the test drive's branch, read with the evidence and never changed by the run, since the setting is the repository's own or has no command (impeccable_enable_previews takes no Vercel). `enabled: false` (`git.deploymentEnabled` in vercel.json turns the branch off, or Vercel records no deployments on GitHub, where Impeccable finds its previews): the Vercel setup ends before step 4, as "A step fails before the test drive is pushed" says, and say in one line under "What was skipped, and why" that Vercel builds no preview of the pull requests, with the `reason`, and the exact change for the person: the vercel.json rule to lift, or Vercel GitHub deployments under the project's Settings → Git. `enabled: 'unknown'` (an Ignored Build Step decides commit by commit, or the CLI could not read the project): go on, and say so in one line under "What is not verified".
24
24
  - **A push before step 3** is refused, since a connected project builds every push: run impeccable_vercel_connect first.
25
25
  - **On a repository with several apps**, the project step 2 creates builds the repository's root: impeccable_vercel_connect connects and locks it only where an app this run sets up is the root's, and otherwise answers so, and the Vercel setup ends there for that app, as "A step fails before the test drive is pushed" says.
package/dist/playbook.js CHANGED
@@ -12,7 +12,7 @@ export function loadCases(directory = new URL('./playbook/cases/', import.meta.u
12
12
  }
13
13
  /** The system prompt: the setup, its steps, then the reference and the cases. The instructions JSON carries every write shape and the fence decides what runs; this restates neither. */
14
14
  export function playbook(repo) {
15
- return `You are Impeccable's setup agent, in a checkout of ${repo.owner}/${repo.name} on the person's own machine. This setup covers one situation: Impeccable previews the repository's web front end, taking the previews its host deploys for pull requests (vercel-previews, netlify-previews, cloudflare-previews, or vercel-setup and netlify-setup, which turn them on) or, for a static site, building it itself through the workflow the setup state generates (\`static-upload\`), and signs in with the app's own username-or-email-and-password accounts when it has a sign-in. You read files and run commands here, save Impeccable's settings with the impeccable_* tools, and open pull requests as the person. The person watches this terminal, and a web page mirrors your tasks and the report.
15
+ return `You are Impeccable's setup agent, in a checkout of ${repo.owner}/${repo.name} on the person's own machine. This setup covers one situation: Impeccable previews the repository's web front end, taking the previews its host deploys for pull requests (vercel-previews, netlify-previews, cloudflare-previews, or vercel-setup and netlify-setup, which turn them on) and signs in with the app's own username-or-email-and-password accounts when it has a sign-in. You read files and run commands here, save Impeccable's settings with the impeccable_* tools, and open pull requests as the person. The person watches this terminal, and a web page mirrors your tasks and the report.
16
16
 
17
17
  # The aim
18
18
  Set the repository up with whatever the host's CLI can do (Netlify's, Vercel's, Cloudflare's, signed in as the person), through the commands the fence runs and the impeccable_* tools, and find your own way past a roadblock instead of handing it to the person. Ask the fewest questions possible: a sign-in where a CLI is signed out, and a yes before anything in their account changes (*"Make your previews public, so reviews can open them?"*). Set the live app's address as the before yourself where the host or the README names it (live-app-address): the host's own address of the connected project changes nothing of theirs, so it takes no question. **A step for the person is the last resort**, only where no command the fence runs and no tool can do it; a refusal is final, as Commands says.
@@ -21,7 +21,7 @@ Set the repository up with whatever the host's CLI can do (Netlify's, Vercel's,
21
21
 
22
22
  1. **Read the state.** The setup API's instructions and state came with the first message, trimmed to this setup; impeccable_state reads the state again. Done when you know the preview item's status, \`file\` and \`workflowStanding\`, whether there is a \`signin\` item, whether there is an \`apps\` item (the monorepo case comes first then), and the titles in \`notPartOfThisSetup\`.
23
23
  2. **Read the repository**: manifests, the lockfile, scripts, framework config, CI workflows, env examples, the routes, the auth code and the seed. Done when you can name the package manager, the install, build and start commands, whether the pages need a server, how people sign in, and which pages a visitor opens without signing in.
24
- 3. **Set up the preview** as the vercel-previews case says when impeccable_hosted_previews lists Vercel's previews of the pull requests, as the netlify-previews case says when impeccable_netlify_previews lists Netlify's, or a site of this repository with Deploy Previews on (and as preview-protection says when the host's login or password stands in front of either), as the cloudflare-previews case says when impeccable_cloudflare_previews lists Cloudflare Pages', as the vercel-setup case says when the checkout is set up for Vercel and Vercel has never deployed it, as the netlify-setup case says when the checkout is set up for Netlify and it lists none, else as the server-previews case says when the pages need a server, and as the static-site case says when they do not; then the live app's address as the live-app-address case says, in the method's write where it can be. Done when those cases' steps are done.
24
+ 3. **Set up the preview** as the vercel-previews case says when impeccable_hosted_previews lists Vercel's previews of the pull requests, as the netlify-previews case says when impeccable_netlify_previews lists Netlify's, or a site of this repository with Deploy Previews on (and as preview-protection says when the host's login or password stands in front of either), as the cloudflare-previews case says when impeccable_cloudflare_previews lists Cloudflare Pages', as the vercel-setup case says when the checkout is set up for Vercel and Vercel has never deployed it, as the netlify-setup case says when the checkout is set up for Netlify and it lists none, else as the server-previews case says; then the live app's address as the live-app-address case says, in the method's write where it can be. Done when those cases' steps are done.
25
25
  4. **Set up the sign-in** as the sign-in case says, when the app has its own. Done when that case's steps are done.
26
26
  5. **Run impeccable_checkup**, do what \`next\` says, and run it again. Done when \`status\` is complete, or all that is left is in \`forPerson\` or \`waitingFor\`.
27
27
  6. **Open the pull requests** as the pull-requests case says. What waits on the person holds neither back. Done when that case's steps are done.
@@ -62,7 +62,7 @@ Keep secrets out of files, settings and messages: the workflow names a repositor
62
62
  ## Voice
63
63
  The person reads your [STATUS] lines, task titles and questions, here and on the web page: calm, specific, one line each.
64
64
  - **[STATUS] lines**: start each step's message with one, a present-tense verb phrase of at most 70 characters saying what is happening (*"Running the build"*, *"Opening the test drive"*), or a short finding (*"Next.js 16, pnpm, Prisma on SQLite. No hosting found."*). impeccable_progress refuses a line that narrates like an assistant: write *"Reading the deploy workflows"*, not *"I'm trying to work out how the app deploys."* Detail belongs in the report.
65
- - **Tasks**: after each task, call impeccable_progress with the whole list. Titles are short noun phrases the person recognizes, the same every run where they fit: Read the repository, Apps (on a repository with several apps), Install and build, Preview method, Preview workflow, Sign-in, Test drive, Setup pull request. The list carries only what this run does: add a task when its case starts, never before, and leave off a task no case of this run takes, such as Preview workflow and Setup pull request where a host builds the previews (impeccable_progress refuses them once its previews are the method). Mark a task completed only once its result is saved, and a task completed stays so: Preview method once a method is saved, so where the test drive's preview is what proves the method (a host's previews with no pull request yet), it waits as pending while Test drive is in progress and is completed with the save. Mark a task skipped only when the person skipped it, and failed only when it was tried and failed. One task is in progress at a time, and every status line is the detail of that task (*"Installing with pnpm"* while Install and build is in progress); impeccable_progress refuses a status line about another task.
65
+ - **Tasks**: after each task, call impeccable_progress with the whole list. Titles are short noun phrases the person recognizes, the same every run where they fit: Read the repository, Set up monorepo apps (on a repository with several apps), Install and build, Preview method, Preview workflow, Sign-in, Test drive, Setup pull request. The list carries only what this run does: add a task when its case starts, never before, and leave off a task no case of this run takes, such as Preview workflow and Setup pull request where a host builds the previews (impeccable_progress refuses them once its previews are the method). Mark a task completed only once its result is saved, and a task completed stays so: Preview method once a method is saved, so where the test drive's preview is what proves the method (a host's previews with no pull request yet), it waits as pending while Test drive is in progress and is completed with the save. Mark a task skipped only when the person skipped it, and failed only when it was tried and failed. One task is in progress at a time, and every status line is the detail of that task (*"Installing with pnpm"* while Install and build is in progress); impeccable_progress refuses a status line about another task.
66
66
  - **Questions**: one sentence, at most one hint line, and a title that names the subject.
67
67
 
68
68
  ## The report
@@ -74,7 +74,7 @@ impeccable_report ends the run: no tool or model call works after it, so call it
74
74
  ## What was skipped, and why
75
75
  [ends with the "Not part of this setup" line]
76
76
  ## What is not verified
77
- [everything you did not see working]
77
+ [each part of the setup you did not see working: a preview, a proof, a check, a sign-in, a secret. Not what a page shows, such as a README's manual check of a page's content or data: the review renders the page]
78
78
  ## Before you merge
79
79
  \`\`\`
80
80
 
package/dist/sandbox.js CHANGED
@@ -617,10 +617,9 @@ export async function startSandbox(o) {
617
617
  }
618
618
  },
619
619
  };
620
- const proven = "the preview job on the pull request proves a static site's build instead";
621
620
  return {
622
621
  warning: off && `${off[0].toUpperCase()}${off.slice(1)}, so the setup's commands run without a sandbox, with all your access.`,
623
- noPreview: off ? `The local preview check, which needs a sandbox of its own, does not run either; ${proven}.` : platform === 'linux' ? `On Linux bubblewrap keeps the app from the local preview check, so the check does not run here; ${proven}.` : undefined,
622
+ noPreview: off ? `The local preview check, which needs a sandbox of its own, does not run either.` : platform === 'linux' ? `On Linux bubblewrap keeps the app from the local preview check, so the check does not run here.` : undefined,
624
623
  shell,
625
624
  finish: () => watched.settle(),
626
625
  preview(folder) {
package/dist/tools.js CHANGED
@@ -10,9 +10,9 @@ import { defineTool } from '@earendil-works/pi-coding-agent';
10
10
  import { agreeToHostSetup, bindPreviewPullRequest, chooseHostTeam, chosenHostTeam, classifyPreviewCommand, endHostSetup, hostSetupAgreed, isSecretFile, linkedByRun, observePreviewAttempts, parseCommand, previewRetryState, settleVercelFirstDeployment, vercelProjectsAdded } from './fence.js';
11
11
  import { requestJson } from './http.js';
12
12
  import { appsLeftOut, focusCheckup, focusInstructions, focusState, severalApps } from './focus.js';
13
- import { APPS_QUESTION, cloudflareAccessPaste, cloudflareAccessReusable, HOST_QUESTIONS, HOST_SCREENS, HOST_SIGNED_IN, namedAppQuestion, sentTo, startedIn, vercelBypassPaste } from './host-questions.js';
13
+ import { APPS_QUESTION, cloudflareAccessConsent, cloudflareAccessPaste, cloudflareAccessReusable, HOST_QUESTIONS, HOST_SCREENS, HOST_SIGNED_IN, namedAppQuestion, sentTo, startedIn, vercelBypassConsent, vercelBypassPaste } from './host-questions.js';
14
14
  import { enableNetlifyPreviews, folderOf } from './host-previews.js';
15
- import { checkoutNames, liveShaped, netlifySiteOf, previewHostName, provePreview, readHostedPreviews } from './hosted.js';
15
+ import { checkoutNames, liveShaped, netlifySiteOf, previewHostName, provePreview, readHostedPreviews, readmeNamesApp, vercelDeploymentOf } from './hosted.js';
16
16
  import { cfRunner, cfStatus, CLOUDFLARE_APP_PAGE, connectCloudflareProject, enableCloudflarePreviews, pagesProject, pagesProjectName, readCloudflarePreviews } from './cloudflare.js';
17
17
  import { passCloudflareAccess } from './cloudflare-access.js';
18
18
  import { commandRunner, connectNetlifySite, liveSite, netlifyStatus, netlifyTeams, readNetlifyPreviews, waitForNetlifyAccess } from './netlify.js';
@@ -167,6 +167,22 @@ export function ending(report) {
167
167
  }
168
168
  /** Said once, here and in the report, when the host builds the previews and the repository needed no change. */
169
169
  export const nothingToMerge = (host) => `Nothing to merge: ${host} hosts your previews, so the repository needed no change.`;
170
+ /** The last screen's line for each host, in the order first met: "Cloudflare: Acme's Account · project crewline", "Netlify: team Acme · site crewline", "Vercel: team acme · project crewline" (#360). */
171
+ export function hostLines(used) {
172
+ const byHost = new Map();
173
+ for (const u of used) {
174
+ const entry = byHost.get(u.host) ?? { accounts: new Set(), projects: new Set() };
175
+ if (u.account)
176
+ entry.accounts.add(u.account);
177
+ entry.projects.add(u.project);
178
+ byHost.set(u.host, entry);
179
+ }
180
+ return [...byHost].map(([host, { accounts, projects }]) => {
181
+ const what = host === 'Netlify' ? 'site' : 'project';
182
+ const account = accounts.size ? `${host === 'Cloudflare' ? '' : 'team '}${[...accounts].join(', ')} · ` : '';
183
+ return `${host}: ${account}${projects.size > 1 ? `${what}s` : what} ${[...projects].join(', ')}`;
184
+ });
185
+ }
170
186
  const PROGRESS = {
171
187
  type: 'object',
172
188
  additionalProperties: false,
@@ -229,6 +245,14 @@ const TEST_PR = {
229
245
  const APP_KEY_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
230
246
  /** The app a host tool reads for, on a repository with several apps: its previews are those of the project or site that builds the app's folder. */
231
247
  const APP_KEY = { type: 'string', minLength: 1, description: 'On a repository with several apps, the key of the app this is for, one the apps question picked: only the previews of the host project that builds that app\'s folder are read.' };
248
+ const VERCEL_BYPASS = {
249
+ type: 'object',
250
+ additionalProperties: false,
251
+ properties: {
252
+ appKey: APP_KEY,
253
+ beforePush: { type: 'boolean', description: "True before the test drive's push on Vercel, where no preview met Vercel's login yet: reads whether the project's previews are protected and asks the consent now, before anything is pushed." },
254
+ },
255
+ };
232
256
  const HOSTED_PREVIEWS = {
233
257
  type: 'object',
234
258
  additionalProperties: false,
@@ -1584,6 +1608,27 @@ export function createTools(ctx) {
1584
1608
  /** Whether any preview write with a method was taken this run, and the host's lock the last gated proof met: the last screen names it when no preview ever opened (#321), and Preview method is completed only once a method is saved (#322). */
1585
1609
  let methodSaved = false;
1586
1610
  let lastLock;
1611
+ /** The host preview addresses a proof or a host's key opened, and the Cloudflare account and Netlify team of each project or site the host tools read, by host and project: the last screen names them (#360), and no tool's answer does. */
1612
+ const usedPreviews = new Set();
1613
+ const accountOf = new Map();
1614
+ /** The Vercel project the bypass tool read for each address its secret opened, and its team where that is not the CLI's current scope. */
1615
+ const vercelNamed = new Map();
1616
+ /** What the last screen says the run used on each host, from the previews it opened. */
1617
+ const hostsUsed = () => [...usedPreviews].flatMap((url) => {
1618
+ const origin = new URL(url).origin;
1619
+ const label = pagesProject(origin);
1620
+ if (label)
1621
+ return [{ host: 'Cloudflare', project: label, ...(accountOf.has(`cloudflare ${label}`) ? { account: accountOf.get(`cloudflare ${label}`) } : {}) }];
1622
+ const site = netlifySiteOf(url);
1623
+ if (site)
1624
+ return [{ host: 'Netlify', project: site, ...(accountOf.has(`netlify ${site}`) ? { account: accountOf.get(`netlify ${site}`) } : {}) }];
1625
+ const read = vercelNamed.get(origin);
1626
+ const named = read?.project ?? listedApps.get(origin)?.name;
1627
+ const vercel = vercelDeploymentOf(url, named);
1628
+ const team = vercel?.team ?? read?.team;
1629
+ const project = vercel?.project ?? named;
1630
+ return project ? [{ host: 'Vercel', project, ...(team ? { account: team } : {}) }] : [];
1631
+ });
1587
1632
  /**
1588
1633
  * A host's build, waited for on the screen as the task in progress, naming `host`, with the time so far: `read` again
1589
1634
  * every so often until `built` says the evidence is final, or the wait runs out. The status line names the host and what it builds.
@@ -1641,6 +1686,8 @@ export function createTools(ctx) {
1641
1686
  const bypassOrigins = new Map();
1642
1687
  /** What impeccable_vercel_bypass answered for a store, less the secret: a skip is asked once, so that answer stands. */
1643
1688
  const bypassOutcomes = new Map();
1689
+ /** The stores whose consent was given before the test drive's push, each with the project it named (`<store>/<projectId>`): the call once the gate is met asks no second time for that project alone. */
1690
+ const bypassConsented = new Set();
1644
1691
  /** The Netlify site password saved in this run, for the proof of that site's addresses to send; never for the model. It is one site's, so it goes only to that site's deploy previews. */
1645
1692
  let sitePassword;
1646
1693
  const passwordSites = new Set();
@@ -1659,6 +1706,8 @@ export function createTools(ctx) {
1659
1706
  let accessStored = false;
1660
1707
  /** What impeccable_cloudflare_access answered, less the token: a skip is asked once, so that answer stands. */
1661
1708
  let accessOutcome;
1709
+ /** The Access applications, by id, the person said yes to a token for. */
1710
+ const accessConsented = new Set();
1662
1711
  /** What each host's settings last said of its pull-request previews, as the evidence tools read them: the one fact impeccable_enable_previews acts on. */
1663
1712
  const previewFacts = new Map();
1664
1713
  /** What impeccable_enable_previews answered for a host: asked once, so that answer stands. */
@@ -2349,13 +2398,16 @@ export function createTools(ctx) {
2349
2398
  if (target.url.endsWith('/preview') && saved && 'beforeUrl' in saved && (typeof saved.beforeUrl !== 'string' || !URL.canParse(saved.beforeUrl) || !provenLive.has(new URL(saved.beforeUrl).origin))) {
2350
2399
  return "Refused: beforeUrl is the live app's address that impeccable_prove_preview opened with this app in this run (the production address a host tool answered, or the one the README names), or a page on it; an address no key opens is not set, and a saved one is never cleared from here. Nothing was saved.";
2351
2400
  }
2352
- // One app's before is its own: the production address its own project or site answered, or one a file in its own folder names.
2401
+ // One app's before is its own: the production address its own project or site answered, or one a file in its own folder names, or a line of the root README that names that app and no other (#367).
2353
2402
  if (appsRepo && target.url.endsWith('/preview') && saved && typeof saved.beforeUrl === 'string') {
2354
2403
  const origin = new URL(saved.beforeUrl).origin;
2355
2404
  const owner = liveApps.get(origin);
2356
2405
  const folder = await appFolder(pi, String(saved.appKey));
2357
- if (owner !== undefined ? owner !== saved.appKey : folder === undefined || !(await checkoutNames(pi.cwd, origin, folder))) {
2358
- return `Refused: ${origin} is not the live address of ${String(saved.appKey)}: it is another app's, or no file in that app's folder names it. Nothing was saved.`;
2406
+ const app = knownApps?.find((a) => a.key === saved.appKey);
2407
+ const named = async () => folder !== undefined && ((await checkoutNames(pi.cwd, origin, folder))
2408
+ || (app !== undefined && (await readmeNamesApp(pi.cwd, origin, app, (knownApps ?? []).filter((a) => a.key !== app.key)))));
2409
+ if (owner !== undefined ? owner !== saved.appKey : !(await named())) {
2410
+ return `Refused: ${origin} is not the live address of ${String(saved.appKey)}: it is another app's, or neither a file in that app's folder nor a line of the root README that names that app (and no other) names it. Nothing was saved.`;
2359
2411
  }
2360
2412
  }
2361
2413
  const testAccounts = target.url.endsWith('/test-accounts');
@@ -2389,8 +2441,6 @@ export function createTools(ctx) {
2389
2441
  confirmedApps = true;
2390
2442
  if (target.url.endsWith('/preview') && response.ok) {
2391
2443
  const keys = object(object(response.data)?.keys);
2392
- if (keys?.intendedMethod === 'stored' && object(p.body)?.intendedMethod === 'static-upload')
2393
- retry.requireHistory = true;
2394
2444
  if (object(p.body)?.intendedMethod === 'preview-deploy')
2395
2445
  savedHost = provenHost;
2396
2446
  if (saved?.appKey !== undefined)
@@ -2615,7 +2665,8 @@ export function createTools(ctx) {
2615
2665
  return `Error: the report was refused (${res.status}: ${res.error}). Fix what it says and call impeccable_report again.`;
2616
2666
  }
2617
2667
  // Otherwise final: a completed write whose answer was lost may still have landed, so it is never taken back.
2618
- ctx.onReport({ markdown, left, testPr: drive, setupPr: ctx.sync.payload.setup_pr, ...(savedHost ? { hostedBy: savedHost } : {}), ...(apps ? { apps } : {}), methodSaved, ...(lastLock && !hostedPreviewProven ? { lock: lastLock } : {}) });
2668
+ const hosts = hostLines(hostsUsed());
2669
+ ctx.onReport({ markdown, left, testPr: drive, setupPr: ctx.sync.payload.setup_pr, ...(savedHost ? { hostedBy: savedHost } : {}), ...(apps ? { apps } : {}), methodSaved, ...(lastLock && !hostedPreviewProven ? { lock: lastLock } : {}), ...(hosts.length ? { hosts } : {}) });
2619
2670
  pi.shutdown();
2620
2671
  const saved = res?.ok ? 'Saved.' : `The web page did not take it (${res?.error ?? 'no answer'}).`;
2621
2672
  return { text: `${saved} The terminal shows the summary and the command exits.`, terminate: true };
@@ -2625,6 +2676,9 @@ export function createTools(ctx) {
2625
2676
  const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
2626
2677
  if (refused)
2627
2678
  return refused;
2679
+ // The bypass secret skipped: Vercel's login keeps the test drive's preview from the review, so there is none to wait for (#356).
2680
+ if (branch !== undefined && bypassOutcomes.get(appsRepo ? p.appKey : '')?.status === 'skipped')
2681
+ return "Refused: the person skipped the bypass secret, and Vercel's login stands in front of the previews, so the test drive's preview could not be reviewed: push and open no test drive, as preview-protection's step 3 says. Nothing was read.";
2628
2682
  // The fact, the live app and an app's project read the same projects: each `vercel api` call is made once.
2629
2683
  const reads = new Map();
2630
2684
  const vercel = (args) => {
@@ -2734,7 +2788,9 @@ export function createTools(ctx) {
2734
2788
  const directory = appKey !== undefined ? await appFolder(pi, appKey) : undefined;
2735
2789
  if (appKey !== undefined && directory === undefined)
2736
2790
  return JSON.stringify({ appKey, sites: [], previews: [], unmatched: `The setup state names no folder for the app ${appKey}.` });
2737
- const read = () => (ctx.netlifyPreviewReader ?? readNetlifyPreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, ...(directory !== undefined ? { directory } : {}) });
2791
+ // Each site's team is for the last screen alone.
2792
+ const onTeams = (teams) => Object.entries(teams).forEach(([name, team]) => accountOf.set(`netlify ${name}`, team));
2793
+ const read = () => (ctx.netlifyPreviewReader ?? readNetlifyPreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, onTeams, ...(directory !== undefined ? { directory } : {}) });
2738
2794
  // A pull request's preview is still building until Netlify's deploy of it is built or has failed.
2739
2795
  const states = ['success', 'ready', 'failure', 'error'];
2740
2796
  const final = (evidence) => Boolean(evidence.error) || evidence.previews.some((preview) => states.includes(preview.state ?? ''));
@@ -2785,7 +2841,9 @@ export function createTools(ctx) {
2785
2841
  const appProject = appKey !== undefined ? cloudflareApps.get(appKey) : undefined;
2786
2842
  if (appKey !== undefined && (pull !== undefined || branch !== undefined) && !appProject)
2787
2843
  return `Refused: read impeccable_cloudflare_previews with appKey ${appKey} and no pull or branch first: it tells which Pages project builds that app.`;
2788
- const readAll = () => (ctx.cloudflarePreviewReader ?? readCloudflarePreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, branch, ...(directory !== undefined && pull === undefined && branch === undefined ? { directory } : {}), ...(appProject ? { project: appProject.project } : {}) });
2844
+ // The name of the account the project was found in is for the last screen alone.
2845
+ let accountName;
2846
+ const readAll = () => (ctx.cloudflarePreviewReader ?? readCloudflarePreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, branch, onAccountName: (name) => (accountName = name), ...(directory !== undefined && pull === undefined && branch === undefined ? { directory } : {}), ...(appProject ? { project: appProject.project } : {}) });
2789
2847
  const read = async () => {
2790
2848
  const evidence = await readAll();
2791
2849
  return appProject ? { ...evidence, previews: evidence.previews.filter((preview) => pagesProject(preview.url) === appProject.label) } : evidence;
@@ -2800,6 +2858,10 @@ export function createTools(ctx) {
2800
2858
  const access = branch !== undefined && previewFacts.get('cloudflare')?.enabled === true && app?.repositories !== 'all' && !cloudflareEvidence?.previews.length ? { page: app?.page ?? CLOUDFLARE_APP_PAGE, now: app?.installed === false } : undefined;
2801
2859
  const waited = subject ? await awaitBuild('Cloudflare', `Waiting for Cloudflare Pages to build ${subject}`, read, final, () => 'https://dash.cloudflare.com/?to=/:account/workers-and-pages', signal, access) : await read();
2802
2860
  const evidence = subject ? { ...waited, ...(app ? { githubApp: app } : {}) } : waited;
2861
+ // By the label the project's addresses carry.
2862
+ const label = (evidence.production && pagesProject(evidence.production)) || evidence.project;
2863
+ if (accountName && label && !evidence.unmatched)
2864
+ accountOf.set(`cloudflare ${label}`, accountName);
2803
2865
  if (!subject)
2804
2866
  cloudflareEvidence = evidence;
2805
2867
  if (!subject && appKey !== undefined && !evidence.unmatched && evidence.project) {
@@ -2857,6 +2919,8 @@ export function createTools(ctx) {
2857
2919
  hostedPreviewProven ||= proof.proven;
2858
2920
  if (proof.proven)
2859
2921
  provenHost = previewHostName(url) ?? provenHost;
2922
+ if (proof.proven)
2923
+ usedPreviews.add(url);
2860
2924
  if (proof.proven && app)
2861
2925
  proveApp(app.appKey, url);
2862
2926
  if (proof.proven && app)
@@ -2867,8 +2931,16 @@ export function createTools(ctx) {
2867
2931
  lockPending.set(app.appKey, proof.gate);
2868
2932
  provenApps.delete(app.appKey);
2869
2933
  }
2870
- if (proof.gate && Object.hasOwn(LOCKS, proof.gate))
2934
+ if (proof.gate && Object.hasOwn(LOCKS, proof.gate)) {
2871
2935
  lastLock = proof.gate;
2936
+ // Pristine learns the lock from the session, proven past or not, for a preview it may never meet itself (#365): an app's on its summary, a repository of one app's beside them.
2937
+ const met = { gate: lastLock, preview_host: new URL(url).hostname };
2938
+ // The app is proven again only once something opens it, as `provenApps` has it.
2939
+ if (app)
2940
+ noteApp(app.appKey, { ...met, preview_proven: false });
2941
+ else if (!appsRepo)
2942
+ ctx.sync.update({ preview_gate: met });
2943
+ }
2872
2944
  if (proof.proven)
2873
2945
  provePattern(proof.pattern, app?.appKey);
2874
2946
  if (proof.gate === 'vercel-protection' || proof.gate === 'netlify-protection')
@@ -2956,7 +3028,7 @@ export function createTools(ctx) {
2956
3028
  return `Refused: the Pages project ${fact.project} is connected to the repository, and its settings turn pull-request previews off: impeccable_enable_previews turns them on.`;
2957
3029
  const create = !fact;
2958
3030
  if (create && !evidence.worker && !evidence.project)
2959
- return 'Refused: nothing says the repository is set up for Cloudflare: no Pages project is built from it, the checkout names none, and no Worker is built from it. Set the preview up as the server-previews or static-site case says.';
3031
+ return 'Refused: nothing says the repository is set up for Cloudflare: no Pages project is built from it, the checkout names none, and no Worker is built from it. Set the preview up as the server-previews case says.';
2960
3032
  const project = fact?.project || evidence.project || pagesProjectName(ctx.repo);
2961
3033
  const answer = (status, more) => {
2962
3034
  connectOutcome = { status, project, created: create, ...more };
@@ -3014,23 +3086,40 @@ export function createTools(ctx) {
3014
3086
  noteProduction(answer.productionUrl, 'vercel', undefined, connectedApp);
3015
3087
  return JSON.stringify(answer);
3016
3088
  }),
3017
- tool('impeccable_vercel_bypass', "Vercel's login", 'Get past the Vercel login that impeccable_prove_preview met (gate "vercel-protection") in front of a preview Vercel made, the way Vercel documents for tools: a bypass secret of the Vercel project, sent to it in a header. It does the rest with the Vercel CLI signed in as the person: it reads the project this folder is linked to (.vercel/project.json; once this run\'s own vercel link wrote that file, only what it wrote counts), or, with no such file, the one Vercel lists as connected to this repository, so no vercel link is needed (where the CLI is signed out, the consent screen comes first and its yes signs it in, in the browser), and refuses unless that project is connected to this repository, shows Deployment Protection in its settings (a page that looks like Vercel\'s login is the deployment\'s own to serve, so with none set the sign-in is the app\'s own: prove a page a visitor opens without signing in) and made the preview\'s deployment (among its latest, or read by its address), since the secret goes only to a preview the linked project serves; then it finds the project\'s secret labelled "Impeccable reviews", or shows one screen asking to create one, or, when the CLI cannot, one screen taking a pasted one; opens the preview with it; and saves it for reviews. Answers {"status": …}: saved (the preview is proven, so the method may be saved; secret says whether it was created, existing or typed, and project which Vercel project holds it), skipped (the person skipped the screen; reason says what came before), refused (no secret opened the preview; reason says what the address answered) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No secret ever comes back.', NO_INPUT, async (_p, pi, signal) => {
3018
- if (gated?.gate !== 'vercel-protection' || !vercelMade.has(new URL(gated.url).origin))
3019
- return 'Refused: this is for a preview that Vercel made, listed by impeccable_hosted_previews, which impeccable_prove_preview answered with gate "vercel-protection". Prove one first.';
3020
- const at = gated;
3021
- if (at.app)
3089
+ tool('impeccable_vercel_bypass', "Vercel's login", 'Get past the Vercel login that impeccable_prove_preview met (gate "vercel-protection") in front of a preview Vercel made, the way Vercel documents for tools: a bypass secret of the Vercel project, sent to it in a header. It does the rest with the Vercel CLI signed in as the person: it reads the project this folder is linked to (.vercel/project.json; once this run\'s own vercel link wrote that file, only what it wrote counts), or, with no such file, the one Vercel lists as connected to this repository, so no vercel link is needed (where the CLI is signed out, the consent screen comes first and its yes signs it in, in the browser), and refuses unless that project is connected to this repository, shows Deployment Protection in its settings (a page that looks like Vercel\'s login is the deployment\'s own to serve, so with none set the sign-in is the app\'s own: prove a page a visitor opens without signing in) and made the preview\'s deployment (among its latest, or read by its address), since the secret goes only to a preview the linked project serves; then it finds the project\'s secret labelled "Impeccable reviews", or shows one screen asking to create one, or, when the CLI cannot, one screen taking a pasted one; opens the preview with it; and saves it for reviews. Answers {"status": …}: saved (the preview is proven, so the method may be saved; secret says whether it was created, existing or typed, and project which Vercel project holds it), skipped (the person skipped the screen; reason says what came before), refused (no secret opened the preview; reason says what the address answered) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No secret ever comes back. With beforePush (and the app\'s appKey on a repository with several apps), before the test drive\'s push, where no preview met Vercel\'s login yet (#356): it reads the project the same way, and where its settings show Deployment Protection in front of the previews and it holds no secret labelled "Impeccable reviews", shows the consent screen now, naming the project and the app, so the person answers before anything is pushed; the secret is made on the yes once the test drive\'s preview meets the login, with no second question. Answers {"protected": false} (no lock: push), {"protected": true, "consent": "given" or "not-needed"} (push, and call this again without beforePush once the proof meets the gate), {"protected": "unknown", "reason": …} (the project could not be read: push, and the consent comes when the gate is met) or {"status": "skipped", …} (push no test drive; reason says why).', VERCEL_BYPASS, async (p, pi, signal) => {
3090
+ const before = p.beforePush === true;
3091
+ if (before) {
3092
+ const refused = appRefusal(p.appKey, 'asked');
3093
+ if (refused)
3094
+ return refused;
3095
+ }
3096
+ else if (gated?.gate !== 'vercel-protection' || !vercelMade.has(new URL(gated.url).origin))
3097
+ return 'Refused: this is for a preview that Vercel made, listed by impeccable_hosted_previews, which impeccable_prove_preview answered with gate "vercel-protection". Prove one first, or call it with beforePush before the test drive\'s push.';
3098
+ const at = before ? undefined : gated;
3099
+ if (at?.app)
3022
3100
  lockPending.delete(at.app.appKey);
3023
- const { origin, host } = new URL(at.url);
3101
+ const vercel = (ctx.hostRunner ?? commandRunner)('vercel', pi.cwd, { signal });
3102
+ // Before the push, the app's project as impeccable_hosted_previews reads it: the one that builds the app's folder (#281).
3103
+ let app = at?.app;
3104
+ if (before && appsRepo) {
3105
+ const read = await vercelApp(pi, p.appKey, vercel);
3106
+ if ('reason' in read)
3107
+ return JSON.stringify({ appKey: p.appKey, protected: 'unknown', reason: read.reason });
3108
+ app = { appKey: p.appKey, link: read.link, name: read.name };
3109
+ }
3024
3110
  // Where the setup API keeps the secret: the app's own on a repository with several apps, else the repository's.
3025
- const store = at.app?.appKey ?? '';
3111
+ const store = app?.appKey ?? '';
3026
3112
  const asked = bypassOutcomes.get(store);
3027
3113
  if (asked?.status === 'skipped')
3028
3114
  return JSON.stringify(asked);
3115
+ /** What stops the tool before the project is vouched for: the address got nothing, or, before the push, the lock is not known yet and the gate's call asks. */
3116
+ const nothing = (why) => (at ? `Refused: nothing was sent to ${at.url}. ${why}` : JSON.stringify({ ...(app ? { appKey: app.appKey } : {}), protected: 'unknown', reason: why }));
3117
+ /** The consent screen, naming the project and the app once they are known (#358). */
3118
+ const consentScreen = (project) => vercelBypassConsent(project ?? app?.name, app && appName(app.appKey));
3029
3119
  // Whose preview this is: a deployment status anyone with push access records gives any address, and the page there says nothing, so the project has to have
3030
3120
  // made the deployment before any secret of it goes there. That project is the app's own on a repository with several apps (#281), else the one the link file names,
3031
3121
  // else the one Vercel lists as connected to this repository (#310).
3032
- const vercel = (ctx.hostRunner ?? commandRunner)('vercel', pi.cwd, { signal });
3033
- const fileLink = at.app ? null : await readLink(pi.cwd);
3122
+ const fileLink = app ? null : await readLink(pi.cwd);
3034
3123
  /** The consent screen's yes, given before Vercel's sign-in, stands for the secret: it is not asked again. */
3035
3124
  let consented = false;
3036
3125
  /**
@@ -3044,7 +3133,7 @@ export function createTools(ctx) {
3044
3133
  bypassOutcomes.set(store, outcome);
3045
3134
  return JSON.stringify(outcome);
3046
3135
  };
3047
- if ((await ask(sentTo(HOST_SCREENS['vercel-bypass'], at.url))).skipped)
3136
+ if ((await ask(consentScreen())).skipped)
3048
3137
  return skip('The person skipped creating a bypass secret.');
3049
3138
  consented = true;
3050
3139
  const agreed = hostSetupAgreed(ctx.log, 'vercel');
@@ -3067,23 +3156,23 @@ export function createTools(ctx) {
3067
3156
  // A folder Vercel deploys from GitHub has no link file unless vercel link ran in it (the file is gitignored), so the project is found on Vercel by this repository,
3068
3157
  // with the CLI signed in as the person.
3069
3158
  let found;
3070
- if (!at.app && !fileLink) {
3159
+ if (!app && !fileLink) {
3071
3160
  const cli = (await vercelStatus(vercel)).cli;
3072
3161
  const stop = cli === 'signed-out' ? await signIn() : null;
3073
3162
  if (stop)
3074
3163
  return stop;
3075
3164
  const listed = cli === 'missing' ? { reason: 'The Vercel CLI is not installed here.' } : await vercelRepoProject(vercel, repository);
3076
3165
  if ('reason' in listed)
3077
- return `Refused: nothing was sent to ${at.url}. This folder is linked to no Vercel project (.vercel/project.json), and Vercel named none for this repository: ${listed.reason} A bypass secret goes only to a preview the repository's project serves.`;
3166
+ return nothing(`This folder is linked to no Vercel project (.vercel/project.json), and Vercel named none for this repository: ${listed.reason} A bypass secret goes only to a preview the repository's project serves.`);
3078
3167
  found = listed;
3079
3168
  }
3080
- const link = at.app?.link ?? fileLink ?? found.link;
3169
+ const link = app?.link ?? fileLink ?? found.link;
3081
3170
  // Once this run's own vercel link wrote the file, only that link counts, as impeccable_vercel_connect holds it: rewritten since, it would name another project of the person's. A project the person linked before the run has no such record, and the reads below vouch for it.
3082
3171
  // An app's project, and one found with no link file, are read from Vercel's list of this repository's projects, never from the file.
3083
3172
  const links = linkedByRun(ctx.log);
3084
3173
  const own = links.get(link.projectId);
3085
3174
  if (fileLink && [...links.values()].some((l) => l.orgId !== undefined) && own?.orgId !== link.orgId)
3086
- return `Refused: nothing was sent to ${at.url}. This folder's .vercel/project.json is not what this run's vercel link wrote (another project, or another team), and a bypass secret goes only to a preview the linked project serves.`;
3175
+ return nothing("This folder's .vercel/project.json is not what this run's vercel link wrote (another project, or another team), and a bypass secret goes only to a preview the linked project serves.");
3087
3176
  let project = await readOwnProject(vercel, link, repository);
3088
3177
  // A linked project, or an app's, read nothing because the CLI is signed out: the same sign-in, then the read again.
3089
3178
  if ('reason' in project && !consented && (await vercelStatus(vercel)).cli === 'signed-out') {
@@ -3092,9 +3181,27 @@ export function createTools(ctx) {
3092
3181
  return stop;
3093
3182
  project = await readOwnProject(vercel, link, repository);
3094
3183
  }
3095
- const which = at.app ? 'The Vercel project that builds this app' : found ? 'The Vercel project connected to this repository' : 'The Vercel project this folder is linked to';
3184
+ const which = app ? 'The Vercel project that builds this app' : found ? 'The Vercel project connected to this repository' : 'The Vercel project this folder is linked to';
3096
3185
  if ('reason' in project)
3097
- return `Refused: nothing was sent to ${at.url}. ${project.reason} A bypass secret goes only to a preview the linked project serves.`;
3186
+ return nothing(`${project.reason} A bypass secret goes only to a preview the linked project serves.`);
3187
+ if (!at) {
3188
+ // Before the push: the project's settings say whether Vercel's login will stand in front of the test drive's preview, and the consent is asked now (#356).
3189
+ const of = { ...(app ? { appKey: app.appKey } : {}), ...(project.name ? { project: project.name } : {}) };
3190
+ if (!project.previewsProtected)
3191
+ return JSON.stringify({ ...of, protected: false });
3192
+ // A secret this run saved for the same store and project, or one labelled for Impeccable, opens the test drive's preview with no question; a yes given already stands.
3193
+ const held = bypassHeld.get(store);
3194
+ if (project.bypassLabelled || held?.projectId === link.projectId)
3195
+ return JSON.stringify({ ...of, protected: true, consent: 'not-needed' });
3196
+ if (consented || bypassConsented.has(`${store}/${link.projectId}`) || !(await ask(consentScreen(project.name))).skipped) {
3197
+ bypassConsented.add(`${store}/${link.projectId}`);
3198
+ return JSON.stringify({ ...of, protected: true, consent: 'given' });
3199
+ }
3200
+ const outcome = { status: 'skipped', ...of, protected: true, reason: `The person skipped creating a bypass secret before the test drive's push. Vercel's login stands in front of the previews of ${project.name ? `the Vercel project ${project.name}` : 'the Vercel project'}, so a test drive pushed now could not be reviewed. A secret made in the project's Settings → Deployment Protection → Protection Bypass for Automation, named "Impeccable reviews", and saved on the setup page lets reviews in.` };
3201
+ bypassOutcomes.set(store, outcome);
3202
+ return JSON.stringify(outcome);
3203
+ }
3204
+ const { origin, host } = new URL(at.url);
3098
3205
  // The page at the address is the deployment's to word, whatever host it is on, so the lock is told from the project's settings, read as the person: with none, no login of Vercel's stood in front.
3099
3206
  if (!project.previewsProtected && lastLock === 'vercel-protection')
3100
3207
  lastLock = undefined;
@@ -3107,6 +3214,8 @@ export function createTools(ctx) {
3107
3214
  bypassOrigins.set(origin, store);
3108
3215
  hostedPreviewProven = true;
3109
3216
  provenHost = 'Vercel';
3217
+ usedPreviews.add(at.url);
3218
+ vercelNamed.set(origin, { ...(project.name ? { project: project.name } : {}), ...(found?.scope ? { team: found.scope } : {}) });
3110
3219
  if (at.app)
3111
3220
  proveApp(at.app.appKey, at.url);
3112
3221
  };
@@ -3123,7 +3232,8 @@ export function createTools(ctx) {
3123
3232
  link: async () => link,
3124
3233
  // A project the folder is not linked to is named on the CLI's reads and makes, in its own team; the reads are held to its id all the same.
3125
3234
  ...(at.app?.name ? { project: at.app.name } : found ? { project: found.name, ...(found.scope ? { scope: found.scope } : {}) } : {}),
3126
- consent: async () => consented || !(await ask(sentTo(HOST_SCREENS['vercel-bypass'], at.url))).skipped,
3235
+ // A yes before the push stands for this secret: the gate's call asks no second time (#356).
3236
+ consent: async () => consented || bypassConsented.has(`${store}/${link.projectId}`) || !(await ask(consentScreen(project.name))).skipped,
3127
3237
  paste: async (stopped) => {
3128
3238
  // This project's own settings page, under its own team (the one its ids name, as the CLI lists the person's teams): never another project of the account's.
3129
3239
  (ctx.openBrowser ?? openBrowser)(await vercelPageFor(vercel, link, 'settings/deployment-protection', project.name));
@@ -3243,6 +3353,7 @@ export function createTools(ctx) {
3243
3353
  // The site is public now: its previews open with nothing sent.
3244
3354
  hostedPreviewProven = true;
3245
3355
  provenHost = 'Netlify';
3356
+ usedPreviews.add(at.url);
3246
3357
  if (at.app)
3247
3358
  proveApp(at.app.appKey, at.url);
3248
3359
  provePattern(pattern, at.app?.appKey);
@@ -3254,6 +3365,7 @@ export function createTools(ctx) {
3254
3365
  passwordSites.add(site);
3255
3366
  hostedPreviewProven = true;
3256
3367
  provenHost = 'Netlify';
3368
+ usedPreviews.add(at.url);
3257
3369
  if (at.app)
3258
3370
  proveApp(at.app.appKey, at.url);
3259
3371
  provePattern(pattern, at.app?.appKey);
@@ -3267,7 +3379,7 @@ export function createTools(ctx) {
3267
3379
  }
3268
3380
  return JSON.stringify(passwordOutcome);
3269
3381
  }),
3270
- tool('impeccable_cloudflare_access', 'Cloudflare Access', 'Get past the Cloudflare Access login that impeccable_prove_preview met (gate "cloudflare-access") in front of a Cloudflare Pages preview, the way Cloudflare documents for tools: a service token, sent in two headers, that a Service Auth policy of the Access application allows. It does the rest as the person: one screen asking to create the token, Cloudflare\'s sign-in in the browser where cf, Cloudflare\'s CLI, is not signed in here with Access (with a retry), then the token named "Impeccable reviews" created through cf and allowed in the policy of the application in front of the preview (an application whose policies are its own gets one screen asking to make them reusable first, and on its skip the paste screen says how to make the token by hand); one screen taking a Cloudflare API token only where that sign-in cannot create it; on a skip, or where that cannot be done, one screen taking a pasted client id and secret, only once an Access application in the person\'s accounts was read to cover the preview (none read: refused, with the reason, since the token goes to no other address). It opens the preview with the token and saves it for reviews. Answers {"status": …}: saved (the preview is proven, and pattern is the address pattern to save with the method; token says whether it was created or typed, application which Access application allows it, expires when a created one runs out, apiToken true when the person\'s Cloudflare API token created it), skipped (nothing was saved; reason says what stopped it), refused (no Access application of the person\'s covers the preview, or the typed token did not open it; reason says which) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No token ever comes back.', NO_INPUT, async (_p, pi, signal) => {
3382
+ tool('impeccable_cloudflare_access', 'Cloudflare Access', 'Get past the Cloudflare Access login that impeccable_prove_preview met (gate "cloudflare-access") in front of a Cloudflare Pages preview, the way Cloudflare documents for tools: a service token, sent in two headers, that a Service Auth policy of the Access application allows. It does the rest as the person: one screen asking to create the token, naming the Access application in front of the preview and its Cloudflare account once cf, signed in here, found it (none found: refused, and nothing asked), Cloudflare\'s sign-in in the browser where cf, Cloudflare\'s CLI, is not signed in here with Access (with a retry), then the token named "Impeccable reviews" created through cf and allowed in the policy of the application in front of the preview (an application whose policies are its own gets one screen asking to make them reusable first, and on its skip the paste screen says how to make the token by hand); one screen taking a Cloudflare API token only where that sign-in cannot create it; on a skip, or where that cannot be done, one screen taking a pasted client id and secret, only once an Access application in the person\'s accounts was read to cover the preview (none read: refused, with the reason, since the token goes to no other address). It opens the preview with the token and saves it for reviews. Answers {"status": …}: saved (the preview is proven, and pattern is the address pattern to save with the method; token says whether it was created or typed, application which Access application allows it, expires when a created one runs out, apiToken true when the person\'s Cloudflare API token created it), skipped (nothing was saved; reason says what stopped it), refused (no Access application of the person\'s covers the preview, or the typed token did not open it; reason says which) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No token ever comes back.', NO_INPUT, async (_p, pi, signal) => {
3271
3383
  if (!accessGated)
3272
3384
  return 'Refused: this is for a Cloudflare Pages preview impeccable_cloudflare_previews listed, which impeccable_prove_preview answered with gate "cloudflare-access". Prove one first.';
3273
3385
  const at = accessGated;
@@ -3305,7 +3417,15 @@ export function createTools(ctx) {
3305
3417
  else {
3306
3418
  answer = await passCloudflareAccess({
3307
3419
  url: at.url,
3308
- consent: async () => !(await ask(sentTo(HOST_SCREENS['cloudflare-access'], at.url))).skipped,
3420
+ // Named once the Access application was found (#357), and asked once per application: a yes stands for another app's previews it covers.
3421
+ consent: async (found) => {
3422
+ if (found && accessConsented.has(found.id))
3423
+ return true;
3424
+ const yes = !(await ask(sentTo(found ? cloudflareAccessConsent(found, new URL(at.url).host, at.app && appName(at.app.appKey)) : HOST_SCREENS['cloudflare-access'], at.url))).skipped;
3425
+ if (yes && found)
3426
+ accessConsented.add(found.id);
3427
+ return yes;
3428
+ },
3309
3429
  cf: cfRunner({ checkout: pi.cwd }, ctx.hostRunner),
3310
3430
  login: cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd }, ctx.hostRunner),
3311
3431
  retry: async () => !(await ask(HOST_SCREENS['cloudflare-signin-retry'])).skipped,
@@ -3342,6 +3462,7 @@ export function createTools(ctx) {
3342
3462
  if (told.status === 'saved') {
3343
3463
  hostedPreviewProven = true;
3344
3464
  provenHost = 'Cloudflare Pages';
3465
+ usedPreviews.add(at.url);
3345
3466
  if (at.app)
3346
3467
  proveApp(at.app.appKey, at.url);
3347
3468
  provePattern(pattern, at.app?.appKey);
package/dist/ui/look.js CHANGED
@@ -113,6 +113,8 @@ const pullAction = (url, copied, inner, st) => [
113
113
  copied ? center(st.soft('Copied to clipboard!'), inner) : copyWay(inner, st),
114
114
  ];
115
115
  /** What waits on the person, under the supporting text: two lines at most, the rest counted. */
116
+ /** The hosts the run used, a line each under the result. */
117
+ const usedLines = (outro, inner, st) => (outro.used ?? []).flatMap((line) => said(line, inner, st));
116
118
  function leftLines(todo, inner, st) {
117
119
  const shown = todo.length > 2 ? [todo[0], `${todo.length - 1} more in the report`] : todo;
118
120
  return shown.map((line) => center(st.soft(`→ ${line}`), inner));
@@ -255,12 +257,10 @@ function runPage(scene, f, inner, st) {
255
257
  const waiting = row(`waiting for ${scene.wait.host ? `${scene.wait.host}'s preview` : 'the preview'} · ${scene.wait.timer}`);
256
258
  return ink(visibleWidth(waiting) <= inner ? waiting : row(scene.wait.timer));
257
259
  });
258
- const counted = scene.tasks.filter((t) => t.status !== 'skipped');
259
- const skipped = scene.tasks.length - counted.length;
260
260
  const widest = Math.max(...lines.map((line) => visibleWidth(line)));
261
+ // No count over the list: the list shows each task's state itself.
261
262
  return {
262
263
  heading: 'Setting up',
263
- text: said(`${counted.filter((t) => t.status === 'completed').length} of ${counted.length} done${skipped ? ` · ${skipped} skipped` : ''}`, inner, st),
264
264
  rule: divider(inner, st),
265
265
  action: block(lines, inner, widest),
266
266
  at: running >= 0 ? running : Math.max(0, scene.tasks.findIndex((t) => t.status === 'pending')),
@@ -273,7 +273,7 @@ function outroPage(scene, inner, st) {
273
273
  return {
274
274
  heading: outro.title,
275
275
  // A warning is read whole: wrapped, where centered on one line it would be cut.
276
- text: [...said(outro.result, inner, st), ...leftLines(outro.todo, inner, st), ...outro.warnings.flatMap((line) => said(`! ${line}`, inner, st))],
276
+ text: [...said(outro.result, inner, st), ...usedLines(outro, inner, st), ...leftLines(outro.todo, inner, st), ...outro.warnings.flatMap((line) => said(`! ${line}`, inner, st))],
277
277
  rule: divider(inner, st),
278
278
  action: pullAction(outro.link, scene.copied, inner, st),
279
279
  ways: scene.leaving ? undefined : [['Close', 'Enter']],
@@ -285,7 +285,7 @@ function outroPage(scene, inner, st) {
285
285
  const links = outro.links.flatMap((l) => linkLines(l.label, l.url, inner, st));
286
286
  return {
287
287
  heading: outro.title,
288
- text: said(outro.result, inner, st),
288
+ text: [...said(outro.result, inner, st), ...usedLines(outro, inner, st)],
289
289
  rule: divider(inner, st),
290
290
  action: [...block(notes, inner), ...(notes.length && links.length ? [''] : []), ...links, ...(outro.links.length === 1 ? [scene.copied ? center(st.soft('Copied to clipboard!'), inner) : copyWay(inner, st)] : [])],
291
291
  ways: scene.leaving ? undefined : [['Close', 'Enter']],
@@ -83,9 +83,4 @@ export class RunState {
83
83
  return { previous: this.current, current: `Still working… ${elapsed(silence)}` };
84
84
  return { previous: this.previous, current: this.current };
85
85
  }
86
- /** Completed tasks out of all but the skipped ones. */
87
- progress() {
88
- const counted = this.tasks.filter((t) => t.status !== 'skipped');
89
- return { completed: counted.filter((t) => t.status === 'completed').length, total: counted.length };
90
- }
91
86
  }