@impeccable/reviews 0.1.6 → 0.1.7

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.
Files changed (41) hide show
  1. package/README.md +21 -20
  2. package/dist/agent.js +9 -4
  3. package/dist/cloudflare-access.js +77 -47
  4. package/dist/cloudflare.js +325 -23
  5. package/dist/fence.js +939 -143
  6. package/dist/focus.js +5 -4
  7. package/dist/host-previews.js +107 -0
  8. package/dist/host-questions.js +90 -20
  9. package/dist/hosted.js +72 -21
  10. package/dist/http.js +2 -0
  11. package/dist/log.js +7 -1
  12. package/dist/login.js +45 -13
  13. package/dist/main.js +29 -14
  14. package/dist/netlify-password.js +8 -6
  15. package/dist/netlify.js +43 -14
  16. package/dist/pi-paths.js +69 -0
  17. package/dist/playbook/cases/cloudflare-previews.md +12 -4
  18. package/dist/playbook/cases/first-preview-failure.md +1 -1
  19. package/dist/playbook/cases/live-app-address.md +15 -0
  20. package/dist/playbook/cases/monorepo.md +1 -1
  21. package/dist/playbook/cases/netlify-previews.md +3 -1
  22. package/dist/playbook/cases/netlify-setup.md +7 -6
  23. package/dist/playbook/cases/preview-protection.md +2 -1
  24. package/dist/playbook/cases/pull-requests.md +3 -3
  25. package/dist/playbook/cases/static-site.md +2 -2
  26. package/dist/playbook/cases/vercel-previews.md +2 -0
  27. package/dist/playbook/cases/vercel-setup.md +8 -5
  28. package/dist/playbook.js +2 -2
  29. package/dist/preflight.js +36 -7
  30. package/dist/report.js +1 -0
  31. package/dist/sandbox.js +364 -50
  32. package/dist/scan.js +134 -0
  33. package/dist/sync.js +44 -7
  34. package/dist/tools.js +377 -88
  35. package/dist/ui/bridge.js +18 -4
  36. package/dist/ui/look.js +1 -1
  37. package/dist/ui/screens.js +5 -3
  38. package/dist/ui/sensitive.js +10 -0
  39. package/dist/vercel-bypass.js +12 -7
  40. package/dist/vercel.js +110 -17
  41. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -4,19 +4,19 @@ import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { runAgent } from './agent.js';
7
- import { fenceExtension, hostsAgreed, placeOf } from './fence.js';
7
+ import { fenceExtension, guardedFileOperations, hostsAgreed, placeOf } from './fence.js';
8
8
  import { focusInstructions, focusState, severalApps } from './focus.js';
9
9
  import { requestJson } from './http.js';
10
10
  import { copyText, letTabsHear, openBrowser, progressPage, screenPrompt, setupAuth, signIn } from './login.js';
11
11
  import { createLog, UserError } from './log.js';
12
12
  import { firstMessage, playbook, setupContext } from './playbook.js';
13
- import { checkoutFrontEnd, parseFlags, readCheckout, stateSaysNoFrontEnd, USAGE } from './preflight.js';
13
+ import { checkoutFrontEnd, parseFlags, readCheckout, skipWorkingFolderForCommands, stateSaysNoFrontEnd, USAGE } from './preflight.js';
14
14
  import { startSandbox } from './sandbox.js';
15
15
  import { scanRecorder } from './scan.js';
16
16
  import { createReporter, reportingOff } from './report.js';
17
- import { createSessionSync, syncExtension } from './sync.js';
17
+ import { createSessionSync, pageStatus, plainReport, syncExtension } from './sync.js';
18
18
  import { checkPushAccess, readPushAccess } from './push-access.js';
19
- import { createTools, resultLine } from './tools.js';
19
+ import { createTools, ownPullRequest, resultLine } from './tools.js';
20
20
  import { transcriptPaths } from './transcript.js';
21
21
  import { askPerson } from './ui/bridge.js';
22
22
  import { atExit, closeScreens, helpMark, onScreenExit, recordFrames, showIntro, showOutro, showPullRequest, showRun, showSignIn } from './ui/screens.js';
@@ -62,6 +62,7 @@ async function nothingToSetUp(sync, reporter, why, title, result, links) {
62
62
  process.exit(2);
63
63
  }
64
64
  export async function main(argv) {
65
+ skipWorkingFolderForCommands();
65
66
  let log = createLog(false);
66
67
  let sync;
67
68
  // Nothing is reported until the flags say where to and whether to.
@@ -110,8 +111,9 @@ export async function main(argv) {
110
111
  log,
111
112
  place: placeOf,
112
113
  hosts: () => hostsAgreed(log),
114
+ noSandbox: flags.noSandbox,
113
115
  });
