@impeccable/reviews 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hosted.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { ACCESS_ID_HEADER, ACCESS_SECRET_HEADER } from './cloudflare-access.js';
4
+ import { basicCredentials } from './netlify-password.js';
3
5
  import { BYPASS_HEADER } from './vercel-bypass.js';
4
6
  /**
5
7
  * Previews a host already deploys: what GitHub has on record of the
@@ -80,19 +82,34 @@ export async function readHostedPreviews(owner, repo, deps = {}) {
80
82
  const HOSTS = {
81
83
  vercel: { name: 'Vercel', gate: 'vercel-protection', login: /(^|\.)vercel\.com$/ },
82
84
  netlify: { name: 'Netlify', gate: 'netlify-protection', login: /(^|\.)netlify\.com$/ },
85
+ cloudflare: { name: 'Cloudflare Pages', gate: 'cloudflare-access', login: /(^|\.)cloudflareaccess\.com$/ },
83
86
  };
84
87
  const NETLIFY = /^(?=[^.]{1,63}\.netlify\.app$)deploy-preview-[1-9]\d{0,6}--([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.netlify\.app$/;
85
- /** A deployment's own address: https on one name, and nothing else. Vercel's is one name under vercel.app, Netlify's a pull request's deploy preview. */
88
+ /** A Pages preview: one label (the deployment's id, or the branch's alias), then the project, under pages.dev. The project's own address, with no label, is its production. */
89
+ const PAGES = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.pages\.dev$/;
90
+ /** A deployment's own address: https on one name, and nothing else. Vercel's is one name under vercel.app, Netlify's a pull request's deploy preview, Cloudflare's a Pages preview. */
86
91
  const hostOf = (url) => {
87
92
  if (url.protocol !== 'https:' || url.username || url.password || url.port || url.search || url.hash)
88
93
  return null;
89
- return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.vercel\.app$/.test(url.hostname) ? 'vercel' : NETLIFY.test(url.hostname) ? 'netlify' : null;
94
+ return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.vercel\.app$/.test(url.hostname) ? 'vercel' : NETLIFY.test(url.hostname) ? 'netlify' : PAGES.test(url.hostname) ? 'cloudflare' : null;
90
95
  };
91
- /** The host a preview address belongs to, by name; null for an address of neither. */
96
+ /** The host a preview address belongs to, by name; null for an address of none. */
92
97
  export function previewHostName(url) {
93
98
  const host = URL.canParse(url) ? hostOf(new URL(url)) : null;
94
99
  return host ? HOSTS[host].name : null;
95
100
  }
101
+ /** The Netlify site a deploy preview address belongs to, by name; null for any other address. */
102
+ export function netlifySiteOf(url) {
103
+ return URL.canParse(url) && hostOf(new URL(url)) === 'netlify' ? NETLIFY.exec(new URL(url).hostname)[1] : null;
104
+ }
105
+ /** The address pattern a proven preview stands for, where the host reports none to GitHub: Netlify's by pull request, Cloudflare's by the branch's alias. */
106
+ function patternOf(host, hostname) {
107
+ if (host === 'netlify')
108
+ return { pattern: `https://deploy-preview-{pr}--${NETLIFY.exec(hostname)[1]}.netlify.app` };
109
+ if (host === 'cloudflare')
110
+ return { pattern: `https://{branch}.${PAGES.exec(hostname)[1]}.pages.dev` };
111
+ return {};
112
+ }
96
113
  const ENTITIES = {
97
114
  amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–', lsquo: '‘', rsquo: '’', ldquo: '“', rdquo: '”',
98
115
  hellip: '…', middot: '·', bull: '•', laquo: '«', raquo: '»', copy: '©', reg: '®', trade: '™', times: '×',
@@ -136,16 +153,42 @@ async function pageText(res, max = 1_000_000) {
136
153
  }
137
154
  return Buffer.concat(chunks).toString('utf8');
138
155
  }
