@impeccable/reviews 0.1.21 → 0.1.23

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.
@@ -1,3 +1,8 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { setTimeout as sleep } from 'node:timers/promises';
1
6
  /**
2
7
  * Getting past Netlify's site password (Password Protection on Pro and Enterprise plans; "Project visibility: Password"
3
8
  * on credit-based Pro) the way Netlify takes it: on its own form, which answers a session cookie for the preview's host
@@ -6,8 +11,10 @@
6
11
  * CLI on this machine, signed in as the person, reads the site's settings in the command's own process: what
7
12
  * `netlify api` prints is those settings, the password among them, so the agent's shell never runs it (fence.ts), and
8
13
  * no password goes back to the model. The person consents once, on the tool's screen, before the password is stored;
9
- * a password opens the locked address before anything saves it. A password Netlify does not give back is the
10
- * person's to type: setting one through the CLI would put it on the command line, where any process could read it.
14
+ * a password opens the locked address before anything saves it. Where Netlify does not give the password back, the
15
+ * person's yes has the run set a new one on the site as them (#395): generated here, and sent to Netlify's API by
16
+ * this process with the token the CLI signed in with, since `netlify api updateSite` takes its data only on the
17
+ * command line (`--data`), where any process could read it.
11
18
  * Netlify's team login (a private site) has no way in for tools, so on the person's yes the run makes the site public
12
19
  * through the CLI; where Netlify refuses, the person changes Visitor access on Netlify's page and the run tries again.
13
20
  */
@@ -38,14 +45,79 @@ export async function readSite(run, site) {
38
45
  if (typeof read?.id !== 'string' || !ID.test(read.id) || read.name !== site || !NAME.test(site))
39
46
  return null;
40
47
  const given = password(read.password);
41
- return { id: read.id, name: read.name, hasPassword: read.has_password === true || given !== null, password: given, teamLogin: read.sso_login === true };
48
+ return { id: read.id, name: read.name, hasPassword: read.has_password === true || given !== null, password: given, teamLogin: read.sso_login === true, live: read.password_context === 'all' };
42
49
  }
