@impeccable/reviews 0.1.12 → 0.1.13

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,5 +1,6 @@
1
+ import { realpathSync } from 'node:fs';
1
2
  import { readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
+ import { join, resolve, sep } from 'node:path';
3
4
  import { cloudflarePreviewsFact, cloudflarePreviewsPage, folderOf } from './host-previews.js';
4
5
  import { commandRunner } from './netlify.js';
5
6
  import { ownFolder, ownNpmCache } from './sandbox.js';
@@ -238,13 +239,50 @@ export const CF_PACKAGE = 'cf@1.0.0-beta.13';
238
239
  /**
239
240
  * cf here: its JSON uncoloured, and an account's command given its account, which cf otherwise asks the person to pick
240
241
  * when they have several. `browser` lets its sign-in open one. npx runs it from a folder of the command's own, never the
241
- * checkout, whose node_modules and .npmrc would choose which cf runs as the person, outside the sandbox: every cf
242
- * command names its project or its account, so none needs the checkout. It fetches cf through a cache of the command's
243
- * own, never ~/.npm, where a sandboxed install could have planted a cf under the registry's live ETag; the registry is
244
- * asked afresh, and no install script of cf's runs, which it does without.
242
+ * checkout, whose node_modules and .npmrc would choose which cf runs as the person, outside the sandbox, and from where cf
243
+ * itself would read the checkout's .env into its token and account and evaluate its cloudflare.config.ts: every cf
244
+ * command names its project or its account, so none needs the checkout. Only the sign-in cf picks by folder does: with
245
+ * `checkout`, each command runs as the profile cf binds to it (`cf auth activate`), which cf names from its own folder
246
+ * too, so the account is the one the person's cf uses there; where reading the bindings failed, the command fails as well,
247
+ * never falling back to another sign-in. It fetches cf through a cache of the command's own, never ~/.npm, where a
248
+ * sandboxed install could have planted a cf under the registry's live ETag; the registry is asked afresh, and no install
249
+ * script of cf's runs, which it does without.
245
250
  */
246
251
  export function cfRunner(opts = {}, runner = commandRunner) {
247
- return (args, { account, signal } = {}) => runner('npx', ownFolder(), { timeoutMs: 120_000, ...opts, signal, env: { NO_COLOR: '1', ...(account ? { CLOUDFLARE_ACCOUNT_ID: account } : {}) } })(['--yes', '--cache', ownNpmCache(), '--prefer-online', '--ignore-scripts', CF_PACKAGE, ...args]);
252
+ const { checkout, ...run } = opts;
253
+ const npx = (args, signal, env = {}) => runner('npx', ownFolder(), { timeoutMs: 120_000, ...run, signal, env: { NO_COLOR: '1', ...env } })(['--yes', '--cache', ownNpmCache(), '--prefer-online', '--ignore-scripts', CF_PACKAGE, ...args]);
254
+ let profile;
255
+ return async (args, { account, signal } = {}) => {
256
+ // Read once a runner; a failed read is read again by the next command.
257
+ if (checkout && !profile)
258
+ profile = npx(['auth', 'list'], signal).then((listed) => boundProfile(listed, checkout)).catch((err) => { profile = undefined; throw err; });
259
+ const bound = await profile;
260
+ return npx([...(bound ? ['--profile', bound] : []), ...args], signal, account ? { CLOUDFLARE_ACCOUNT_ID: account } : {});
261
+ };
262
+ }
263
+ /** The profile `cf auth list` binds to `checkout`, found as cf finds it from there: the longest bound folder that is the checkout's real path or holds it; none where no folder does. A list that is not cf's throws. */
264
+ export function boundProfile(listed, checkout) {
265
+ const profiles = JSON.parse(listed);
266
+ if (!Array.isArray(profiles))
267
+ throw new Error('cf auth list printed no list of profiles');
268
+ let here = resolve(checkout);
269
+ try {
270
+ here = realpathSync(here);
271
+ }
272
+ catch { /* cf's own folder, as cf resolves it */ }
273
+ let found;
274
+ for (const p of profiles) {
275
+ const { name, boundDirectories } = record(p) ?? {};
276
+ if (typeof name !== 'string' || !/^[A-Za-z0-9_-]+$/.test(name) || !Array.isArray(boundDirectories))
277
+ continue;
278
+ for (const dir of boundDirectories) {
279
+ if (typeof dir !== 'string' || !(here === dir || (here.startsWith(dir) && here[dir.length] === sep)))
280
+ continue;
281
+ if (!found || dir.length > found.dir.length)
282
+ found = { name, dir };
283
+ }
284
+ }
285
+ return found?.name;
248
286
  }
249
287
  /** The status of the API call a cf command failed on, which its error box names on a line of its own ("│ 403 Forbidden", "│ 404 Not Found · HTTP /accounts/…"); null when it names none. */
250
288
  export function cfFailure(err) {
@@ -406,7 +444,7 @@ export async function readCloudflarePreviews(owner, repo, cwd, deps = {}) {
406
444
  // `project`, one app's told by an earlier read, is the one read: never the project the first check run names, which may be another app's.
407
445
  const named = app ? undefined : deps.project ?? github.previews.map((p) => pagesProject(p.url)).find(Boolean) ?? (config?.kind === 'pages' ? config.name : undefined);
408
446
  // cf installs itself through npx on a machine without it, which takes a while the first time. The project's settings are read with the plain evidence, not while a build is waited for.
409
- const cf = await fromCf(named, deps.cf ?? cfRunner(), { owner, repo, heads: github.heads.map((h) => h.sha), readme: readme.pages, ...(app ? { directory: deps.directory } : {}) }, plain, deps.signal);
447
+ const cf = await fromCf(named, deps.cf ?? cfRunner({ checkout: cwd }, deps.run), { owner, repo, heads: github.heads.map((h) => h.sha), readme: readme.pages, ...(app ? { directory: deps.directory } : {}) }, plain, deps.signal);
410
448
  const where = deps.directory ? `the folder ${deps.directory}` : "the repository's root";
411
449
  const unmatched = !app ? undefined
412
450
  : cf.cli !== 'signed-in' ? `Which Cloudflare Pages project builds ${where} cannot be told: cf here is not signed in with Pages access, and only a project's settings name the folder it builds (its root directory).`
package/dist/main.js CHANGED
@@ -256,8 +256,8 @@ export async function main(argv) {
256
256
  todo: [...apps.todo, ...report.left],
257
257
  warnings: [...removed, ...(saved && !saved.ok ? [`The web page missed the last update (${saved.error}), so it may lag behind this.`] : [])],
258
258
  }, hand);
259
- // The web page has the report; when it missed it, the report stays here instead.
260
- if (!saved?.ok)
259
+ // The web page has the report; when it missed it, or there is no test drive, whose why it holds, the report stays here as well.
260
+ if (!saved?.ok || !testPr)
261
261
  say(`\n${plainReport(report.markdown)}`);
262
262
  if (!saved?.ok)
263
263
  reporter.step({ event: 'setup_command_stopped', outcome: 'update_lost' });
package/dist/tools.js CHANGED
@@ -137,7 +137,7 @@ function stageError(status, tasks) {
137
137
  }
138
138
  /** The line under the last screen's heading ("You're all set up", or "Settings saved" without a test drive), about the pull request that screen ends on: the setup pull request when there is one, else the test drive; and, where a host builds the previews and so no setup pull request was needed, one line saying so. */
139
139
  export function resultLine(report) {
140
- const lead = !report.testPr ? 'No test drive: the report says why.' : report.setupPr ? 'The setup pull request adds the workflow that lets Impeccable run on your previews. Merge it when you are ready.' : SEE_THE_REVIEW;
140
+ const lead = !report.testPr ? 'No test drive: the report says why, printed here when this screen closes.' : report.setupPr ? 'The setup pull request adds the workflow that lets Impeccable run on your previews. Merge it when you are ready.' : SEE_THE_REVIEW;
141
141
  return report.setupPr || !report.hostedBy ? lead : `${lead} ${nothingToMerge(report.hostedBy)}`;
142
142
  }
143
143
  /** Said once, here and in the report, when the host builds the previews and the repository needed no change. */
@@ -2797,7 +2797,7 @@ export function createTools(ctx) {
2797
2797
  const turnOn = async () => {
2798
2798
  if (host === 'netlify')
2799
2799
  return enableNetlifyPreviews((ctx.hostRunner ?? commandRunner)('netlify', pi.cwd, { signal }), fact.project);
2800
- const cf = cfRunner({}, ctx.hostRunner);
2800
+ const cf = cfRunner({ checkout: pi.cwd }, ctx.hostRunner);
2801
2801
  return enableCloudflarePreviews(cf, fact.project, await cfStatus(cf, signal), signal);
2802
2802
  };
2803
2803
  // The run's change, read back; where the host kept the setting, the person changes it on the host's page and the run reads again, twice at most.
@@ -2842,8 +2842,8 @@ export function createTools(ctx) {
2842
2842
  if ((await ask({ ...screen, prompt: create ? `Create the Pages project ${project} for ${ctx.owner}/${ctx.repo}?` : `Connect ${ctx.owner}/${ctx.repo} to the Pages project ${project}?` })).skipped)
2843
2843
  return answer('skipped', { reason: `The person skipped ${create ? 'creating the Pages project' : 'connecting the repository to the Pages project'}.` });
2844
2844
  // cf signed in with pages:write, as it is or after Cloudflare's sign-in in the browser, keeping the scopes it held; a sign-in that did not complete offers a retry, twice.
2845
- const cf = cfRunner({}, ctx.hostRunner);
2846
- const login = cfRunner({ browser: true, timeoutMs: 300_000 }, ctx.hostRunner);
2845
+ const cf = cfRunner({ checkout: pi.cwd }, ctx.hostRunner);
2846
+ const login = cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd }, ctx.hostRunner);
2847
2847
  const writes = (status) => status.cli === 'signed-in' && (!status.scopes || status.scopes.includes('pages:write'));
2848
2848
  let status = await cfStatus(cf, signal);
2849
2849
  if (!writes(status)) {
@@ -3121,8 +3121,8 @@ export function createTools(ctx) {
3121
3121
  answer = await passCloudflareAccess({
3122
3122
  url: at.url,
3123
3123
  consent: async () => !(await ask(sentTo(HOST_SCREENS['cloudflare-access'], at.url))).skipped,
3124
- cf: cfRunner({}, ctx.hostRunner),
3125
- login: cfRunner({ browser: true, timeoutMs: 300_000 }, ctx.hostRunner),
3124
+ cf: cfRunner({ checkout: pi.cwd }, ctx.hostRunner),
3125
+ login: cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd }, ctx.hostRunner),
3126
3126
  retry: async () => !(await ask(HOST_SCREENS['cloudflare-signin-retry'])).skipped,
3127
3127
  status: said,
3128
3128
  apiToken: async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Set up a repository for Impeccable for PRs from your terminal.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,