156
+ /**
157
+ * Whether Netlify's own password form opens the address: the password posted as that form posts it (a `password` field
158
+ * to the address itself), then the page read with the session cookie Netlify set. Says whether a password is right
159
+ * where the header did not open the address; a review cannot fill a form, so this proves nothing for one.
160
+ */
161
+ async function formOpens(target, password, phrase, open, signal) {
162
+ try {
163
+ const posted = await open(target, { method: 'POST', redirect: 'manual', credentials: 'omit', signal, headers: { accept: 'text/html', 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ password }).toString() });
164
+ await posted.body?.cancel().catch(() => { });
165
+ const cookies = posted.headers.getSetCookie().map((cookie) => cookie.split(';')[0]).filter((pair) => pair.includes('='));
166
+ if (posted.status >= 400 || !cookies.length)
167
+ return false;
168
+ const res = await open(target, { redirect: 'manual', credentials: 'omit', signal, headers: { accept: 'text/html', cookie: cookies.join('; ') } });
169
+ if (res.status < 200 || res.status >= 300) {
170
+ await res.body?.cancel().catch(() => { });
171
+ return false;
172
+ }
173
+ return visible(await pageText(res)).includes(plain(phrase));
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
139
179
  /**
140
180
  * Opens a preview address the way a visitor does, signed in nowhere, and proves it: it answers, and its page shows
141
181
  * `words` that a file of the checkout at `root` holds, so the page is this app's and not another site's. With `bypass`, a
142
- * Vercel bypass secret, a Vercel address is opened with it in the header Vercel reads it from, as reviews do; no other host's is.
182
+ * Vercel bypass secret, a Vercel address is opened with it in the header Vercel reads it from, as reviews do; with
183
+ * `password`, a Netlify site password, a Netlify address is opened with it as basic credentials in the Authorization
184
+ * header, the one way reviews can send it; with `access`, a Cloudflare Access service token, a Pages address is opened
185
+ * with it in the two headers Access reads it from. None goes to another host.
143
186
  */
144
187
  export async function provePreview(url, words, root, deps = {}) {
145
188
  const unproven = (reason, status = null) => ({ proven: false, status, reason });
146
189
  const host = URL.canParse(url) ? hostOf(new URL(url)) : null;
147
190
  if (!host)
148
- return unproven('Only a Vercel preview address, https://<name>.vercel.app, or a Netlify deploy preview, https://deploy-preview-<n>--<name>.netlify.app, with no query, is opened.');
191
+ return unproven('Only a Vercel preview address, https://<name>.vercel.app, a Netlify deploy preview, https://deploy-preview-<n>--<name>.netlify.app, or a Cloudflare Pages preview, https://<id or branch>.<project>.pages.dev, with no query, is opened.');
149
192
  const { name, gate, login } = HOSTS[host];
150
193
  const phrase = words.trim();
151
194
  if (phrase.length < 3 || phrase.length > 120 || /[\r\n]/.test(phrase))
@@ -153,14 +196,22 @@ export async function provePreview(url, words, root, deps = {}) {
153
196
  if (!(await inCheckout(root, phrase)))
154
197
  return unproven("No file of this checkout holds those words: give words the app's pages show, from its source.");
155
198
  const signal = AbortSignal.any([AbortSignal.timeout(15_000), ...(deps.signal ? [deps.signal] : [])]);
199
+ const open = deps.fetch ?? fetch;
156
200
  const opened = deps.bypass && host === 'vercel';
157
- const headers = { accept: 'text/html', ...(opened ? { [BYPASS_HEADER]: deps.bypass } : {}) };
158
- // Where the login stands in front all the same, the bypass secret did not get past it.
159
- const still = opened ? ' even with the bypass secret' : '';
201
+ const withPassword = deps.password !== undefined && host === 'netlify';
202
+ const withToken = deps.access !== undefined && host === 'cloudflare';
203
+ const headers = {
204
+ accept: 'text/html',
205
+ ...(opened ? { [BYPASS_HEADER]: deps.bypass } : {}),
206
+ ...(withPassword ? { authorization: basicCredentials(deps.password) } : {}),
207
+ ...(withToken ? { [ACCESS_ID_HEADER]: deps.access.id, [ACCESS_SECRET_HEADER]: deps.access.secret } : {}),
208
+ };
209
+ // Where the login stands in front all the same, the bypass secret, the password or the service token did not get past it, or, on Netlify, the app's own sign-in answered past the lock: the address alone cannot say which.
210
+ const still = opened ? ' even with the bypass secret' : withPassword ? ' even with the password' : withToken ? ' even with the service token' : '';
160
211
  let target = new URL(url);
161
212
  try {
162
213
  for (let hop = 0; hop < 4; hop++) {
163
- const res = await (deps.fetch ?? fetch)(target, { redirect: 'manual', headers, credentials: 'omit', signal });
214
+ const res = await open(target, { redirect: 'manual', headers, credentials: 'omit', signal });
164
215
  if (res.status >= 300 && res.status < 400) {
165
216
  await res.body?.cancel().catch(() => { });
166
217
  const location = res.headers.get('location') ?? '';
@@ -172,19 +223,26 @@ export async function provePreview(url, words, root, deps = {}) {
172
223
  target = next;
173
224
  continue;
174
225
  }
175
- if (res.status === 401 || (host === 'netlify' && res.status === 403)) {
226
+ if (res.status === 401 || (host !== 'vercel' && res.status === 403)) {
176
227
  // Vercel's own page names Vercel; any other 401 there is the app's own sign-in, which a page a visitor opens without signing in gets past.
177
228
  // A password or a login in front of a Netlify preview answers 401 or 403 on every page of it, and its page names nothing to go by.
229
+ // Cloudflare Access names itself on the 401 or 403 it answers a request it will not let in (a service token it does not admit), where a browser gets its login.
178
230
  const page = await pageText(res, 64_000);
179
- return host === 'netlify' || /vercel/i.test(page) ? { ...unproven(`The address asked for ${name}'s login or password${still}.`, res.status), gate }
180
- : unproven("The address asked for the app's own sign-in: try a page a visitor opens without signing in.", 401);
231
+ const named = host === 'netlify' || (host === 'vercel' && /vercel/i.test(page)) || (host === 'cloudflare' && (/^cloudflare-access\b/i.test(res.headers.get('www-authenticate') ?? '') || /cloudflareaccess\.com|cloudflare access/i.test(page)));
232
+ if (!named)
233
+ return unproven(res.status === 401 ? "The address asked for the app's own sign-in: try a page a visitor opens without signing in." : `The address answered ${res.status}.`, res.status);
234
+ // A password the header did not get past: Netlify's own form says whether it is the site's password at all.
235
+ if (withPassword && (await formOpens(target, deps.password, phrase, open, signal))) {
236
+ return { ...unproven("The password opens the address on Netlify's own form, but not in the Authorization header, the one way reviews can send it.", res.status), gate, formOpened: true };
237
+ }
238
+ return { ...unproven(`The address asked for ${name}'s login or password${still}.`, res.status), gate };
181
239
  }
182
240
  if (res.status < 200 || res.status >= 300) {
183
241
  await res.body?.cancel().catch(() => { });
184
242
  return unproven(`The address answered ${res.status}.`, res.status);
185
243
  }
186
244
  return visible(await pageText(res)).includes(plain(phrase))
187
- ? { proven: true, status: res.status, ...(host === 'netlify' ? { pattern: `https://deploy-preview-{pr}--${NETLIFY.exec(new URL(url).hostname)[1]}.netlify.app` } : {}) }
245
+ ? { proven: true, status: res.status, ...patternOf(host, new URL(url).hostname) }
188
246
  : unproven('The page does not show those words: it is another app, or another page.', res.status);
189
247
  }
190
248
  return unproven('The address redirected too many times.');
package/dist/login.js CHANGED
@@ -261,7 +261,7 @@ export function setupAuth(first, signInAgain, now = Date.now) {
261
261
  const until = (t) => now() + (Number.isFinite(t.expires_in) ? t.expires_in : 3600) * 1000;
262
262
  let current = { token: first.access_token, expiresAt: until(first) };
263
263
  let pending;
264
- const renew = (pi) => (pending ??= signInAgain(pi)
264
+ const renew = (pi, why = 'ran-out') => (pending ??= signInAgain(pi, why)
265
265
  .then((t) => {
266
266
  current = { token: t.access_token, expiresAt: until(t) };
267
267
  return t.access_token;
package/dist/main.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { mkdtempSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { dirname, join } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { runAgent } from './agent.js';
7
7
  import { fenceExtension } from './fence.js';
8
- import { focusInstructions, focusState } from './focus.js';
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
13
  import { checkoutFrontEnd, parseFlags, readCheckout, stateSaysNoFrontEnd, USAGE } from './preflight.js';
14
+ import { scanRecorder } from './scan.js';
14
15
  import { createSessionSync, syncExtension } from './sync.js';
15
16
  import { createTools, resultLine } from './tools.js';
17
+ import { transcriptPaths } from './transcript.js';
16
18
  import { atExit, closeScreens, helpMark, recordFrames, showIntro, showOutro, showPullRequest, showRun, showSignIn } from './ui/screens.js';
17
19
  const say = (line = '') => console.log(line);
18
20
  const execFileAsync = promisify(execFile);
@@ -152,8 +154,11 @@ export async function main(argv) {
152
154
  // Scratch output (a server's log, a build's output) goes here and never into the checkout.
153
155
  const scratch = mkdtempSync(join(tmpdir(), 'impeccable-reviews-scratch-'));
154
156
  process.on('exit', () => rmSync(scratch, { recursive: true, force: true }));
157
+ // Beside the transcript, named after the run as it is, so a run never writes over another's in the temp folder.
158
+ const scans = scanRecorder(log, flags.scanReport ? join(dirname(transcriptPaths(run.run_id, flags.transcript).markdown), `scan-report-${run.run_id.replace(/[^\w.-]/g, '_')}.json`) : undefined);
159
+ atExit(() => scans.report());
155
160
  // The model reads the instructions and the state trimmed to this setup, and writes only what it read.
156
- const focused = focusInstructions(instructions.data);
161
+ const focused = focusInstructions(instructions.data, { apps: severalApps(state.data) });
157
162
  let report;
158
163
  let fatal;
159
164
  let cancelled = false;
@@ -212,10 +217,11 @@ export async function main(argv) {
212
217
  firstContext: setupContext(focused, focusState(state.data)),
213
218
  tools: createTools({
214
219
  setupApi,
215
- auth: setupAuth(token, () => signIn(flags.host, log, screenPrompt(showSignIn), accept)),
220
+ auth: setupAuth(token, (_pi, why) => signIn(flags.host, log, screenPrompt((link, on) => showSignIn(link, { ...on, why })), accept)),
216
221
  owner: token.repository.owner,
217
222
  repo: token.repository.name,
218
223
  instructions: focused,
224
+ allInstructions: instructions.data,
219
225
  sync: liveSync,
220
226
  viewPullRequest: (n) => viewPullRequest(`${token.repository.owner}/${token.repository.name}`, n),
221
227
  onReport: (r) => void (report = r),
@@ -223,7 +229,7 @@ export async function main(argv) {
223
229
  previewWait: previewWait(),
224
230
  }),
225
231
  extensions: [
226
- fenceExtension(checkout.root, log, checkout.protectedBranches, scratch),
232
+ fenceExtension(checkout.root, log, checkout.protectedBranches, scratch, scans),
227
233
  syncExtension(liveSync, { onFatal: (message) => void (fatal = message), onQuit: finish }),
228
234
  ],
229
235
  interactive,
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Getting past Netlify's site password (Password Protection on Pro and Enterprise plans; "Project visibility: Password"
3
+ * on credit-based Pro) the one way a request can carry a password: as basic credentials in the Authorization header.
4
+ * Netlify's password page asks for a password alone, no username, so the credentials carry an empty name. The Netlify
5
+ * CLI on this machine, signed in as the person, reads the site's settings in the command's own process: what
6
+ * `netlify api` prints is those settings, the password among them, so the agent's shell never runs it (fence.ts), and
7
+ * no password goes back to the model. The person consents once, on the tool's screen, before the password is stored;
8
+ * a password opens the locked address before anything saves it. A password Netlify does not give back is the
9
+ * person's to type: setting one through the CLI would put it on the command line, where any process could read it.
10
+ * Netlify's team login (a private site, or team login protection) has no way in for tools, and is reported as such.
11
+ */
12
+ /** The header reviews send the password in. */
13
+ export const PASSWORD_HEADER = 'Authorization';
14
+ /** The password as the header carries it: basic credentials with an empty username, since Netlify asks for none. */
15
+ export const basicCredentials = (password) => `Basic ${Buffer.from(`:${password}`).toString('base64')}`;
16
+ /** The CLI's read of a site by the address every deploy preview of it carries (`site_id` takes the site's default domain), an authenticated call of Netlify's API. */
17
+ export const readSiteCall = (site) => ['api', 'getSite', '--data', JSON.stringify({ site_id: `${site}.netlify.app` })];
18
+ const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
19
+ /** A site's name and id as Netlify makes them. */
20
+ const NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
21
+ const ID = /^[A-Za-z0-9-]{1,64}$/;
22
+ /** A password as the header can carry it: one line, of a length a person would type. */
23
+ const password = (value) => (typeof value === 'string' && value.length > 0 && value.length <= 200 && !/[\r\n\0]/.test(value) ? value : null);
24
+ /** 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. */
25
+ export async function readSite(run, site) {
26
+ let read;
27
+ try {
28
+ read = record(JSON.parse(await run(readSiteCall(site))));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ if (typeof read?.id !== 'string' || !ID.test(read.id) || typeof read.name !== 'string' || !NAME.test(read.name))
34
+ return null;
35
+ const given = password(read.password);
36
+ return { id: read.id, name: read.name, hasPassword: read.has_password === true || given !== null, password: given, teamLogin: read.sso_login === true };
37
+ }
38
+ /**
39
+ * Gets the site password in place for the address Netlify's lock stands in front of, and saves only one that opens it.
40
+ * The site's settings say what the lock is: a password, which the person's yes has the run store when Netlify gives it
41
+ * back; the team login, which no tool gets past; or neither. When the CLI cannot read the site, or Netlify gives no
42
+ * password back, the person may paste the password.
43
+ */
44
+ export async function passNetlifyLock(deps) {
45
+ const site = await readSite(deps.run, deps.site);
46
+ const from = site ? { site: site.name } : {};
47
+ const finish = async (password, how) => {
48
+ deps.remember(password);
49
+ deps.remember(basicCredentials(password));
50
+ const proof = await deps.prove(password);
51
+ // locked: Netlify's lock stood all the same, and its form did not take the password either. Any other failure is the proof's to say: the form took it and the header did not, or the page past the lock is another, or did not answer.
52
+ if (!proof.proven)
53
+ return { reason: proof.reason ?? 'The address did not open with the password.', locked: proof.gate !== undefined && !proof.formOpened };
54
+ const failed = await deps.save(password);
55
+ return failed ? { status: 'save-failed', reason: failed, protection: 'password', ...from } : { status: 'saved', password: how, protection: 'password', ...from, value: password };
56
+ };
57
+ /** Why the run has no password to open the preview with, for the paste screen's skip. */
58
+ let stopped;
59
+ if (!site)
60
+ stopped = 'The Netlify CLI could not read the site: it is signed out, or the site is in no team of the person\'s.';
61
+ else if (site.hasPassword) {
62
+ if (!site.password)
63
+ stopped = 'Netlify does not give the site password back.';
64
+ else if (!(await deps.consent()))
65
+ return { status: 'skipped', reason: 'The person skipped storing the site password.', protection: 'password', ...from };
66
+ else {
67
+ const done = await finish(site.password, 'existing');
68
+ if ('status' in done)
69
+ return done;
70
+ // The site's own password, as Netlify gives it: nothing typed would be righter. Where the lock stood all the same, the page asks for the app's own sign-in past it,
71
+ // which a page a visitor opens gets past, or Netlify did not answer its form; else the proof's reason says what stood in the way.
72
+ return { status: 'refused', reason: done.locked ? "Netlify's own site password opened the address neither in the header nor on Netlify's form: the page 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, and call this tool again." : done.reason, protection: 'password', ...from };
73
+ }
74
+ }
75
+ else if (site.teamLogin) {
76
+ return { status: 'login', protection: 'team-login', reason: "Netlify's team login stands in front of the previews: they open for a member of the Netlify team signed in in their browser, and Netlify gives tools no way in.", ...from };
77
+ }
78
+ else {
79
+ return { status: 'refused', protection: 'none', reason: "Netlify's settings show neither a password nor the team login on the site: the sign-in is the app's own, so prove a page a visitor opens without signing in.", ...from };
80
+ }
81
+ const typed = await deps.paste();
82
+ if (typed === undefined)
83
+ return { status: 'skipped', reason: stopped, ...from };
84
+ if (!password(typed))
85
+ return { status: 'refused', reason: 'What was typed is not a site password: one line of at most 200 characters.', ...from };
86
+ const done = await finish(typed, 'typed');
87
+ return 'status' in done ? done : { status: 'refused', reason: done.locked ? `The password typed did not open the address: it is not the site's password, or the page asks for the app's own sign-in past the lock. ${done.reason}` : done.reason, ...from };
88
+ }
@@ -0,0 +1,17 @@
1
+ ### When this applies
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 wrangler on this machine lists the project's deployments when it is signed in. 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
+
5
+ ### Steps
6
+
7
+ 1. **Prove the newest preview** with impeccable_prove_preview: the branch's alias (`alias`) of the newest listed preview whose state is success (or a page on it a visitor opens without signing in), and words the page shows that the checkout holds, such as the app's name in its title. The alias is the address the pattern saves, so it is the one proven; a preview listed without one is proven on its own address (`url`). Done when it answers `proven: true`; when an address no longer answers, try the next newest, three at most.
8
+ 2. **Save the method** with impeccable_write to `preview`: `{"previewUrlPattern": "<the proof's pattern>", "intendedMethod": "preview-deploy"}`. The proof stands in for the person's answer, so ask nothing. Done when the answer's `keys` show both stored or unchanged.
9
+
10
+ ### Reference
11
+
12
+ - **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
+ - **Another app, or no answer from any of the three**: Cloudflare's previews do not show this app. Set the preview up as ci-tunnel 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 ci-tunnel 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
+ - **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, and never sign wrangler in. 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".
@@ -0,0 +1,22 @@
1
+ ### When this applies
2
+
3
+ The state's repository has an `apps` item: the scan proposed several apps, folders that each draw pages of their own, and the rest of the setup is asked only once the apps are answered. The scan proposes them, so while the scan item's `scan` is `running` the item may not be there yet: read the state again before taking the repository for one app. The person is asked once, this question, and nothing else about apps. These steps come before the playbook's step 3; steps 3 to 6 then go app by app.
4
+
5
+ ### Steps
6
+
7
+ 1. **Confirm the proposal** as it stands, while the item's status is `needs-you`: impeccable_write to `apps` with the `confirm` action the item's instructions give. Its answer lists every app of the repository, on and off, with its `key`, `name`, `pathPrefixes` and `enabled`; the one the scan switched on is the repository's default. Done when it answered `apps`.
8
+ - **An item that is `done` or `skipped`** was answered before, on the setup page or by an earlier run (a run that stopped at the question left the scan's default, as a skip does): write nothing and ask nothing; the state's apps that are `enabled` are the chosen ones. Step 4.
9
+ 2. **Ask which apps get previews**, once, with impeccable_ask: `id: "apps"`, `kind: "multi"`, `title` *"This repository has several apps"*, `prompt` *"Which apps should get Impeccable previews?"*, `hint` *"Each draws pages of its own; untick any that should not get previews."*, and `options` one per app the answer listed: `value` its `key`, `label` its `name` and folder (*"Storefront · apps/storefront"*), `selected: true` on every one. The repository's own app (`main`) is among them only where the Worker kept it as a product of its own, with pages outside every app's folder: its label is its name and `/`. Done when it answered `values`, or a skip.
10
+ 3. **Write the picks**, one action a call as the item's instructions give them: `enable` each chosen app that is off, then `disable` each app that is on and was not chosen. The last app that is on cannot be switched off (409), which is why the enables come first. **After Skip**, write nothing more: the app the scan switched on stays the one that is on. Done when the last answer's `apps` show `enabled` true for every chosen app and false for the rest, or, after Skip, the confirm's answer stands.
11
+ 4. **Read the state again** with impeccable_state. Done when the preview item's `apps` names each chosen app's `directory`, `build` and `method`, and `perApp` its status.
12
+ 5. **Set each chosen app up**, one after the other, as the playbook's steps 3 and 4 say for that app alone, `appKey` on every write and check (`preview`, `test-accounts`, `latest-pull-check`, `connection-check`): its method by the case that applies to it (ci-tunnel when its pages need a server, static-site when they do not, a host's case when the host deploys that app as a project of its own, proven on that app's own address), and its own sign-in when the scan item's `accessByApp` says it has one, each account question's title naming the app (*"Order desk admin account"*). Done when every chosen app's method is saved and its sign-in saved or skipped, and no other app got a write, a build or a question.
13
+ 6. **The pull requests** as pull-requests says, once for the repository: the setup commit takes the state's one workflow `file` (a job for each chosen app whose preview Impeccable builds) once every chosen app's method and build settings are saved; the test drive adds its page to every chosen app, in that app's own framework and linked from that app's own public page, so one review renders each of them on its own preview; that case's step 3 proves each chosen app with the input that proved it; its step 6 checks once, with the first chosen app's `appKey`, and the other apps' previews go under "What is not verified". Done when pull-requests' steps are done.
14
+ - **A host previews some chosen apps and Impeccable builds the others**: a host's case speaks for its own app. The setup commit carries the workflow for the others and the test drive branches from it, and pull-requests' step 4 goes each app's way: the host's app proven on the host's build, the others by the workflow. The repository goes without a workflow and a setup pull request only where a host previews every chosen app.
15
+ 7. **Report per app**: under "What was configured, and by whom", one line per app the question offered: on or off and why (picked, unticked, the scan's default after a skip), its method, its sign-in, what was verified; an app left out is under "What was skipped, and why" as well, with the setup page as where it is turned on. Done when every app is on one line.
16
+
17
+ ### Reference
18
+
19
+ - **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
+ - **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`.
21
+ - **Status lines** name the app (*"Proving the storefront's preview"*), under the playbook's tasks.
22
+ - The checkup's entries for an app read "<repository> (<app>): …", and its `apps` check is answered by step 1.
@@ -9,7 +9,7 @@ Netlify deploys previews of the pull requests: impeccable_netlify_previews lists
9
9
 
10
10
  ### Reference
11
11
 
12
- - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): a password, a team login or Netlify Identity stands in front of the previews, and getting past it is not part of this setup. Save no method and set up no other preview, whatever the checkup's `next` says, and end the report's "Not part of this setup" line with "A password or login in front of the Netlify previews".
12
+ - **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: the password is got past on the person's yes, the team login is not.
13
13
  - **Another app, or no answer from any of the three**: Netlify's previews do not show this app. Set the preview up as ci-tunnel or static-site says instead, and say under "What is not verified" what the Netlify address answered.
14
14
  - **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 ci-tunnel 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
15
  - **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.
@@ -19,6 +19,6 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
19
19
  - **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.
20
20
  - **The build failed** (`state` is `error` or `failure`): `netlify deploy --build --context deploy-preview` builds this linked checkout as Netlify does, uploads a draft, and prints why it failed, so the report can name the cause.
21
21
  - **No deploy preview within five minutes**: the build had not finished, or Netlify builds no deploy preview for pull requests here: Deploy Previews may be off in the site's settings, so put "Turn Deploy Previews on in the site's Netlify settings" in `left`.
22
- - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): handle it as netlify-previews says.
22
+ - **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, which no tool gets past: put "Make the site public, or password-protected, in Project configuration → General → Visitor access" in `left`.
23
23
  - **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".
24
24
  - **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.
@@ -1,21 +1,23 @@
1
1
  ### When this applies
2
2
 
3
- impeccable_prove_preview answered `gate: "vercel-protection"`: Vercel's Deployment Protection (Vercel Authentication, or Password Protection, which shows the same gate) stands in front of a preview that Vercel made, and a visitor gets its login. Vercel's way in for a tool is a bypass secret of the project, sent in a header, which every plan has. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well: these steps come between the proof and the save of the method, in vercel-previews' step 2 or vercel-setup's step 5, before the test drive's pull request opens, and end with the preview opened. A lock of another host (Cloudflare Access, a Netlify password) is not part of this setup.
3
+ impeccable_prove_preview answered a `gate`: the host's own lock stands in front of a preview the host made, and a visitor gets its login or its password page. Three locks have a way in for a tool, each with a tool of its own that works as the person in the command's own process, so no key ever reaches you: Vercel's Deployment Protection (`gate: "vercel-protection"`: Vercel Authentication, or Password Protection, which shows the same gate), whose way in is a bypass secret of the project, sent in a header, which every plan has; Netlify's site password (`gate: "netlify-protection"`), sent as basic credentials in the Authorization header; and Cloudflare Access in front of a Pages preview (`gate: "cloudflare-access"`), whose way in is a service token that a Service Auth policy of the Access application allows, sent in two headers. Netlify's team login, which the same gate answers for, has none. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well, and so does netlify-setup's new site on a team that keeps new sites private: these steps come between the proof and the save of the method, in vercel-previews', netlify-previews' or cloudflare-previews' step 2 or vercel-setup's or netlify-setup's step 5, before the test drive's pull request opens (on Netlify, before its draft is marked ready), and end with the preview opened.
4
4
 
5
5
  ### Steps
6
6
 
7
- 1. **Get the secret in place** (the Preview method task) with impeccable_vercel_bypass, under the status line *"Getting past Vercel's login"*. The tool does the rest with the Vercel CLI signed in as the person: 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. The secret never comes back to you. Done when it answered `status`:
8
- - `saved`: the preview is proven (`proven: true`); `secret` says whether it was created, existing or typed, and `project` which Vercel project holds it. Step 2.
9
- - `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
10
- - `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" in `left`.
11
- 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' step 2 says. Done when the method is saved.
12
- 3. **When nothing was saved**, save no method and set up no other preview, whatever the checkup's `next` says, and open no test drive (pull-requests' "No preview, no test drive"). Say under "What is not verified" that the previews are behind Vercel's login, with the tool's `reason`, and name the settings page where the person makes the secret (the report's bullet below); end the report's "Not part of this setup" line with "Vercel's Deployment Protection in front of the previews". Done when the report says so.
7
+ 1. **Get the key in place** (the Preview method task): impeccable_vercel_bypass for Vercel's login, under the status line *"Getting past Vercel's login"*; impeccable_netlify_password for Netlify's lock, under *"Getting past Netlify's password"*; impeccable_cloudflare_access for Cloudflare Access, under *"Getting past Cloudflare Access"*. The tool does the rest: it finds the project's secret labelled "Impeccable reviews", or reads what Netlify's settings say stands in front of the site; shows one screen asking to create the secret, to store the site password Netlify gives back, or to create a service token (then one taking the Cloudflare API token that creating it takes, and it allows the token in the Access application's policy), or, when it cannot make or read one, one screen taking a pasted one; opens the preview with it; and saves it for reviews. The key never comes back to you. Done when it answered `status`:
8
+ - `saved`: the preview is proven (`proven: true`); `secret`, `password` or `token` says whether it was created, existing or typed, and `project`, `site` or `application` which one holds or allows it; Cloudflare's `pattern` is the address pattern to save with the method. Step 2.
9
+ - `login` (Netlify): the site is behind Netlify's team login, which no tool gets past; `reason` says so. Step 3.
10
+ - `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.
11
+ - Any other `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
12
+ - `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`.
13
+ 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.
14
+ 3. **When nothing was saved**, save no method and set up no other preview, whatever the checkup's `next` says, and open no test drive (pull-requests' "No preview, no test drive"). Say under "What is not verified" that the previews are behind the host's lock, with the tool's `reason`, and name the settings page where the person makes the key (the report's bullet below); end the report's "Not part of this setup" line with the lock the tool met: "Vercel's Deployment Protection in front of the previews", "Netlify's password in front of the previews", "Netlify's team login in front of the previews", or "Cloudflare Access in front of the previews". Done when the report says so.
13
15
 
14
16
  ### Reference
15
17
 
16
- - **The report** names, under "What was configured, and by whom", the Vercel project that holds the secret named "Impeccable reviews" (`project`), whether this run created it, found it there or the person typed it, and where it is revoked: the project's Settings → Deployment Protection → Protection Bypass for Automation. Revoking or regenerating it stops reviews until this setup is run again.
17
- - **The tool does the writes**: the secret as Impeccable's credential for the lock, and the setting that sends it in the `x-vercel-protection-bypass` header, both through the setup API, so impeccable_write refuses those two. The state's `preview-protection` item words the same calls, and shows `keySaved` once they are done.
18
+ - **The report** names, under "What was configured, and by whom", the Vercel project that holds the secret named "Impeccable reviews" (`project`), whether this run created it, found it there or the person typed it, and where it is revoked: the project's Settings → Deployment Protection → Protection Bypass for Automation; or the Netlify site (`site`) whose password is stored, whether this run read it from the site or the person typed it, and where it is changed: the site's Project configuration → General → Visitor access; or the service token named "Impeccable reviews", whether this run created it or the person typed it, the Access application whose policy allows it (`application`), when a created one runs out (`expires`), and where it is revoked: Zero Trust → Access controls → Service credentials → Service Tokens, with the Cloudflare API token the person made for the run theirs to delete (dash.cloudflare.com/profile/api-tokens). Revoking, regenerating or changing it stops reviews until this setup is run again.
19
+ - **The tool does the writes**: the key as Impeccable's credential for the lock, and the setting that sends it in the host's header (`x-vercel-protection-bypass`, `Authorization`, or `CF-Access-Client-Id` and `CF-Access-Client-Secret`), both through the setup API, so impeccable_write refuses those two. The state's `preview-protection` item words the same calls, and shows `keySaved` once they are done.
18
20
  - **A key saved before** (the item shows `keySaved` and a `check` that `passed`): the lock was handled by an earlier run or the person. Run step 2's check and go on; call the tool only when that check fails.
19
- - **The secret is never yours**: it goes from the command's own process to Vercel and to Impeccable, and into no message, status line, file, command or report. `vercel project protection`, `vercel api` and `vercel curl` print it, and the fence refuses them.
21
+ - **The key is never yours**: it goes from the command's own process to the host and to Impeccable, and into no message, status line, file, command or report. `vercel project protection`, `vercel api`, `vercel curl` and `netlify api` print it, and the fence refuses them.
20
22
  - **The app's own sign-in** is another card: a preview opened past the lock proves nothing about it.
21
- - **Status lines** belong to the Preview method task. Word Vercel's login as a login.
23
+ - **Status lines** belong to the Preview method task. Word Vercel's login and Cloudflare Access as a login, and Netlify's lock as a password.
@@ -10,18 +10,20 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
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
11
  - **Skip**: set the preview up as ci-tunnel 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.
12
12
  2. **Create the project** (run each Vercel command plain: its answer is short, and a pipe to `tail` is refused): `vercel link --yes --team <the team>` creates the project in that team, named after the folder (or links the one that has that name), and connects the GitHub `origin`; the CLI names no team on its own without a terminal, so the option is given whenever there is a team. Done when it prints the linked project.
13
- 3. **Connect the repository** with impeccable_vercel_connect: it runs the connection on the linked project and asks the person for Vercel's GitHub app's access when the app cannot reach the repository. Done when it answered `connected: true`; a project connected by the link already is, and it says so.
13
+ 3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, 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. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
14
14
  4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say, with the changes vercel-previews' reference gives: from the default branch, no setup pull request, built once with plain commands. Stage its files by name: `vercel link` writes `.vercel/` and a `.env.local` that holds a Vercel token, and may add `.vercel` to `.gitignore`, and none of that goes into the pull request. Its push starts the project's first deployment. Done when `git push -u origin impeccable/test-drive` succeeded.
15
15
  5. **Its preview, proven and saved, then its pull request** as pull-requests' step 4 (Vercel) and step 5 say: the wait for the pushed head's deployment, the test drive's page proven on the address it lists (past Vercel's login as preview-protection says), impeccable_write to `preview` as vercel-previews' step 2 says, then the pull request and its 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".
16
16
 
17
17
  ### Reference
18
18
 
19
19
  - **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): the Vercel setup ends there. Set the preview up as ci-tunnel 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.
20
+ - **A push before step 3** is refused, since a connected project builds every push: run impeccable_vercel_connect first.
21
+ - **`locked: false`** from step 3, with `production` not "exists": push nothing, and set up no other preview either, since the connected project would build that push as its first deployment, public at its own address. Mark the Test drive task failed, say under "What was skipped, and why" what stopped the lock (the answer's `reason`) and how the person locks the project (Vercel Authentication for All Deployments, under the project's Settings → Deployment Protection), and put "Lock the Vercel project, then run the setup again" in `left`.
20
22
  - **No preview for the pushed test drive**: save no method, set up no other preview and open no pull request, as pull-requests' "No preview, no test drive" says; the project and the connection stay, and the report names them.
21
23
  - **The build failed** (`state` is `failure` or `error`): `vercel deploy --yes` builds this checkout on Vercel and prints why it failed, so the report can name the cause.
22
24
  - **No deployment within five minutes**: the build had not finished, or Vercel builds no push here: the project's Git connection is the thing to check, and "Run the setup again once Vercel builds the repository" goes in `left`.
23
25
  - **Behind Vercel's login** (`gate: "vercel-protection"`) with nothing getting past it: preview-protection says what the report names.
24
- - **The first deployment** of a new project is called Production, whatever branch it builds: it is the test drive's preview, and the proof may open it. Vercel serves a Production deployment at the project's own address (`vercel project ls` prints it) without its login, so the report tells the person that the test drive's build is public there until the project's default branch deploys.
25
- - **Behind Vercel's login** (`gate: "vercel-protection"`): a project the CLI creates starts with Vercel Authentication on, so expect it, and go on as the preview-protection case says.
26
- - **The report** names the Vercel project and its team under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Vercel account, and the CLI's install when there was one, and that the link wrote a `.env.local` with a Vercel token in the checkout. The project's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
27
- - **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 wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Vercel's login as a login, since a line about signing in belongs to the Sign-in task.
26
+ - **The first deployment** of a new project is called Production, whatever branch it builds. Locked in step 3, and still so when its deployment is read, it is the test drive's preview: impeccable_hosted_previews lists it, and the proof opens it past Vercel's login as preview-protection says. Unlocked by then, or another project's, the evidence lists no address for it and the proof refuses it: no test drive, as "No preview for the pushed test drive" says, and the report says under "What is not verified" that Vercel serves the test drive's build at the project's own address (`vercel project ls` prints it) without a login until the default branch deploys, with the step from `locked: false` in `left`. A project that deployed production before the run (`production: "exists"`) builds the push as a Preview, and keeps its addresses as they were.
27
+ - **Behind Vercel's login** (`gate: "vercel-protection"`): a project the CLI creates starts with Vercel Authentication on, and step 3 locks it at every address, so expect it, and go on as the preview-protection case says.
28
+ - **The report** names the Vercel project and its team under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Vercel account, and the CLI's install when there was one, and that the link wrote a `.env.local` with a Vercel token in the checkout. A project locked in step 3 gets this too: Vercel's login stands in front of every address of it, its production one included, until the person chooses Standard Protection under Settings → Deployment Protection, which opens production and keeps the previews behind the login. When the answer's `productionBranch` is not the repository's default branch, say under "Before you merge" that Vercel deploys production from that branch (Settings → Environments → Production → Branch Tracking changes it). The project's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
29
+ - **Status lines**: the sign-in, the link, the connection and the lock 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 wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Vercel's login as a login, since a line about signing in belongs to the Sign-in task.
package/dist/playbook.js CHANGED
@@ -12,13 +12,13 @@ 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, or vercel-setup and netlify-setup, which turn them on) or building it itself through the workflow the setup state generates (\`ci-tunnel\` for pages that need a server, \`static-upload\` for a static site), 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) or building it itself through the workflow the setup state generates (\`ci-tunnel\` for pages that need a server, \`static-upload\` for a static site), 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
  # Steps
18
18
 
19
- 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, and the titles in \`notPartOfThisSetup\`.
19
+ 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\`.
20
20
  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.
21
- 3. **Set up the preview** as the vercel-previews case says when impeccable_hosted_previews lists Vercel's previews of the pull requests (and as preview-protection says when Vercel's login stands in front of them), as the netlify-previews case says when impeccable_netlify_previews lists Netlify's, 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 ci-tunnel case says when the pages need a server, and as the static-site case says when they do not. Done when that case's steps are done.
21
+ 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 (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 ci-tunnel case says when the pages need a server, and as the static-site case says when they do not. Done when that case's steps are done.
22
22
  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.
23
23
  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\`.
24
24
  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.
package/dist/preflight.js CHANGED
@@ -21,6 +21,9 @@ Options:
21
21
  removed, and print where they are
22
22
  --transcript <path>
23
23
  write the transcript there: a folder, or a .md file
24
+ --scan-report write what the fence's content checks found (which check,
25
+ where, what it did; never the text) to a JSON file beside
26
+ the transcript, and print where it is
24
27
  --frames <dir> write each screen into this folder as it changes (for evals)
25
28
  -h, --help show this help`;
26
29
  const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
@@ -35,6 +38,7 @@ export function parseFlags(argv) {
35
38
  host: { type: 'string', default: 'https://impeccable.reviews' },
36
39
  debug: { type: 'boolean', default: false },
37
40
  transcript: { type: 'string' },
41
+ 'scan-report': { type: 'boolean', default: false },
38
42
  frames: { type: 'string' },
39
43
  model: { type: 'string' },
40
44
  effort: { type: 'string', default: 'high' },
@@ -66,7 +70,7 @@ export function parseFlags(argv) {
66
70
  throw new UserError('--transcript needs a folder or a .md file.');
67
71
  if (values.frames !== undefined && !values.frames.trim())
68
72
  throw new UserError('--frames needs a folder.');
69
- return { host: host.origin, debug: values.debug, transcript: values.transcript?.trim(), frames: values.frames?.trim(), model: values.model?.trim(), effort: values.effort, yes: values.yes, help: values.help };
73
+ return { host: host.origin, debug: values.debug, transcript: values.transcript?.trim(), scanReport: values['scan-report'], frames: values.frames?.trim(), model: values.model?.trim(), effort: values.effort, yes: values.yes, help: values.help };
70
74
  }
71
75
  /** owner/repo from a GitHub remote address, in any of the forms git accepts. */
72
76
  export function parseGitHubRemote(url) {