50
+ /** Netlify's API, as the CLI calls it by default; NETLIFY_API_URL naming it, with or without the path, is the same. */
51
+ const NETLIFY_API = 'https://api.netlify.com/api/v1';
52
+ const DEFAULT_API = /^https:\/\/api\.netlify\.com(?:\/api\/v1)?\/?$/;
53
+ /** Where the Netlify CLI keeps its sign-in: `config.json` in env-paths' config folder for "netlify", as @netlify/dev-utils names it. */
54
+ export function netlifyConfigPath(env = process.env, home = homedir(), platform = process.platform) {
55
+ if (platform === 'darwin')
56
+ return join(home, 'Library', 'Preferences', 'netlify', 'config.json');
57
+ if (platform === 'win32')
58
+ return join(env.APPDATA || join(home, 'AppData', 'Roaming'), 'netlify', 'Config', 'config.json');
59
+ return join(env.XDG_CONFIG_HOME || join(home, '.config'), 'netlify', 'config.json');
60
+ }
61
+ /**
62
+ * The token the Netlify CLI acts with, found as netlify-cli 27.11.2 finds it (its getToken, less the `--auth` flag the
63
+ * run never passes): NETLIFY_AUTH_TOKEN, else the signed-in user's in its config. Read in this process, which the
64
+ * agent's shell is not (the sandbox denies it the CLI's config); null where there is none.
65
+ */
66
+ export function netlifyToken(env = process.env, path = netlifyConfigPath(env)) {
67
+ if (env.NETLIFY_AUTH_TOKEN && env.NETLIFY_AUTH_TOKEN !== 'null')
68
+ return env.NETLIFY_AUTH_TOKEN;
69
+ try {
70
+ const config = record(JSON.parse(readFileSync(path, 'utf8')));
71
+ const user = typeof config?.userId === 'string' ? record(record(config.users)?.[config.userId]) : undefined;
72
+ const token = record(user?.auth)?.token;
73
+ return typeof token === 'string' && token.length > 0 ? token : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * Sets a site's password through Netlify's API, as the CLI's `updateSite` would, but from this process: the password
81
+ * goes in the request's body, never on a command line or through the agent's shell. What stopped it, else null; never
82
+ * Netlify's own answer, which may carry the site's settings.
83
+ */
84
+ export async function setSitePassword(id, password, deps) {
85
+ // The CLI read the site from the API NETLIFY_API_URL names; the token goes to Netlify's own alone, so a site read elsewhere is not changed there.
86
+ const api = (deps.env ?? process.env).NETLIFY_API_URL;
87
+ if (api && !DEFAULT_API.test(api))
88
+ return "The Netlify CLI here reads Netlify through another API (NETLIFY_API_URL), so the run did not change the site password on Netlify's own.";
89
+ if (!deps.token)
90
+ return 'The Netlify CLI here holds no sign-in the run can use, so the site password was not changed.';
91
+ let res;
92
+ try {
93
+ res = await deps.fetch(`${NETLIFY_API}/sites/${encodeURIComponent(id)}`, {
94
+ method: 'PATCH',
95
+ headers: { authorization: `Bearer ${deps.token}`, 'content-type': 'application/json' },
96
+ body: JSON.stringify({ password }),
97
+ redirect: 'error',
98
+ signal: deps.signal ? AbortSignal.any([deps.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000),
99
+ });
100
+ }
101
+ catch {
102
+ return 'Netlify did not answer the change of the site password, so it may or may not have changed.';
103
+ }
104
+ await res.body?.cancel().catch(() => { });
105
+ if (res.ok)
106
+ return null;
107
+ if (res.status === 401)
108
+ return 'Netlify refused the Netlify CLI\'s sign-in, so the site password was not changed: sign the CLI in again (netlify login).';
109
+ if (res.status === 403 || res.status === 404)
110
+ return 'Netlify refused to change the site password for the account the Netlify CLI is signed in as: its role in the team cannot change the site\'s settings, or Netlify no longer lists the site to it.';
111
+ return `Netlify answered ${res.status} to the change of the site password, so it was not changed.`;
112
+ }
113
+ /** A password the run sets on a site: as long as Netlify's form takes comfortably, and nothing a person would guess. */
114
+ const newPassword = () => randomBytes(24).toString('base64url');
43
115
  /**
44
116
  * Gets the site password in place for the address Netlify's lock stands in front of, and saves only one that opens it.
45
117
  * The site's settings say what the lock is: a password, which the person's yes has the run store when Netlify gives it
46
- * back; the team login, which the person's yes has the run lift by making the site public; or neither. When the CLI
47
- * cannot read the site, no password goes anywhere, since the site's lock may be another's; when Netlify gives none
48
- * back for a site it read, the person may paste the password.
118
+ * back, or replace with a new one set as them when it does not; the team login, which the person's yes has the run lift
119
+ * by making the site public; or neither. When the CLI cannot read the site, no password goes anywhere, since the site's
120
+ * lock may be another's.
49
121
  */
50
122
  export async function passNetlifyLock(deps) {
51
123
  let site = await readSite(deps.run, deps.site);
@@ -57,46 +129,51 @@ export async function passNetlifyLock(deps) {
57
129
  // The person picked Password protected on Netlify's page: the password is got in place as any site password is.
58
130
  site = opened;
59
131
  }
60
- const from = site ? { site: site.name } : {};
132
+ // A site the CLI cannot read as the person is not shown to be theirs: a password for it could go to anyone's deploy preview that asks for one.
133
+ if (!site)
134
+ return { status: 'refused', reason: `The Netlify CLI could not read the site ${deps.site} as the person: it is not installed or 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 set or sent.` };
135
+ if (!site.hasPassword)
136
+ 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.", site: site.name };
137
+ const from = { site: site.name, protection: 'password' };
138
+ /** Opens the preview with the password on Netlify's form, a password set moments ago again while the lock stands, until Netlify takes it or the wait runs out; saves it once it opens. */
61
139
  const finish = async (password, how) => {
62
140
  deps.remember(password);
63
141
  // Netlify's form, which an app's own password form looks like, gets the password only on a site whose settings show one: the CLI read this one as the person's.
64
- const proof = await deps.prove(password);
142
+ const wait = deps.wait ?? { totalMs: 30_000, everyMs: 3_000 };
143
+ let proof = await deps.prove(password);
144
+ for (const end = Date.now() + wait.totalMs; how === 'set' && !proof.proven && proof.gate !== undefined && Date.now() < end && !deps.signal?.aborted; proof = await deps.prove(password)) {
145
+ await sleep(wait.everyMs, undefined, { signal: deps.signal });
146
+ }
65
147
  // locked: Netlify's lock stood all the same, so its form did not take the password. Any other failure is the proof's to say: the page past the lock is another, or did not answer.
66
148
  if (!proof.proven)
67
149
  return { reason: proof.reason ?? 'The address did not open with the password.', locked: proof.gate !== undefined };
68
150
  const failed = await deps.save(password);
69
- return failed ? { status: 'save-failed', reason: failed, protection: 'password', ...from } : { status: 'saved', password: how, protection: 'password', ...from, value: password };
151
+ return failed ? { status: 'save-failed', reason: failed, ...from } : { status: 'saved', password: how, ...from, value: password };
70
152
  };
71
- // 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.
72
- if (!site)
73
- 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
- /** Why the run has no password to open the preview with, for the paste screen's skip. */
75
- let stopped;
76
- if (site.hasPassword) {
77
- if (!site.password)
78
- stopped = 'Netlify does not give the site password back.';
79
- else if (!(await deps.consent()))
80
- return { status: 'skipped', reason: 'The person skipped storing the site password.', protection: 'password', ...from };
81
- else {
82
- const done = await finish(site.password, 'existing');
83
- if ('status' in done)
84
- return done;
85
- // 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,
86
- // 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.
87
- return { status: 'refused', reason: done.locked ? "Netlify's own site password did not open the address 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 };
88
- }
89
- }
90
- else {
91
- 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 };
153
+ if (site.password) {
154
+ if (!(await deps.consent()))
155
+ return { status: 'skipped', reason: 'The person skipped storing the site password.', ...from };
156
+ const done = await finish(site.password, 'existing');
157
+ if ('status' in done)
158
+ return done;
159
+ // The site's own password, as Netlify gives it: nothing else would be righter. Where the lock stood all the same, the page asks for the app's own sign-in past it,
160
+ // 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.
161
+ return { status: 'refused', reason: done.locked ? "Netlify's own site password did not open the address 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, ...from };
92
162
  }
93
- const typed = await deps.paste();
94
- if (typed === undefined)
95
- return { status: 'skipped', reason: stopped, ...from };
96
- if (!password(typed))
97
- return { status: 'refused', reason: 'What was typed is not a site password: one line of at most 200 characters.', ...from };
98
- const done = await finish(typed, 'typed');
99
- 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 };
163
+ // Netlify does not give the password back: on the person's yes, a new one replaces it, set as them, so nothing is copied by hand (#395).
164
+ if (!(await deps.replace(site.live)))
165
+ return { status: 'skipped', reason: 'Netlify does not give the site password back, and the person skipped setting a new one.', ...from };
166
+ const fresh = newPassword();
167
+ // Remembered before Netlify sees it, so the debug log and the transcript remove it wherever it turns up (finish remembers every password again, for the site's own).
168
+ deps.remember(fresh);
169
+ const failed = await deps.setPassword(site.id, fresh);
170
+ if (failed)
171
+ return { status: 'refused', reason: failed, ...from };
172
+ deps.changed?.();
173
+ const done = await finish(fresh, 'set');
174
+ if ('status' in done)
175
+ return { ...done, ...(done.status === 'save-failed' ? { reason: `The run set a new site password on ${site.name}, which replaced the one before, but it was not saved for reviews: ${done.reason}` } : {}), replaced: true };
176
+ return { status: 'refused', replaced: true, reason: `The run set a new site password on ${site.name}, which replaced the one before, but the address did not open with it on Netlify's form, so it was not saved. ${done.locked ? "The page asks for the app's own sign-in past the lock, or Netlify did not take the password yet: prove a page a visitor opens without signing in, and call this tool again." : done.reason}`, ...from };
100
177
  }
101
178
  /**
102
179
  * A site behind Netlify's team login made public on the person's yes: the CLI's change, read back, then the preview
@@ -5,13 +5,13 @@ The state's repository has an `apps` item: the scan proposed several apps, folde
5
5
  ### Steps
6
6
 
7
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. An item that is `done` or `skipped` was answered before (on the setup page, or by an earlier run): no confirm, and step 2 all the same. Done when it answered `apps`, or the item was answered before.
8
- 2. **Ask which apps this run sets up**, on every run, with impeccable_ask: `id: "apps"`, `kind: "multi"`, and the `prompt` *"Which apps should get Impeccable previews?"*. The tool words the question and lists the state's apps (the repository's own, `main`, only where the Worker kept it as a product of its own), each picked as it is on, or every one after this run's confirm. Mark the Set up monorepo apps task in progress first. Every write and host tool for an app is refused until it is answered. Done when it answered `values`, `named` (the command was started for that one app with `--app`, from its card on the dashboard: the tool showed one confirm screen naming that app instead of the list, and the list all the same when the person asked for other apps there), or a skip, whose `on` lists the apps that are on: the ones this run sets up. After `named`, that app is the one this run sets up: every host read, key, proof, method and check is its own, and no other app is mentioned past that screen.
9
- 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 `named`**, `enable` that app when it is off, and switch no other off. **After Skip**, write nothing more: the apps that are on stay on. Done when the last answer's `apps` show `enabled` true for every chosen app and false for the rest (after `named`, true for that app), or, after Skip, nothing was written.
8
+ 2. **Ask which apps this run sets up**, on every run, with impeccable_ask: `id: "apps"`, `kind: "multi"`, and the `prompt` *"Which apps should get Impeccable previews?"*. The tool words the question and lists the state's apps (the repository's own, `main`, only where the Worker kept it as a product of its own), each picked as it is on, or every one after this run's confirm. Mark the Set up monorepo apps task in progress first. Every write and host tool for an app is refused until it is answered. It has no Skip: the person picks at least one app. Done when it answered `values` or `named` (the command was started for that one app with `--app`, from its card on the dashboard: the tool showed one confirm screen naming that app instead of the list, and the list all the same when the person asked for other apps there). Where no answer came (the input ended), the tool wrote nothing and the run ends there: call nothing more. After `named`, that app is the one this run sets up: every host read, key, proof, method and check is its own, and no other app is mentioned past that screen.
9
+ 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 `named`**, `enable` that app when it is off, and switch no other off. Done when the last answer's `apps` show `enabled` true for every chosen app and false for the rest (after `named`, true for that app).
10
10
  4. **Read the state again** with impeccable_state. Done when the preview item's `apps` names each chosen app's `directory`, `build` and `method` (none yet for an app not set up), and `perApp` its status.
11
- 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 (server-previews when no host deploys it, a host's case when the host deploys that app as a project of its own, proven on that app's own address: impeccable_hosted_previews, impeccable_netlify_previews and impeccable_cloudflare_previews take its `appKey` and list only the previews of the project or site that builds its folder (impeccable_netlify_connect takes it too, and connects that site), so another app's address, project or key never stands in for it; their `unmatched` says why none can be told, and that app goes as server-previews says, with the reason under "What is not verified"), 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"*). **The lock in front of each app's previews is that app's own**: when its proof answers a `gate`, preview-protection's steps run for that app, with its `appKey`, before any other app's step (the tools refuse the next app's reads and writes until its key tool was called), and the key goes to that app's own project or site; a key saved for one app says nothing about another's, and the state's `keySaved` speaks for one app at most. Done when every chosen app's method is saved, or its preview skipped as server-previews says, and its sign-in saved or skipped, and no other app got a write, a build or a question.
12
- 6. **The pull requests** as pull-requests says, once for the repository: the setup commit takes the state's one workflow `file` once every chosen app's method and build settings are saved, or its preview skipped as server-previews says; the test drive adds its page to every chosen app with a preview (an app whose preview was skipped gets none, and goes under "What was skipped, and why"), 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 builds each chosen app as its host's case says; its step 4 proves the test drive's page on each chosen app's own preview, with that app's key where its lock stands in front; its step 6 checks each chosen app with a preview, with its `appKey`, one call each. Done when pull-requests' steps are done.
11
+ 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 (server-previews when no host deploys it, a host's case when the host deploys that app as a project of its own, proven on that app's own address: impeccable_hosted_previews, impeccable_netlify_previews and impeccable_cloudflare_previews take its `appKey` and list only the previews of the project or site that builds its folder (impeccable_netlify_connect takes it too, and connects that site), so another app's address, project or key never stands in for it; their `unmatched` says why none can be told, and that app goes as server-previews says, with the reason under "What is not verified"), 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"*). **The lock in front of each app's previews is that app's own**: when its proof answers a `gate`, preview-protection's steps run for that app, with its `appKey`, before any other app's step (the tools refuse the next app's reads and writes until its key tool was called), and the key goes to that app's own project or site; a key saved for one app says nothing about another's, and the state's `keySaved` speaks for one app at most. Where a host's setup case (vercel-setup, netlify-setup) opens and records an app's test drive here, check that app's preview right after the recording, as pull-requests' step 6 says with its `appKey`, while its test drive is still the newest open pull request, before the next app is set up. Done when every chosen app's method is saved, or its preview skipped as server-previews says, and its sign-in saved or skipped, and no other app got a write, a build or a question.
12
+ 6. **The pull requests** as pull-requests says: the setup commit once for the repository, taking the state's one workflow `file` once every chosen app's method and build settings are saved, or its preview skipped as server-previews says. A run that sets up one app opens its test drive as pull-requests says, on `impeccable/test-drive`, its page in that app alone. A run that sets up more opens **a test drive of its own for each chosen app with a preview**, one app after the other, so each app's review shows on its own pull request; an app whose host's setup case (vercel-setup, netlify-setup) already opened, recorded and checked its test drive in step 5 has it, and gets none again. An app's test drive is branch `impeccable/test-drive-<appKey>` from the setup commit (from the default branch when there is none), with its page in that app alone, in that app's own framework and linked from that app's own public page, and no change to any other app; that case's step 3 builds that app as its host's case says; its step 4 pushes it and proves its page on that app's own preview, with that app's key where its lock stands in front; its step 5 opens it with the app's name in the title (*"Impeccable test drive: Storefront (safe to close)"*) and records it with impeccable_test_pr and that app's `appKey` (which the tool needs only when the run sets up more than one app); its step 6 checks that app with its `appKey` while its test drive is the newest open pull request, before the next app's test drive opens. An app whose preview was skipped gets no test drive, and goes under "What was skipped, and why". Step 7, when there is a setup commit, opens the setup pull request once, last, and records it with the last app's test drive and its `appKey` again. Done when every chosen app with a preview has its test drive recorded and checked, or the reason it has none in the report, and pull-requests' step 7 is done.
13
13
  - **A chosen app whose preview was skipped** (server-previews): it stays on, with no method, no job in the workflow and no test drive page. It holds nothing back: the other apps' setup commit, test drive and checks go on as above, and its checkup entry left in `next` counts as done for the playbook's step 5. The report says, on its line and under "What was skipped, and why", that it gets no preview until its host deploys them.
14
- 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, named with `--app`, on already after a skip), its method, its sign-in, what was verified; an app the question did not offer and the state shows off (`enabled` false) was turned off on the setup page, and its line says so, never that it was left off in the question; after `named`, an app still on that this run did not set up is outside this run, not turned off; an app left out is under "What was skipped, and why" as well, with the setup page as where it is turned on. impeccable_report adds a section of its own, "Each app, as this run left it", from what the run saved and proved, and the last screen says "You're all set up" only when every chosen app is: an app whose preview met a lock with no key saved, or got no method, is named there with its one step, so the report's lines say the same. Done when every app is on one line.
14
+ 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, named with `--app`), its method, its sign-in, what was verified; an app the question did not offer and the state shows off (`enabled` false) was turned off on the setup page, and its line says so, never that it was left off in the question; after `named`, an app still on that this run did not set up is outside this run, not turned off; an app left out is under "What was skipped, and why" as well, with the setup page as where it is turned on. impeccable_report adds a section of its own, "Each app, as this run left it", from what the run saved and proved, and the last screen says "You're all set up" only when every chosen app is: an app whose preview met a lock with no key saved, or got no method, is named there with its one step, so the report's lines say the same. Done when every app is on one line.
15
15
 
16
16
  ### Reference
17
17
 
@@ -5,13 +5,13 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
5
5
  ### Steps
6
6
 
7
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.
8
+ - **`cli: "missing"`**: the CLI was not on this machine, and the person said Install on the tool's screen for it: install it with `npm install -g netlify-cli@27.11.2`, exactly so (the run installs it with npm alone, pinned to that version, only after that Install, and refuses another package manager, version or tag); then ask again. The setup screen shows no second time; an install that did not complete brings the steps for the person's own install up instead.
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 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
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
12
  - **A site already built from this repository** (impeccable_netlify_previews named it in `sites`; on a repository with several apps, read with the app's `appKey`): link nothing, since `netlify link` asks in a terminal this run does not have when several sites are built from the repository. Step 3 finds that site by itself.
13
13
  3. **Connect the repository** with impeccable_netlify_connect (with the app's `appKey` on a repository with several apps): 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.
14
- 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
+ 4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say (on a repository with several apps, where the run sets up more than one, that app's own test drive, on `impeccable/test-drive-<appKey>` with its page in that app alone, as the monorepo case's step 6 says), 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` (the app's own branch, where it has one) succeeded.
15
15
  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".
16
16
 
17
17
  ### Reference
@@ -4,13 +4,14 @@ impeccable_prove_preview answered a `gate`: the host's own lock stands in front
4
4
 
5
5
  ### Steps
6
6
 
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 Vercel's project with or without a `.vercel/` link (no `vercel link` is needed, and the tool signs the Vercel CLI in where it is signed out), then 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, to make a site behind Netlify's team login public (signing the Netlify CLI in, in the browser, on that yes where it is signed out; where Netlify refuses, it opens Netlify's Visitor access page and asks the person to try again once they changed it there), or to create a service token (naming the Access application in front of the preview, its Cloudflare account and the app, once cf signed in here found it; none found, nothing is asked; then Cloudflare's sign-in in the browser where cf here lacks it, and it creates the token as the person and allows it in the Access application's policy; a screen takes a Cloudflare API token only where that sign-in cannot), or, when it cannot make or read one, one screen taking a pasted one (Vercel's with the page where it is made opened); 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, and `apiToken` says a Cloudflare API token the person made created the token. Step 2.
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 Vercel's project with or without a `.vercel/` link (no `vercel link` is needed, and the tool signs the Vercel CLI in where it is signed out), then 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 set a new one on the site where Netlify does not give it back (it replaces the current one, and nothing is pasted), to make a site behind Netlify's team login public (signing the Netlify CLI in, in the browser, on that yes where it is signed out; where Netlify refuses, it opens Netlify's Visitor access page and asks the person to try again once they changed it there), or to create a service token (naming the Access application in front of the preview, its Cloudflare account and the app, once cf signed in here found it; none found, nothing is asked; then Cloudflare's sign-in in the browser where cf here lacks it, and it creates the token as the person and allows it in the Access application's policy; a screen takes a Cloudflare API token only where that sign-in cannot), or, when it cannot make or read one, one screen taking a pasted one (Vercel's with the page where it is made opened); 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, set 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, and `apiToken` says a Cloudflare API token the person made created the token. Step 2.
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
+ - Any answer with `replaced` but `saved`: the run set a new Netlify site password, which replaced the one before, and it is not saved for reviews; the report says so, and that the person sets a password they know in Visitor access and in the app's Settings, whatever step comes next.
12
13
  - `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 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 Netlify's form did not take it, 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.
14
+ - 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 the run set) got past the lock to the app's own sign-in on the page you proved, or Netlify's form did not take it, 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.
14
15
  - Any other `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
15
16
  - `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", "Type the Netlify site password in the app's Settings, under "A way past your host's lock"" or "Save the Cloudflare service token on the setup page" in `left`.
16
17
  2. **Check it from Impeccable's side**: impeccable_write to `connection-check` with `{"appKey": "main"}` (on a repository with several apps, the app's own key: the tool saved the key for that app), and report what it answered. A check that answers `access: "auth-wall"` met the lock all the same: say so under "What is not verified", with its `detail`, and never as verified; the last screen does not call the app set up while it stands. 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.
@@ -19,7 +20,7 @@ impeccable_prove_preview answered a `gate`: the host's own lock stands in front
19
20
  ### Reference
20
21
 
21
22
  - **Before the test drive's push, on Vercel** (pull-requests' step 4): impeccable_vercel_bypass with `beforePush: true`, and the app's `appKey` on a repository with several apps, reads the project's settings and, where Vercel's login will stand in front of the test drive's preview, asks the person now, on a screen naming the project and the app. `protected: false`, or `protected: true` with `consent` `given` or `not-needed`, or `protected: "unknown"`: push. Once the proof meets the gate, step 1 runs as written, and the yes is not asked again. `skipped`: push and open no test drive, and end as step 3 says, with the tool's `reason`.
22
- - **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, with `opened`, the Netlify site made public, by this run on the person's yes or by the person on Netlify's page (`madePublic`), so anyone with its address opens it, and where it is made private again: the same 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 `apiToken`, the Cloudflare API token the person made for the run is theirs to delete (dash.cloudflare.com/profile/api-tokens). Revoking, regenerating or changing it stops reviews until a new one is saved on the setup page.
23
+ - **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 set a new one on it, replacing the one before, and where it is changed: the site's Project configuration → General → Visitor access; or, with `opened`, the Netlify site made public, by this run on the person's yes or by the person on Netlify's page (`madePublic`), so anyone with its address opens it, and where it is made private again: the same 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 `apiToken`, the Cloudflare API token the person made for the run is theirs to delete (dash.cloudflare.com/profile/api-tokens). Revoking, regenerating or changing it stops reviews until a new one is saved on the setup page.
23
24
  - **The tool does the writes**: the key as Impeccable's credential for the lock, and the setting that sends it (Vercel's `x-vercel-protection-bypass` header, Netlify's password form, or Cloudflare's `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers), 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.
24
25
  - **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. On a repository with several apps, the item's `keySaved` speaks for one app at most: an app whose own proof answered a `gate` gets step 1 with its `appKey`, whatever another app's key did, and its step 2 check is its own.
25
26
  - **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.
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- Every run, once the settings are saved. The person merges the setup pull request and closes the test drive, so they must tell the two apart at a glance. The test drive exists to show a rendered review, so its pull request opens only once its preview is proven: **no preview, no test drive**.
3
+ Every run, once the settings are saved. The person merges the setup pull request and closes the test drive, so they must tell the two apart at a glance. On a repository with several apps, each chosen app gets a test drive of its own, as the monorepo case's step 6 says: these steps then run once an app for the test drive, under its own branch. The test drive exists to show a rendered review, so its pull request opens only once its preview is proven: **no preview, no test drive**.
4
4
 
5
5
  ### Steps
6
6
 
@@ -5,14 +5,14 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
5
5
  ### Steps
6
6
 
7
7
  1. **Sign in**, before any other preview work, with impeccable_ask: `kind: "confirm"`, `id: "vercel-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 Vercel CLI is signed out and offers a retry. Done when it answered `confirmed`, with `team` the slug the project goes into.
8
- - **`cli: "missing"`**: install the CLI with `npm install -g vercel@62.7.0`, exactly so: the run installs it with npm alone, pinned to that version, and refuses another package manager, version or tag; then ask again: the screen shows no second time.
8
+ - **`cli: "missing"`**: the CLI was not on this machine, and the person said Install on the tool's screen for it: install it with `npm install -g vercel@62.7.0`, exactly so (the run installs it with npm alone, pinned to that version, only after that Install, and refuses another package manager, version or tag); then ask again. The setup screen shows no second time; an install that did not complete brings the steps for the person's own install up instead.
9
9
  - **`teams` in the answer instead of `team`** (several teams, none current): ask once which owns the project, `kind: "single"`, `id: "vercel-team"`, `options` the teams as given (the tool words the rest). The pick is the team.
10
10
  - **Neither `team` nor `teams`**: the account has no team, and `vercel link --yes` without `--team` links in the person's own scope.
11
11
  - **Skip**: set the preview up as server-previews says, and say under "What was skipped, and why" that Vercel was found and the sign-in was skipped. The steps below do not apply.
12
12
  2. **Create the project, then link it** (run each Vercel command plain, on its own: its answer is short, a pipe to `tail` is refused, and a command chained after it leaves the project unrecorded): `vercel project add <the folder's name> --team <the team>` creates the project in that team, under the name `vercel link` gives a project (the checkout folder's name, lowercase); then `vercel link --yes --team <the team>`, in the checkout's root (never with `--cwd` or after a `cd`), links the folder to the project of that name, which is now the run's. The CLI names no team on its own without a terminal, so the option is given whenever there is a team. Done when the link prints the linked project.
13
13
  - **`vercel project add` fails** (the name is in use: the project is the person's, not this run's): run no link; the Vercel setup ends there, and with the checkout linked to no project its CLI stops and the hold on pushes goes. Go on as "A step fails before the test drive is pushed" says, naming the project under "What was skipped, and why". When `.vercel/project.json` was in the checkout before the run, that link is not this run's, and the hold on pushes stays, by design: no test drive, as "No preview for the pushed test drive" says, and the report says under "What was skipped, and why" that the person removes `.vercel/project.json` or links the project themselves and runs the setup again.
14
14
  3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, when that project is the one step 2 created or one connected to this repository already, asks the person for Vercel's GitHub app's access when the app cannot reach the repository, and then, on a project with no production deployment, puts Vercel's login in front of every address of the project, its own included. Vercel makes a new project's first deployment Production whatever branch it builds and serves it at the project's own address, and the test drive's push is that first deployment. When Vercel refuses the lock, the tool asks the person on one screen to turn it on in the project's settings and waits for it. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
15
- 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 the new page and the page that links to it 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
+ 4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say (on a repository with several apps, where the run sets up more than one, that app's own test drive, on `impeccable/test-drive-<appKey>` with its page in that app alone, as the monorepo case's step 6 says), with the changes vercel-previews' reference gives: from the default branch, no setup pull request, built once with plain commands. Stage the new page and the page that links to it 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` (the app's own branch, where it has one) succeeded.
16
16
  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".
17
17
 
18
18
  ### Reference
package/dist/playbook.js CHANGED
@@ -41,7 +41,7 @@ The state names the setup page's other cards in \`notPartOfThisSetup\`. They are
41
41
  - Ask through impeccable_ask, which shows a dialog and returns the answer; a question written in your message reaches nobody.
42
42
  - Ask only for what the repository cannot tell you. Running this command is the person's go-ahead for every step, so ask no approval questions.
43
43
  - Ask the questions that belong together back to back, one call each.
44
- - Every question offers Skip: after a skip, carry on without the answer, and say in the report what it left undone.
44
+ - Every question offers Skip but the apps question on a repository with several apps, which needs at least one app (the monorepo case): after a skip, carry on without the answer, and say in the report what it left undone.
45
45
  - **No run ends by telling the person to run the setup again**: what a step needs from them is asked in this run, on the tool's screen, and the run carries on. After a skip, or retries that ran out, the report says what stopped it, and no \`left\` line or question asks for a rerun.
46
46
 
47
47
  ## Commands
@@ -66,7 +66,7 @@ The person reads your [STATUS] lines, task titles and questions, here and on the
66
66
  - **Questions**: one sentence, at most one hint line, and a title that names the subject.
67
67
 
68
68
  ## The report
69
- impeccable_report ends the run: no tool or model call works after it, so call it once, alone, last. Open the report with the pull requests, one line each with its link: the test drive, which Impeccable is reviewing (its review had not posted when the run ended; the web page shows it) and which the person closes when done, or, when none opened, what stopped it in a few words; then the setup pull request, when there is one, which they merge to keep previews working, or, when a host builds the previews and the repository needed no change, this one line with the host's name: *Nothing to merge: Vercel hosts your previews, so the repository needed no change.* Then these sections:
69
+ impeccable_report ends the run: no tool or model call works after it, so call it once, alone, last. Open the report with the pull requests, one line each with its link: the test drive (each app's, named, on a repository with several apps), which Impeccable is reviewing (its review had not posted when the run ended; the web page shows it) and which the person closes when done, or, when none opened, what stopped it in a few words; then the setup pull request, when there is one, which they merge to keep previews working, or, when a host builds the previews and the repository needed no change, this one line with the host's name: *Nothing to merge: Vercel hosts your previews, so the repository needed no change.* Then these sections:
70
70
 
71
71
  \`\`\`markdown
72
72
  ## What we found
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The summary in each pull request's description, with the pages it changes before and after (#402): pristine's
3
+ * `modules.overview`, off by default because it writes into the author's description, so the person is asked once,
4
+ * worded here and never by the model. One question per repository, since pristine keeps the setting there.
5
+ */
6
+ export const PR_SUMMARY_SCREEN = {
7
+ id: 'pr-summary',
8
+ kind: 'confirm',
9
+ title: 'See every change, before and after',
10
+ hint: 'Each pull request gets side-by-side screenshots of the pages it changes, right in its description, so reviewers spot the difference at a glance. Turn it off any time in Settings.',
11
+ prompt: 'Show before and after on your pull requests?',
12
+ confirmLabel: 'Show it',
13
+ skipLabel: 'Not now',
14
+ };
15
+ /** The end screen's line when the summary is not on. */
16
+ export const PR_SUMMARY_OFF = 'Before and after is off: turn it on any time in Settings.';
17
+ const modulesOf = (data) => {
18
+ const modules = data?.effective?.modules;
19
+ return modules && typeof modules === 'object' && !Array.isArray(modules) ? modules : null;
20
+ };
21
+ /**
22
+ * Reads the repository's review settings, and unless the summary is on already asks the person; on their yes, reads
23
+ * them again, since Settings may have changed while the question was up, and saves `modules.overview: true` with every
24
+ * other module as it is in force then: the save replaces the whole `modules`, and the API has no conditional write.
25
+ * A Not now, an answer that never came (headless, closed input) or a settings read that failed writes nothing.
26
+ */
27
+ export async function askPrSummary(read, ask, save) {
28
+ const inForce = async () => {
29
+ const current = await read();
30
+ return current.ok ? modulesOf(current.data) : null;
31
+ };
32
+ const before = await inForce();
33
+ if (!before)
34
+ return 'off';
35
+ if (before.overview === true)
36
+ return 'on';
37
+ if ((await ask(PR_SUMMARY_SCREEN)).skipped)
38
+ return 'off';
39
+ const modules = await inForce();
40
+ if (!modules)
41
+ return 'off';
42
+ if (modules.overview === true)
43
+ return 'on';
44
+ return (await save({ modules: { ...modules, overview: true } })).ok ? 'on' : 'off';
45
+ }
package/dist/preflight.js CHANGED
@@ -316,7 +316,7 @@ export function currentBranch(root) {
316
316
  */
317
317
  export function backToStartBranch(root, start) {
318
318
  const now = currentBranch(root);
319
- if (now === start || !/^impeccable\/(?:setup|test-drive)(?:-\d+)?$/.test(now))
319
+ if (now === start || !/^impeccable\/(?:setup(?:-\d+)?|test-drive(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?)$/.test(now))
320
320
  return null;
321
321
  if (!start)
322
322
  return `Your checkout is on ${now}, the branch the setup made.`;
package/dist/sandbox.js CHANGED
@@ -16,7 +16,7 @@ let pinned;
16
16
  let own;
17
17
  /**
18
18
  * A folder of the command's own, outside the checkout and everything a sandboxed command writes. What runs from here
19
- * as the person and fetches a package (npx for cf, a host CLI's install) reads no .npmrc and no node_modules of the
19
+ * as the person and fetches a package (a host CLI's install) reads no .npmrc and no node_modules of the
20
20
  * repository's choosing.
21
21
  */
22
22
  export function ownFolder() {
@@ -26,7 +26,7 @@ export function ownFolder() {
26
26
  }
27
27
  return own;
28
28
  }
29
- /** npm's cache for what runs from there: a host CLI's install and npx for cf fetch through it, and never through ~/.npm, whose entries a sandboxed install could have planted for them. */
29
+ /** npm's cache for what runs from there: a host CLI's install fetches through it, and never through ~/.npm, whose entries a sandboxed install could have planted for them. */
30
30
  export const ownNpmCache = () => join(ownFolder(), 'npm-cache');
31
31
  /**
32
32
  * The environment of every git that runs outside the sandbox, the command's own and the person's commands (gh and the
@@ -438,8 +438,8 @@ const scratchCaches = (scratch) => ({ npm_config_cache: join(scratch, 'npm-cache
438
438
  * hard-linked, so a write through an installed file changes neither the store nor a global command linked from it.
439
439
  */
440
440
  export const pnpmEnvironment = (home, platform) => ({ pnpm_config_store_dir: join(home, platform === 'darwin' ? 'Library/pnpm/store' : '.local/share/pnpm/store'), pnpm_config_package_import_method: 'copy' });
441
- /** For a host CLI's install outside the sandbox: npm's cache of the command's own, and the registry asked afresh for what it has. */
442
- const installEnvironment = () => ({ npm_config_cache: ownNpmCache(), npm_config_prefer_online: 'true' });
441
+ /** For a host CLI's install outside the sandbox, the agent's or the tools' own: npm's cache of the command's own, and the registry asked afresh for what it has. */
442
+ export const installEnvironment = () => ({ npm_config_cache: ownNpmCache(), npm_config_prefer_online: 'true' });
443
443
  /** The sandbox's own command line, which the local preview check runs each command through, in a sandbox of its own. */
444
444
  const SRT = createRequire(import.meta.url).resolve('@anthropic-ai/sandbox-runtime/dist/cli.js');
445
445
  /**