114
- if (!(await showIntro(slug, sandbox.warning))) {
116
+ if (!(await showIntro(slug, [sandbox.warning, sandbox.noPreview].filter(Boolean).join(' ') || undefined))) {
115
117
  reporter.step({ event: 'setup_command_sign_in_ended', outcome: 'cancelled' });
116
118
  return void (await reporter.done());
117
119
  }
@@ -173,7 +175,7 @@ export async function main(argv) {
173
175
  sync = liveSync;
174
176
  log.write('run started', { run_id: run.run_id, cap_usd: run.cap_usd, expires_at: run.expires_at });
175
177
  // Impeccable reviews web front ends: a checkout without one stops here, before the state is read or anything runs.
176
- const frontEnd = checkoutFrontEnd(checkout.root);
178
+ const frontEnd = checkoutFrontEnd(checkout.root, log);
177
179
  log.write('front end', frontEnd);
178
180
  const webPage = { label: 'Web page', url: page };
179
181
  if (frontEnd === 'none')
@@ -212,10 +214,22 @@ export async function main(argv) {
212
214
  let report;
213
215
  let fatal;
214
216
  let cancelled = false;
217
+ /** The setup pull request the end screens open and copy, rebuilt from this checkout's owner and repo and its number; one that is not that is left out. */
218
+ const shownSetupPr = (pr) => {
219
+ const own = ownPullRequest(pr, checkout.owner, checkout.repo);
220
+ if (pr && !own)
221
+ log.write('setup pull request left out', { number: pr.number });
222
+ return own;
223
+ };
215
224
  const finish = async () => {
225
+ // What a command left in the git folders goes before the summary, where the person reads it.
226
+ const removed = sandbox.finish();
227
+ if (removed.length)
228
+ log.write('git settings removed', removed);
216
229
  if (report) {
217
230
  const saved = await settle(liveSync.flush(), 5000);
218
- const { testPr, setupPr } = report;
231
+ const { testPr } = report;
232
+ const setupPr = shownSetupPr(report.setupPr);
219
233
  // One pull request a screen, each opened in the browser and put on the clipboard as its screen appears: the test drive, then the setup pull request.
220
234
  const hand = { open: openBrowser, copy: copyText };
221
235
  if (testPr && setupPr)
@@ -223,7 +237,7 @@ export async function main(argv) {
223
237
  await showOutro({
224
238
  outcome: testPr ? 'done' : 'partial',
225
239
  title: testPr ? "You're all set up" : 'Settings saved',
226
- result: resultLine(report),
240
+ result: resultLine({ ...report, setupPr }),
227
241
  link: (setupPr ?? testPr)?.url,
228
242
  links: [
229
243
  ...(testPr ? [{ label: 'Test drive, watch the review there', url: testPr.url }] : []),
@@ -232,32 +246,32 @@ export async function main(argv) {
232
246
  { label: 'Web page', url: page },
233
247
  ],
234
248
  todo: report.left,
235
- warnings: saved && !saved.ok ? [`The web page missed the last update (${saved.error}), so it may lag behind this.`] : [],
249
+ warnings: [...removed, ...(saved && !saved.ok ? [`The web page missed the last update (${saved.error}), so it may lag behind this.`] : [])],
236
250
  }, hand);
237
251
  // The web page has the report; when it missed it, the report stays here instead.
238
252
  if (!saved?.ok)
239
- say(`\n${report.markdown.trim()}`);
253
+ say(`\n${plainReport(report.markdown)}`);
240
254
  if (!saved?.ok)
241
255
  reporter.step({ event: 'setup_command_stopped', outcome: 'update_lost' });
242
256
  await reporter.done();
243
257
  process.exit(0);
244
258
  }
245
259
  const waiting = liveSync.payload.phase === 'waiting' ? liveSync.payload.question?.prompt : undefined;
246
- const why = fatal ? `Impeccable stopped: ${fatal}` : waiting ? `Impeccable stopped while waiting for an answer: ${waiting}` : 'Impeccable stopped before the setup finished.';
260
+ const why = pageStatus(fatal ? `Impeccable stopped: ${fatal}` : waiting ? `Impeccable stopped while waiting for an answer: ${waiting}` : 'Impeccable stopped before the setup finished.');
247
261
  const stopped = fatal ? 'model_error' : waiting ? 'unanswered' : cancelled ? 'stopped' : 'failed';
248
262
  liveSync.update({ phase: 'error', status: why, question: null, stop_reason: stopped });
249
263
  const saved = await settle(liveSync.flush(), 5000);
250
264
  if (!saved?.ok)
251
265
  reporter.step({ event: 'setup_command_stopped', outcome: 'update_lost' });
252
266
  const missed = saved?.ok ? [] : [`The web page missed this (${saved?.error ?? 'no answer'}), so it may still show the run as going.`];
253
- const { test_pr: drive, setup_pr: setup } = liveSync.payload;
267
+ const drive = liveSync.payload.test_pr, setup = shownSetupPr(liveSync.payload.setup_pr);
254
268
  await showOutro({
255
269
  outcome: 'stopped',
256
270
  title: cancelled && !fatal ? 'Setup cancelled' : 'Setup failed',
257
271
  result: why,
258
272
  links: [...(drive ? [{ label: 'Test drive', url: drive.url }] : []), ...(setup ? [{ label: 'Setup pull request', url: setup.url }] : []), { label: 'Web page', url: page }],
259
273
  todo: ['Run impeccable-reviews again to carry on. What was saved stays saved; the web page has the details.'],
260
- warnings: missed,
274
+ warnings: [...removed, ...missed],
261
275
  });
262
276
  await reporter.done();
263
277
  process.exit(1);
@@ -289,9 +303,10 @@ export async function main(argv) {
289
303
  }),
290
304
  extensions: [
291
305
  fenceExtension(checkout.root, log, checkout.protectedBranches, scratch, scans),
292
- syncExtension(liveSync, { onFatal: (message) => void (fatal = message), onQuit: finish }),
306
+ syncExtension(liveSync, { onFatal: (message) => void (fatal = message), onQuit: finish }, scans),
293
307
  ],
294
308
  bash: sandbox.shell,
309
+ files: guardedFileOperations(checkout.root, scratch),
295
310
  interactive,
296
311
  runId: run.run_id,
297
312
  transcript: flags.transcript,
@@ -29,7 +29,7 @@ const NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
29
29
  const ID = /^[A-Za-z0-9-]{1,64}$/;
30
30
  /** A password as the header can carry it: one line, of a length a person would type. */
31
31
  const password = (value) => (typeof value === 'string' && value.length > 0 && value.length <= 200 && !/[\r\n\0]/.test(value) ? value : null);
32
- /** The site's protection, read through the CLI; null when it could not say: signed out, or no site of that name in a team of the person's. */
32
+ /** The site's protection, read through the CLI, which is what makes the site the person's; null when it could not say: signed out, no site of that name in a team of the person's, or a site read back under another name. */
33
33
  export async function readSite(run, site) {
34
34
  let read;
35
35
  try {
@@ -38,7 +38,7 @@ export async function readSite(run, site) {
38
38
  catch {
39
39
  return null;
40
40
  }
41
- if (typeof read?.id !== 'string' || !ID.test(read.id) || typeof read.name !== 'string' || !NAME.test(read.name))
41
+ if (typeof read?.id !== 'string' || !ID.test(read.id) || read.name !== site || !NAME.test(site))
42
42
  return null;
43
43
  const given = password(read.password);
44
44
  return { id: read.id, name: read.name, hasPassword: read.has_password === true || given !== null, password: given, teamLogin: read.sso_login === true };
@@ -47,7 +47,8 @@ export async function readSite(run, site) {
47
47
  * Gets the site password in place for the address Netlify's lock stands in front of, and saves only one that opens it.
48
48
  * The site's settings say what the lock is: a password, which the person's yes has the run store when Netlify gives it
49
49
  * back; the team login, which the person's yes has the run lift by making the site public; or neither. When the CLI
50
- * cannot read the site, or Netlify gives no password back, the person may paste the password.
50
+ * cannot read the site, no password goes anywhere, since the site's lock may be another's; when Netlify gives none
51
+ * back for a site it read, the person may paste the password.
51
52
  */
52
53
  export async function passNetlifyLock(deps) {
53
54
  let site = await readSite(deps.run, deps.site);
@@ -71,11 +72,12 @@ export async function passNetlifyLock(deps) {
71
72
  const failed = await deps.save(password);
72
73
  return failed ? { status: 'save-failed', reason: failed, protection: 'password', ...from } : { status: 'saved', password: how, protection: 'password', ...from, value: password };
73
74
  };
75
+ // A site the CLI cannot read as the person is not shown to be theirs: a password typed for it could go to anyone's deploy preview that asks for one.
76
+ if (!site)
77
+ return { status: 'refused', reason: `The Netlify CLI could not read the site ${deps.site} as the person: it is signed out, or the site is in no team of theirs. A password goes only to a site of the person's, so none was asked for or sent.` };
74
78
  /** Why the run has no password to open the preview with, for the paste screen's skip. */
75
79
  let stopped;
76
- if (!site)
77
- stopped = 'The Netlify CLI could not read the site: it is signed out, or the site is in no team of the person\'s.';
78
- else if (site.hasPassword) {
80
+ if (site.hasPassword) {
79
81
  if (!site.password)
80
82
  stopped = 'Netlify does not give the site password back.';
81
83
  else if (!(await deps.consent()))
package/dist/netlify.js CHANGED
@@ -1,14 +1,17 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
5
  import { promisify } from 'node:util';
6
+ import { netlifyPreviewsFact, pickPreviewsFact } from './host-previews.js';
6
7
  import { gitEnvironment } from './sandbox.js';
7
8
  /**
8
9
  * Netlify: the deploy previews this repository's pull requests get (from
9
10
  * GitHub's record and from the Netlify CLI on this machine when it is signed
10
- * in), whether that CLI is signed in, and the connection of a site to this
11
- * repository through Netlify's site settings and its GitHub app. Nothing here
11
+ * in), whether that CLI is signed in, whether a site built from this
12
+ * repository builds Deploy Previews at all (host-previews.ts), and the
13
+ * connection of a site to this repository through Netlify's site settings and
14
+ * its GitHub app. Nothing here
12
15
  * reads a token, and nothing comes back beyond the fields below.
13
16
  */
14
17
  const execFileAsync = promisify(execFile);
@@ -123,11 +126,13 @@ async function fromGitHub(owner, repo, gh, only) {
123
126
  /** What the Netlify CLI on this machine knows, when it is signed in: the sites built from this repository, and their deploy previews. */
124
127
  async function fromCli(owner, repo, netlify, only) {
125
128
  const { cli } = await netlifyStatus(netlify);
129
+ const addresses = new Map();
126
130
  if (cli !== 'signed-in')
127
- return { cli, sites: [], previews: [], failed: cli === 'unknown' };
131
+ return { cli, sites: [], addresses, previews: [], failed: cli === 'unknown' };
128
132
  const slug = `${owner}/${repo}`.toLowerCase();
129
133
  const sites = [];
130
134
  const previews = [];
135
+ const facts = [];
131
136
  let failed = false;
132
137
  try {
133
138
  for (const site of list(JSON.parse(await netlify(['sites:list', '--json'])))) {
@@ -136,6 +141,12 @@ async function fromCli(owner, repo, netlify, only) {
136
141
  if (typeof site.name !== 'string' || typeof site.id !== 'string' || !new RegExp(`^${SITE}$`).test(site.name) || typeof path !== 'string' || path.toLowerCase() !== slug)
137
142
  continue;
138
143
  sites.push(site.name);
144
+ const fact = netlifyPreviewsFact(site);
145
+ if (fact)
146
+ facts.push(fact);
147
+ // The site's own address, its custom domain when it has one: what Netlify prints as ssl_url.
148
+ if (typeof site.ssl_url === 'string' && /^https:\/\/[a-z0-9.-]{1,253}$/i.test(site.ssl_url))
149
+ addresses.set(site.name, site.ssl_url.toLowerCase());
139
150
  try {
140
151
  // The newest 100 deploy previews, newest first: the first deploy of a pull request is its current one.
141
152
  for (const deploy of list(JSON.parse(await netlify(['api', 'listSiteDeploys', '--data', JSON.stringify({ site_id: site.id, 'deploy-previews': true, per_page: 100 })])))) {
@@ -153,8 +164,11 @@ async function fromCli(owner, repo, netlify, only) {
153
164
  catch {
154
165
  failed = true;
155
166
  }
156
- return { cli: 'signed-in', sites, previews, failed };
167
+ const pullRequestPreviews = pickPreviewsFact(facts);
168
+ return { cli: 'signed-in', sites, addresses, previews, failed, ...(pullRequestPreviews ? { pullRequestPreviews } : {}) };
157
169
  }
170
+ /** The site whose address `production` is: the newest preview's, else the first the evidence names. */
171
+ export const liveSite = (evidence) => evidence.previews[0]?.site ?? evidence.sites[0];
158
172
  /** Whether this repository's pull requests get Netlify deploy previews: the sites and the newest previews that GitHub and the signed-in Netlify CLI name; or, with `pull`, that pull request's only. */
159
173
  export async function readNetlifyPreviews(owner, repo, cwd, deps = {}) {
160
174
  if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner) || !/^[A-Za-z0-9_.-]{1,100}$/.test(repo) || repo === '.' || repo === '..') {
@@ -172,11 +186,17 @@ export async function readNetlifyPreviews(owner, repo, cwd, deps = {}) {
172
186
  previews.push(p);
173
187
  // The most recently updated first: an old pull request with a high number is not the newest preview.
174
188
  previews.sort((a, b) => b.at - a.at);
189
+ const sites = [...new Set([...cli.sites, ...previews.map((p) => p.site)])];
190
+ // The live site, at its own domain when the CLI read one, else where Netlify serves every site.
191
+ const live = liveSite({ sites, previews });
175
192
  return {
176
- sites: [...new Set([...cli.sites, ...previews.map((p) => p.site)])],
193
+ sites,
194
+ ...(live ? { production: cli.addresses.get(live) ?? `https://${live}.netlify.app` } : {}),
177
195
  previews: previews.slice(0, NEWEST).map(({ at, sha, ...preview }) => preview),
178
196
  cli: cli.cli,
179
197
  ...(github.failed || cli.failed ? { error: "Some of Netlify's previews could not be read: what is missing is unknown, not absent." } : {}),
198
+ // The site's settings ride with the plain evidence, not with a wait for one pull request's preview.
199
+ ...(deps.pull === undefined && cli.pullRequestPreviews ? { pullRequestPreviews: cli.pullRequestPreviews } : {}),
180
200
  };
181
201
  }
182
202
  const CLONE_WAIT = { totalMs: 60_000, everyMs: 5_000 };
@@ -195,9 +215,9 @@ async function pause(ms, deps) {
195
215
  }
196
216
  }
197
217
  /** The site this folder is linked to, as `netlify link` and `sites:create` leave it in .netlify/state.json. */
198
- async function linkedSiteId(cwd) {
218
+ export function linkedNetlifySite(cwd) {
199
219
  try {
200
- const id = JSON.parse(await readFile(join(cwd, '.netlify', 'state.json'), 'utf8')).siteId;
220
+ const id = JSON.parse(readFileSync(join(cwd, '.netlify', 'state.json'), 'utf8')).siteId;
201
221
  return typeof id === 'string' && /^[A-Za-z0-9-]{1,64}$/.test(id) ? id : undefined;
202
222
  }
203
223
  catch {
@@ -252,11 +272,12 @@ export async function waitForNetlifyAccess(siteId, deps) {
252
272
  * when a repository is linked: the repository, its default branch, and the installation of Netlify's GitHub app on the
253
273
  * repository's GitHub account, which Netlify names on every site of the person's that the app already builds from that
254
274
  * account. GitHub sends the app each push and pull request, so the pull requests build deploy previews. Then the first
255
- * build says whether the app reads the repository.
275
+ * build says whether the app reads the repository. Only a site this run's own sites:create made takes the connection,
276
+ * and a site built from another repository keeps it: the person's sites are never pointed elsewhere.
256
277
  */
257
278
  export async function connectNetlifySite(owner, repo, cwd, deps) {
258
279
  const not = (reason, more = {}) => ({ connected: false, reason, ...more });
259
- const siteId = await linkedSiteId(cwd);
280
+ const siteId = linkedNetlifySite(cwd);
260
281
  if (!siteId)
261
282
  return not('This folder is linked to no Netlify site: link or create one first.');
262
283
  let site;
@@ -284,10 +305,16 @@ export async function connectNetlifySite(owner, repo, cwd, deps) {
284
305
  if (typeof settings?.repo_path === 'string' && settings.repo_path.toLowerCase() === slug) {
285
306
  // Connected before this run: its newest build says whether the app still reads the repository.
286
307
  const reads = await netlifyReadsRepository(siteId, 0, { ...deps, wait: { totalMs: 0, everyMs: 0 } });
308
+ const fact = netlifyPreviewsFact(site);
309
+ const previews = fact ? { pullRequestPreviews: fact } : {};
287
310
  return reads === 'no' && positive(settings.installation_id)
288
- ? not("Netlify's GitHub app cannot read the repository.", { site: linked, how: 'already', access: accessPage(settings.installation_id) })
289
- : { connected: true, site: linked, how: 'already' };
311
+ ? not("Netlify's GitHub app cannot read the repository.", { site: linked, how: 'already', access: accessPage(settings.installation_id), ...previews })
312
+ : { connected: true, site: linked, how: 'already', ...previews };
290
313
  }
314
+ if (typeof settings?.repo_path === 'string' && settings.repo_path)
315
+ return not('The linked site is built from another repository, and stays so: link a site built from this repository, or create one with netlify sites:create.', { site: linked });
316
+ if (!deps.linked.has(siteId))
317
+ return not('The linked site was not created in this run: only a site netlify sites:create made here, or one built from this repository already, takes the connection.', { site: linked });
291
318
  const branch = github?.default_branch;
292
319
  const fullName = typeof github?.full_name === 'string' && github.full_name.toLowerCase() === slug ? github.full_name : undefined;
293
320
  if (!github || !positive(github.id) || typeof branch !== 'string' || !branch || !fullName)
@@ -330,7 +357,9 @@ export async function connectNetlifySite(owner, repo, cwd, deps) {
330
357
  }
331
358
  if (record(updated?.build_settings)?.repo_path !== fullName)
332
359
  return not('Netlify did not keep the repository in the site settings.', { site: linked });
360
+ const fact = updated && netlifyPreviewsFact(updated);
361
+ const previews = fact ? { pullRequestPreviews: fact } : {};
333
362
  return (await netlifyReadsRepository(siteId, since, deps)) === 'no'
334
- ? not("Netlify's GitHub app cannot read the repository.", { site: linked, how: 'github-app', access: accessPage(installation) })
335
- : { connected: true, site: linked, how: 'github-app' };
363
+ ? not("Netlify's GitHub app cannot read the repository.", { site: linked, how: 'github-app', access: accessPage(installation), ...previews })
364
+ : { connected: true, site: linked, how: 'github-app', ...previews };
336
365
  }
@@ -0,0 +1,69 @@
1
+ import { accessSync, constants } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * How pi's file tools turn the model's path string into the path they open, mirrored from @earendil-works/pi-coding-agent
7
+ * (normalizePath and resolvePath in dist/utils/paths.js, resolveToCwd and resolveReadPath in dist/core/tools/path-utils.js),
8
+ * which the package does not export. The fence judges the path pi opens, never the string the model typed, so each of
9
+ * these is held against pi's own in test/pi-paths.test.ts: a pi upgrade that changes them fails there.
10
+ */
11
+ const UNICODE_SPACES = /[  -    ]/g;
12
+ /** Git Bash, MSYS, Cygwin and WSL drive paths, as pi takes them on Windows. */
13
+ export function windowsShellPath(path) {
14
+ if (!path.startsWith('/') || path.startsWith('//') || path.includes('\\'))
15
+ return path;
16
+ const match = /^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i.exec(path);
17
+ if (!match)
18
+ return path;
19
+ const suffix = match[2]?.replaceAll('/', '\\');
20
+ return `${match[1].toUpperCase()}:\\${suffix ?? ''}`;
21
+ }
22
+ function normalizePath(input, platform, tool) {
23
+ let normalized = tool ? input.replace(UNICODE_SPACES, ' ') : input;
24
+ if (tool && normalized.startsWith('@'))
25
+ normalized = normalized.slice(1);
26
+ if (platform === 'win32')
27
+ normalized = windowsShellPath(normalized);
28
+ const home = homedir();
29
+ if (normalized === '~')
30
+ return home;
31
+ if (normalized.startsWith('~/') || (platform === 'win32' && normalized.startsWith('~\\')))
32
+ return join(home, normalized.slice(2));
33
+ if (/^file:\/\//.test(normalized))
34
+ return fileURLToPath(normalized);
35
+ return normalized;
36
+ }
37
+ /** The path pi's write, edit, grep, find and ls tools open for the model's string. */
38
+ export function resolveToCwd(path, cwd, platform = process.platform) {
39
+ const normalized = normalizePath(path, platform, true);
40
+ return isAbsolute(normalized) ? resolve(normalized) : resolve(normalizePath(cwd, platform, false), normalized);
41
+ }
42
+ const exists = (path) => {
43
+ try {
44
+ accessSync(path, constants.F_OK);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ };
51
+ /** The path pi's read tool opens: the string's own, or, when that is not there, the first of its spellings that is (a narrow no-break space before AM/PM, NFD, a curly apostrophe, both). */
52
+ export function resolveReadPath(path, cwd, platform = process.platform) {
53
+ const resolved = resolveToCwd(path, cwd, platform);
54
+ if (exists(resolved))
55
+ return resolved;
56
+ const amPm = resolved.replace(/ (AM|PM)\./gi, ' $1.');
57
+ if (amPm !== resolved && exists(amPm))
58
+ return amPm;
59
+ const nfd = resolved.normalize('NFD');
60
+ if (nfd !== resolved && exists(nfd))
61
+ return nfd;
62
+ const curly = resolved.replace(/'/g, '’');
63
+ if (curly !== resolved && exists(curly))
64
+ return curly;
65
+ const nfdCurly = nfd.replace(/'/g, '’');
66
+ if (nfdCurly !== resolved && exists(nfdCurly))
67
+ return nfdCurly;
68
+ return resolved;
69
+ }
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- Cloudflare Pages deploys previews of the pull requests: impeccable_cloudflare_previews lists a built preview of a pull request (`state` success), with its address and the branch's alias. Ask it only where the repository is set up for Cloudflare (`wrangler.toml`, `wrangler.json` or `wrangler.jsonc`, a `wrangler` or `@cloudflare/*` package, a `.pages.dev` address in the README or the workflows), but its record decides, not the checkout: Cloudflare's GitHub app posts a check run on each commit it built and a comment on the pull request, and cf, Cloudflare's CLI, lists the project's deployments when it is signed in here. Cloudflare 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.
3
+ Cloudflare Pages deploys previews of the pull requests: impeccable_cloudflare_previews lists a built preview of a pull request (`state` success), with its address and the branch's alias. Ask it only where the repository is set up for Cloudflare (`wrangler.toml`, `wrangler.json` or `wrangler.jsonc`, a `wrangler` or `@cloudflare/*` package, a `.pages.dev` or `.workers.dev` address in the README or the workflows), but its record decides, not the checkout: Cloudflare's GitHub app posts a check run on each commit it built and a comment on the pull request, and cf, Cloudflare's CLI, lists the project's deployments when it is signed in here. It applies as well where the checkout is set up for Cloudflare and no pull request got a preview: the answer then says why (`pullRequestPreviews`, `worker`, `githubApp`), and "No pull-request previews" below says what the run does about each cause, in that order, before any other preview is set up. Cloudflare 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
5
  ### Steps
6
6
 
@@ -9,9 +9,17 @@ Cloudflare Pages deploys previews of the pull requests: impeccable_cloudflare_pr
9
9
 
10
10
  ### Reference
11
11
 
12
+ - **The live app**: `production` in the evidence is the project's own address, `https://<project>.pages.dev`; live-app-address says how it becomes the before, in step 2's write.
12
13
  - **Behind Cloudflare Access** (`gate: "cloudflare-access"`): Cloudflare's login 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.
13
14
  - **Another app, or no answer from any of the three**: Cloudflare'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 Cloudflare address answered.
14
- - **Nothing listed**: Cloudflare's previews are not on record, so set the preview up as server-previews or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
15
+ - **No pull-request previews**: the checkout is set up for Cloudflare and nothing is listed, or a project's settings build none. The aim holds: find out why from the answer, in this order, and for each cause ask once with the tool that fixes it, before anything is pushed and never a second time; with a preview listed, after step 1's proof and preview-protection's steps, so the person sees the lock's question and this one back to back, at most one each. Each tool shows its one screen and does the change as the person, or opens the page where it cannot. After `enabled` or `connected`, nothing is listed yet, so the test drive's build is the first preview: take pull-requests' step 4 the Cloudflare way and save the method on its alias as step 2 says, no setup pull request.
16
+ 1. **Previews off** (`pullRequestPreviews` with `enabled: false` and `cause` `previews` or `deployments`, with its `reason`): cf, signed in, found the Pages project built from this repository, and its settings build no previews of pull requests. impeccable_enable_previews (`host: "cloudflare"`) turns the preview branch control on; deployments off altogether is the person's change, on the page it opens.
17
+ 2. **Not connected** (`cause: "source"`): the Pages project the checkout names (its wrangler config, or the README's pages.dev address) exists with no Git repository connected, so no push builds. impeccable_cloudflare_connect connects it to this repository.
18
+ 3. **A Worker, not a Pages project** (`worker` in the answer, and no `pullRequestPreviews`): Cloudflare builds the repository as a Worker (Workers Builds: its check runs name the script, or the wrangler config or the README's workers.dev address does), which gives pull requests no preview, and a Worker with static assets no branch build at all. impeccable_cloudflare_connect, with the build command and output folder the checkout runs, creates a Pages project for the repository and connects it; the Worker stays as it is. The same tool creates the project where the config names a Pages project cf finds nowhere.
19
+ 4. **Cloudflare's GitHub app cannot see the repository** (`githubApp` in the answer: the app's installation covers selected repositories, or `installed: false`): a project connected with previews on still gets no build, since the app builds only the repositories its installation names. Nothing in the account shows it, so the wait for the test drive's build tells: when none comes, the wait's own screen opens the installation page (`githubApp.page`) and waits again on the person's yes. Its `reason` says when they skipped it or no build came after the retries.
20
+ - **`skipped` or `refused` from any of these**: push no test drive on Cloudflare (pull-requests' "No preview, no test drive" where a preview was proven; else set the preview up as server-previews or static-site says, never without saying so) and say under "What was skipped, and why" which cause stopped it, with the `reason`, and name the `page` where there is one; the person was asked in this run, so `left` stays empty.
21
+ - **No `pullRequestPreviews`, no `worker`** (`cli` signed out or unknown, or no Pages project built from this repository among the accounts cf reaches): the check could not be made. Say so in one line under "What is not verified", naming why, and go on as the aim says: nothing listed, as "Nothing listed" says; a preview listed, as the steps say, where the wait for the test drive's build then tells.
22
+ - **Nothing listed**, and neither `pullRequestPreviews` nor `worker` nor a project the checkout names: Cloudflare's previews are not on record, so set the preview up as server-previews or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
15
23
  - **The pull requests**: no setup pull request; the test drive branches from the default branch, and Cloudflare builds every pushed branch, so pull-requests' step 4 goes the Vercel way with impeccable_cloudflare_previews in place of impeccable_hosted_previews: with `branch: "impeccable/test-drive"` it waits up to five minutes for the check run on the pushed head, under a status line with its timer, and the test drive's page is proven on the alias it lists before the pull request opens. In place of pull-requests' step 3, build the test drive branch once with the build Cloudflare runs (the Pages project's build command, which the checkout's `package.json` `build` script usually is), so a page that does not build is never pushed.
16
- - The evidence is read only: link, deploy and change nothing on Cloudflare. The one Cloudflare sign-in is impeccable_cloudflare_access's, behind Access, on the person's yes. Cloudflare does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern, the branch's alias: save the pattern the proof answered, and no other.
17
- - The report names the address that answered with this app under "What we found", and the pattern saved under "What was configured, and by whom". The test drive's own preview, which Cloudflare builds after the run, goes under "What is not verified".
24
+ - The evidence is read only: link, deploy and change nothing on Cloudflare beyond what impeccable_enable_previews and impeccable_cloudflare_connect do on the person's yes. The Cloudflare sign-ins are those tools' own, on the person's yes. Cloudflare does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern, the branch's alias: save the pattern the proof answered, and no other.
25
+ - The report names the address that answered with this app under "What we found" (and the Worker, where Cloudflare builds one), and the pattern saved under "What was configured, and by whom", with the pull-request previews turned on when they were (`by` says whether the run or the person did), the Pages project created or connected (`created` says which; name its `subdomain` address) and the access given to Cloudflare's GitHub app, since the account changed. The test drive's own preview, which Cloudflare builds after the run, goes under "What is not verified".
@@ -6,7 +6,7 @@ A rerun: the setup pull request an earlier run opened has a preview job that fai
6
6
 
7
7
  1. **Record** the earlier pull requests with impeccable_test_pr: the open test drive, and the setup pull request in `setup_pr`. That binding cannot change. Done when it answers "Recorded".
8
8
  2. **Read the attempts** with impeccable_preview_run. Done when you know the failed attempt's `stage` and `reason`, the `budget`, and that the state's `lastRun` for the app is the same run.
9
- 3. **Reproduce and correct**: run the failed step again with impeccable_verify_preview and find the smallest correction (see the table). Done when the correction passes locally, or no safe one exists: then push nothing more, and report.
9
+ 3. **Reproduce and correct**: run the failed step again with impeccable_verify_preview and find the smallest correction (see the table). Where the check is skipped on this machine (its answer says it has no sandbox for it), reason from the attempt's `stage`, the table and the repository's files instead, and the next attempt is what proves the correction. Done when the correction passes locally, or, with the check skipped, is the smallest the table gives: then step 4. When no safe correction exists, push nothing more, and report.
10
10
  4. **Push one correction**: save it with impeccable_write to `preview` (with `appKey`), impeccable_sync_workflow, commit on the setup branch, push once, then impeccable_preview_run again. Done when the new attempt shows in `budget`.
11
11
  5. **Check it once** with impeccable_write to `latest-pull-check`: `verified: true` means this setup pull request's preview answered at the pushed head, which a passed job alone does not prove. A 409 `no-preview-running` or a job still starting is expected. Done when it answered; report the outcome you saw.
12
12
  6. **Open a new test drive** from the corrected setup branch, with the default branch as its base (the earlier one ran on the old head), record it with impeccable_test_pr beside the same setup pull request, then impeccable_report. Done when impeccable_report answered.
@@ -0,0 +1,15 @@
1
+ ### When this applies
2
+
3
+ Every run that saves a preview method, for the state's `before` item: the address of the live app, which every review then shows beside the change. The item reads `optional` until an address is saved, so the checkup never waits on it; the run sets it all the same, from what the host or the repository already says, and asks nobody: the host's own address of the connected project changes nothing of the person's. Where Impeccable builds the previews itself (static-site), its build of the default branch is the before and the item reads done on its own: set nothing.
4
+
5
+ ### Steps
6
+
7
+ 1. **Find the address**: the `production` address a host tool answered (impeccable_netlify_previews, the Netlify site's own; impeccable_cloudflare_previews, the Pages project's, `https://<project>.pages.dev`; impeccable_hosted_previews, the linked Vercel project's own domain where GitHub records a production deployment of it; impeccable_vercel_connect's `productionUrl` where production exists), else the live-site line the item's `proposal` read in the README, checked against the file, when it is the site's or project's own address on one of the hosts (an address elsewhere is only the host tools' to name). A project or site created in this run has no live app yet. Done when you have one address, or know there is none.
8
+ 2. **Prove it** with impeccable_prove_preview, under the status line *"Opening the live app"*: that address, or a page on it a visitor opens without signing in, and words the page shows that the checkout holds. A key the run already got in place (Vercel's bypass secret, Netlify's site password, Cloudflare's service token) goes with it where the lock is the same project's or site's. Done when it answers `proven: true`. **An address no key opens is not set**: a `gate`, the app's own sign-in on every page, or no answer leaves the item as it is, and the report says what the address answered.
9
+ 3. **Save it** with impeccable_write to `preview`: `beforeUrl` the proven address, in the same write as the method where the proof came before it (`{"intendedMethod": "preview-deploy", "beforeUrl": "https://…"}`), else in a write of its own; `appKey` the app's on a repository of several apps, one address per app. Done when the answer carries `beforeUrl`; its `beforeCheck.access` says how Impeccable's own check opened it: `ok`; `auth-wall`, the app's sign-in, which the saved test accounts get past; anything else goes under "What is not verified".
10
+
11
+ ### Reference
12
+
13
+ - **The report** names the address under "What was configured, and by whom" (*The live app's address, https://…, as the before of every review, by the setup agent.*), or, with none set, says under "What was skipped, and why" what stopped it: no live app yet, no address known, or what the address answered.
14
+ - Never save a preview's address (one deployment's, with a hash or a branch in its name), a placeholder, or an address the proof did not answer with this app: impeccable_write refuses an unproven one. Clear nothing: an address the person saved stays.
15
+ - **Status lines** belong to the Preview method task.
@@ -17,7 +17,7 @@ The state's repository has an `apps` item: the scan proposed several apps, folde
17
17
 
18
18
  ### Reference
19
19
 
20
- - **The folders**: the install runs where the manifest the lockfile belongs to is (`build.installDirectory` in the state: the root of a workspace with one lockfile), the build and the start in the app's own folder (`build.workingDirectory`, the `directory` the preview item's `apps` names). impeccable_verify_preview takes no `appKey`: give each step its `directory` accordingly, and `output.directory` from the checkout's root; the saved `outputDir` is relative to the app's folder (`.` when its files are the pages), since the job uploads from there.
20
+ - **The folders**: the install runs where the manifest the lockfile belongs to is (`build.installDirectory` in the state: the root of a workspace with one lockfile), the build and the start in the app's own folder (`build.workingDirectory`, the `directory` the preview item's `apps` names). impeccable_verify_preview takes no `appKey`: give each step its `directory` accordingly, and `output.directory` from the checkout's root; the saved `outputDir` is relative to the app's folder (`.` when its files are the pages), since the job uploads from there. Where the check is skipped on this machine (static-site says when), each app's job proves its build on the pull request.
21
21
  - **Ask nothing else about apps**: names, a split or a merge are the person's on the setup page. A proposal that looks wrong (a shared package, a missing folder) goes into the report, never into a `split`.
22
22
  - **Status lines** name the app (*"Proving the storefront's preview"*), under the playbook's tasks.
23
23
  - The checkup's entries for an app read "<repository> (<app>): …", and its `apps` check is answered by step 1.
@@ -9,10 +9,12 @@ Netlify deploys previews of the pull requests: impeccable_netlify_previews lists
9
9
 
10
10
  ### Reference
11
11
 
12
+ - **The live app**: `production` in the evidence is the site's own address; live-app-address says how it becomes the before, in step 2's write.
12
13
  - **The aim** holds here: the CLI does the work, and the person is asked only what the playbook's aim allows.
13
14
  - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the site password, or Netlify's team login, stands in front of the previews. That is the preview-protection case's, whose steps come before step 2: on the person's yes the password is stored, or the site behind the team login made public.
15
+ - **Deploy Previews off now** (`pullRequestPreviews` in the answer, `enabled: false`, with its `reason`): the site built previews before, and its settings build none now. The aim holds: ask once with impeccable_enable_previews (`host: "netlify"`), after step 1's proof and preview-protection's steps and before the test drive is pushed, so the person sees the lock's question and this one back to back, at most one each. `enabled`: go on. `skipped` or `refused`: push no test drive (pull-requests' "No preview, no test drive"), and say under "What was skipped, and why" that Deploy Previews are off for the site, with the `reason`, and name the `page`; `left` stays empty, since the person was asked in this run. `enabled: 'unknown'`: say so in one line under "What is not verified" and go on.
14
16
  - **Another app, or no answer from any of the three**: Netlify'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 Netlify address answered.
15
17
  - **Nothing listed**: Netlify's previews are not on record. Where the checkout is set up for Netlify that is the netlify-setup case's; else set the preview up as server-previews or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
16
18
  - **The pull requests**: no setup pull request; the test drive branches from the default branch. Netlify builds a deploy preview only for a pull request, so the test drive opens as a draft, its deploy preview is waited for and the test drive's page proven on it, and then the draft is marked ready: pull-requests' step 4. In place of pull-requests' step 3, build the test drive branch once with the build Netlify runs (`netlify.toml`'s `[build]` `command`, else the package's `build` script), so a page that does not build is never pushed.
17
- - The evidence is read only: link, deploy and change nothing on Netlify, beyond what impeccable_netlify_password does on the person's yes. Netlify does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern: save the pattern the proof answered, and no other.
19
+ - The evidence is read only: link, deploy and change nothing on Netlify, beyond what impeccable_netlify_password and impeccable_enable_previews do on the person's yes. Netlify does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern: save the pattern the proof answered, and no other.
18
20
  - The report names the address that answered with this app under "What we found", and the pattern saved under "What was configured, and by whom". The test drive's own preview, which Netlify builds after the run, goes under "What is not verified".
@@ -4,22 +4,23 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
4
4
 
5
5
  ### Steps
6
6
 
7
- 1. **Sign in**, before any other preview work, with impeccable_ask: `kind: "confirm"`, `id: "netlify-setup"`. The tool does the rest: it shows the sign-in screen only when the Netlify CLI is signed out (a person signed in already is asked nothing), runs the login in the browser and offers a retry. Done when it answered `confirmed`.
8
- - **`cli: "missing"`**: install the CLI with the person's package manager (`npm install -g netlify-cli`, `pnpm add -g netlify-cli`, `yarn global add netlify-cli` or `bun add -g netlify-cli`), then ask again: the screen shows no second time.
7
+ 1. **Sign in**, before any other preview work, with impeccable_ask: `kind: "confirm"`, `id: "netlify-setup"`. The tool does the rest: it shows the setup screen once (a person signed in already confirms the setup with that sign-in on it, with no login to run), runs the login in the browser when the Netlify CLI is signed out and offers a retry. Done when it answered `confirmed`.
8
+ - **`cli: "missing"`**: install the CLI with `npm install -g netlify-cli@27.11.2`, 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** (the person has several Netlify teams): ask once which owns the site, `kind: "single"`, `id: "netlify-team"`, `options` the teams as given (the tool words the rest), and pass the pick to `sites:create` as `--account-slug`.
10
10
  - **Skip**: set the preview up as server-previews or static-site says, and say under "What was skipped, and why" that Netlify was found and the sign-in was skipped. The steps below do not apply.
11
- 2. **Link or create the site**: `netlify link --git-remote-url <the GitHub origin>` links a site Netlify already builds from this repository; when it finds none, `netlify sites:create --name <name>` creates one in the person's team and links this folder. Name it after the repository: lowercase letters, digits and hyphens, at most 40 characters; a name Netlify says is taken gets `-2`. Done when `netlify status` names the linked project.
12
- 3. **Connect the repository** with impeccable_netlify_connect: it connects the linked site to this repository through Netlify's GitHub app, so pull requests build deploy previews, and asks the person for the app's access when Netlify cannot read the repository. Give `command` and `publish` only where no `netlify.toml` sets the build. Done when it answered `connected: true`; a site `netlify link` found is connected already, and it says so.
11
+ 2. **Link or create the site** (run each Netlify command plain, on its own: a command chained after `sites:create` leaves the site unrecorded): `netlify link --git-remote-url <the GitHub origin>` links a site Netlify already builds from this repository; when it finds none, `netlify sites:create --name <name>` creates one in the person's team and links this folder. Name it after the repository: lowercase letters, digits and hyphens, at most 40 characters; a name Netlify says is taken gets `-2`. Done when `netlify status` names the linked project.
12
+ 3. **Connect the repository** with impeccable_netlify_connect: it connects the linked site to this repository through Netlify's GitHub app, so pull requests build deploy previews, when that site is the one this run created or one built from this repository already (a site built from another repository, or one the run did not create, is left as it is, with the reason), and asks the person for the app's access when Netlify cannot read the repository. Give `command` and `publish` only where no `netlify.toml` sets the build. Done when it answered `connected: true`; a site `netlify link` found is connected already, and it says so. **Deploy Previews off** on that site (`pullRequestPreviews` in its answer, or in impeccable_netlify_previews', `enabled: false`): the aim holds, so ask once with impeccable_enable_previews (`host: "netlify"`) before step 4, and never a second time. `enabled`: step 4. `skipped` or `refused`: the setup ends here, as "A step fails before the test drive is pushed" says, with the `reason` and the `page` in the report, and `left` empty, since the person was asked in this run.
13
13
  4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say, with the changes netlify-previews' reference gives: from the default branch, no setup pull request, built once with the build Netlify runs. Stage the new page and the page that links to it by name: `netlify sites:create` writes `.netlify/` and may add it to `.gitignore`, and that change stays out of the pull request. Done when `git push -u origin impeccable/test-drive` succeeded.
14
14
  5. **Its draft, its preview proven and saved, then marked ready** as pull-requests' step 4 (Netlify) and step 5 say: the draft, whose deploy preview is the site's first, the wait for it by `pull`, the test drive's page proven on it, impeccable_write to `preview` as netlify-previews' step 2 says, `gh pr ready`, then the recording. The playbook's step 6 then has no pull request left to open: go on with the playbook's step 4. Done when impeccable_test_pr answered "Recorded".
15
15
 
16
16
  ### Reference
17
17
 
18
+ - **The live app**: a site created here has none yet, so the before stays unset, which the report says (live-app-address).
18
19
  - **The aim** holds here: the CLI does the work, and the person is asked only what the playbook's aim allows.
19
20
  - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the site not created, the repository not connected): the Netlify 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 Netlify's GitHub app access to the repository", with the `access` page in the report.
20
21
  - **No preview for the draft**: save no method, set up no other preview, and leave the draft as it is, unrecorded, as pull-requests' "No preview, no test drive" says; the site and the connection stay, and the report names them.
21
22
  - **The build failed** (`state` is `error` or `failure`): say so under "What is not verified", with the command that builds this linked checkout as Netlify does and prints why, for the person to run: `netlify deploy --build --context deploy-preview`. It never runs here, since it builds with their Netlify sign-in.
22
- - **No deploy preview within five minutes**: the wait's own screen opens the site's deploy settings, where Deploy Previews are turned on, and waits again on the person's yes. When they skip it, or none came after its retries, say under "What is not verified" that Netlify built no deploy preview here, and that Deploy Previews may be off in the site's settings.
23
+ - **No deploy preview within five minutes**: the wait's own screen opens the site's deploy settings, where Deploy Previews are turned on, and waits again on the person's yes (a site whose settings said they were off was asked about in step 3, so this wait is for a build, not a setting). When they skip it, or none came after its retries, say under "What is not verified" that Netlify built no deploy preview here, and that Deploy Previews may be off in the site's settings.
23
24
  - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the preview-protection case's, whose steps come before the save in step 5. A team that keeps new sites private puts Netlify's team login in front of the new site's previews: the tool makes the site public on the person's yes, and step 5 goes on once it answers `opened`.
24
- - **The report** names the Netlify site under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Netlify account, with how the repository was connected and the CLI's install when there was one. The site's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
25
+ - **The report** names the Netlify site under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Netlify account, with how the repository was connected, Deploy Previews turned on when they were (`by` says whether the run or the person did), and the CLI's install when there was one. The site's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
25
26
  - **Status lines**: the sign-in, the link and the connection belong to the Preview method task; from the page on, the Test drive task, so name the test drive in each line about its build, its push, the draft, the wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Netlify's login as a login, since a line about signing in belongs to the Sign-in task.
@@ -9,7 +9,8 @@ impeccable_prove_preview answered a `gate`: the host's own lock stands in front
9
9
  - `opened` (Netlify): the site is public now and the preview proven (`proven: true`); `madePublic` says whether the run or the person made it public, and `site` which site. Step 2.
10
10
  - `login` (Netlify): Netlify kept its team login in front through the retries; `reason` says so. Step 3.
11
11
  - Any answer with `madePublic` but `opened`: the run made the Netlify site public and it stays so, though the preview did not open; the report names that change as the bullet below says, whatever step comes next.
12
- - `refused` whose `reason` says to prove a page a visitor opens: Netlify's own site password opened neither the header nor Netlify's form, so the page you proved asks for the app's own sign-in past the lock, or Netlify did not answer. Prove a page a visitor opens without signing in, as netlify-previews' step 1 says, and call the tool again; the consent is not asked a second time. Refused again, step 3.
12
+ - `refused` with `protection: "none"` (Netlify: the site's settings show neither a password nor the team login, or the site is public now, `madePublic` saying who made it so, and the page past the login is not the app's), or on Vercel whose `reason` says the settings show no Deployment Protection: the page you proved is the app's own sign-in or another page, however it is worded, and the previews are open. Prove a page a visitor opens without signing in, as vercel-previews' or netlify-previews' step 1 says, and on `proven: true` go on with that case's step 2, the save, calling this tool no more: there is no lock to get past. No such page proven: the host's previews do not show this app to a visitor, so go on as that case's "Another app, or no answer" says (set the preview up as server-previews or static-site says, and say under "What is not verified" what the address answered), never step 3, whose lock the settings show is not there.
13
+ - Any other `refused` on Netlify whose `reason` says to prove or try a page a visitor opens, or that the page asks for the app's own sign-in past the lock: the site password (Netlify's own, or the one typed) got past the lock to the app's own sign-in on the page you proved, or opened neither the header nor Netlify's form, or Netlify did not answer. Prove a page a visitor opens without signing in, as netlify-previews' step 1 says, and call the tool again; the consent is not asked a second time. Refused again, step 3.
13
14
  - Any other `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
14
15
  - `save-failed`: the setup API did not take it all; `reason` says how far it got. Call the tool once more; when it fails again, step 3, with "Save the Vercel bypass secret on the setup page", "Save the Netlify site password on the setup page" or "Save the Cloudflare service token on the setup page" in `left`.
15
16
  2. **Check it from Impeccable's side**: impeccable_write to `connection-check` with `{"appKey": "main"}`, and report what it answered. Then go on with the case that sent you here: the proof stands, so save the method as vercel-previews', netlify-previews' or cloudflare-previews' step 2 says, Cloudflare's with the tool's `pattern`. Done when the method is saved.
@@ -6,8 +6,8 @@ Every run, once the settings are saved. The person merges the setup pull request
6
6
 
7
7
  1. **The setup commit**, when the repository needs a change to keep previews working: the workflow and any fix the build needed, nothing else. With the workflow current on the default branch and no fix, there is none. Check access as workflow-as-person says, branch `impeccable/setup` from `origin/<defaultBranch>`, and bring in the workflow and commit as that case says. Push it in step 7, after the test drive. Done when that case's steps are done.
8
8
  2. **The test drive page**, always. The test drive shows the person a whole review: pages to capture, a finding for each measure, and a new page, which is what starts the review's visitor sessions. Branch `impeccable/test-drive` from the setup commit, so it carries the same workflow and its preview runs from its own head (from the default branch when there is no setup commit). Add one small, self-contained page in the repository's own framework, such as `/impeccable-test-drive`: a file where the routing finds pages, plus its styles, with a heading, a paragraph and a few elements in the app's own words (so the review judges it as one of the app's pages), and the flaws below. Link it from a page **a visitor opens without signing in**, the landing page when it is one, and keep the new page open without signing in too: no guard, on the server or in the browser, sends it to the sign-in. Done when the new page carries every flaw below with its comment, and the page you picked links to it.
9
- 3. **Prove both pages** with impeccable_verify_preview, for a preview Impeccable builds itself (static-site): the input that proved the preview, with both pages in `entries`. A host's preview (vercel-previews, vercel-setup, netlify-previews, netlify-setup) is proven on the host's own build in step 4 instead; its case says how the branch is built here first. Done when it answers `verified: true`, or the preview is a host's.
10
- 4. **Push the test drive and prove its preview**: commit the new page **and** the page that links to it, each staged by name, `git push -u origin impeccable/test-drive`, then the way its preview comes:
9
+ 3. **Prove both pages** with impeccable_verify_preview, for a preview Impeccable builds itself (static-site): the input that proved the preview, with both pages in `entries`. A host's preview (vercel-previews, vercel-setup, netlify-previews, netlify-setup) is proven on the host's own build in step 4 instead; its case says how the branch is built here first. Done when it answers `verified: true`, or the preview is a host's, or the check is skipped on this machine (its answer says it has no sandbox for it): then the workflow's job proves both pages on the pull request, under "What is not verified".
10
+ 4. **Push the test drive and prove its preview**: a host whose evidence said `pullRequestPreviews.enabled: false` is asked first, once, as its case says (impeccable_enable_previews; skipped or refused, push nothing: "No preview, no test drive" below); then commit the new page **and** the page that links to it, each staged by name, `git push -u origin impeccable/test-drive`, then the way its preview comes:
11
11
  - **Vercel** (vercel-previews, vercel-setup) builds every push: impeccable_hosted_previews with `branch: "impeccable/test-drive"` waits up to five minutes for its deployment of the pushed head, under a status line with its timer. Prove the test drive's page on that address with impeccable_prove_preview (past Vercel's login as preview-protection says), and save the method as the case says. Then step 5.
12
12
  - **Netlify** (netlify-previews, netlify-setup) builds a deploy preview only for a pull request: open the test drive now as a **draft**, step 5's `gh pr create` with `--draft`, which Impeccable leaves unreviewed. impeccable_netlify_previews with `pull` the number it printed waits up to five minutes for the deploy preview, under a status line with its timer. Prove the test drive's page on it, save the method as the case says, then `gh pr ready impeccable/test-drive --repo <owner>/<repo>` marks it ready, which starts the review. Then step 5's recording.
13
13
  - **Impeccable's own preview** (static-site): the workflow in the branch builds it on the pull request, and Impeccable's review holds up to eight minutes for that job before it renders, so step 3's proof and the workflow in the branch (from the setup commit) are all this step takes. Then step 5.
@@ -26,6 +26,6 @@ Every run, once the settings are saved. The person merges the setup pull request
26
26
  - **Low-contrast text**: text in a solid light grey, well under 4.5:1 against its background.
27
27
  - **A layout transition**: `transition: width 0.3s`, naming the layout property.
28
28
  - **Body text at the viewport edge**: a paragraph of more than 40 characters in the main content, without a background, flush with the sides of the viewport.
29
- - **No preview, no test drive**: when step 4 ends with the preview unproven (the host's build failed or none came within the five minutes, the host's login stayed in front, step 3 failed), open no test drive: mark the Test drive task failed, say under "What is not verified" what was missing and what the person can do, and put their step in `left`. The setup pull request still opens in step 7 when there is a setup commit. A Netlify draft stays open, unrecorded: name it in the report. Put "Mark the test drive #N ready once Netlify builds its preview" in `left` only where Netlify built no preview; where it built one and the lock stayed in front, preview-protection's step 3 says what the report says instead.
29
+ - **No preview, no test drive**: when step 4 ends with the preview unproven (the host's build failed or none came within the five minutes, the host's login stayed in front, step 3 failed), open no test drive: mark the Test drive task failed, say under "What is not verified" what was missing and what the person can do, and put their step in `left` (on Cloudflare, cloudflare-previews' "No pull-request previews" names the cause and the page; a step the person was asked about in this run goes in the report, not in `left`). The setup pull request still opens in step 7 when there is a setup commit. A Netlify draft stays open, unrecorded: name it in the report. Put "Mark the test drive #N ready once Netlify builds its preview" in `left` only where Netlify built no preview; where it built one and the lock stayed in front, preview-protection's step 3 says what the report says instead.
30
30
  - **A single-page app**, whose routes live in the browser: the new page is a self-contained HTML file in the folder the build copies as it is (`public/` in Vite), so the output holds it for `entries` and the review counts it as a page added.
31
31
  - **A branch name an earlier run took** (checkout or push says so): use `-2`, then `-3`, and name the earlier pull request in the report so the person can close it.
@@ -5,8 +5,8 @@ The pages need no server from this repository: plain HTML, or a build that write
5
5
  ### Steps
6
6
 
7
7
  1. **Find the output**: the folder the build writes the pages to (framework config, scripts), or, with no build, the folder that holds the real `index.html`. Done when you can name that folder and the build command, or that there is none.
8
- 2. **Prove it** with impeccable_verify_preview in static mode. Done when it answers `verified: true` with the folder's `index.html` in `files`; a failure names its `stage`, which the report explains, and pull-requests' "No preview, no test drive" says how the run goes on without a proof.
9
- 3. **Save it** with impeccable_write to `preview`: `intendedMethod: "static-upload"`, `outputDir` and the commands you proved, clearing a saved command you did not prove. Done when the answer's `keys` show each key stored or unchanged.
8
+ 2. **Prove it** with impeccable_verify_preview in static mode. Done when it answers `verified: true` with the folder's `index.html` in `files`; a failure names its `stage`, which the report explains, and pull-requests' "No preview, no test drive" says how the run goes on without a proof. When it answers that this machine has no sandbox for the check (Linux, or `--no-sandbox`), the check is skipped: save the build the scripts name in step 3, and the preview job on the pull request proves it; put that under "What is not verified".
9
+ 3. **Save it** with impeccable_write to `preview`: `intendedMethod: "static-upload"`, `outputDir` and the commands you proved, clearing a saved command you did not prove; when the check was skipped on this machine, the build the scripts name is what you save, and the preview job proves it. Done when the answer's `keys` show each key stored or unchanged.
10
10
 
11
11
  ### Reference
12
12