@impeccable/reviews 0.1.31 → 0.1.33

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/hosting.js CHANGED
@@ -33,7 +33,24 @@ export function hostingOf(state, repo) {
33
33
  return [{ key, liveUrl: address(a.liveUrl), previews }];
34
34
  });
35
35
  const unread = Array.isArray(hosting.unread) ? hosting.unread.flatMap((u) => text(u) ?? []) : [];
36
- return { apps, readAt: text(hosting.readAt), unread };
36
+ return { apps, readAt: text(hosting.readAt), unread, ...(hosting.readPending === true ? { readPending: true } : {}) };
37
+ }
38
+ /**
39
+ * The record once it is whole: while the state says a read of it is pending, the state is read again every few seconds,
40
+ * up to a minute by the clock, each read bounded by what is left of it. Past that, its previews are left out and it
41
+ * counts as never read, so each host tool reads its previews itself, as it does without a record; the live addresses stay.
42
+ */
43
+ export async function settledHosting(first, repo, readState, opts = {}) {
44
+ const { everyMs = 3_000, upToMs = 60_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now } = opts;
45
+ const deadline = now() + upToMs;
46
+ let hosting = hostingOf(first, repo);
47
+ while (hosting?.readPending && now() + everyMs < deadline) {
48
+ await sleep(everyMs);
49
+ const state = await readState(deadline - now());
50
+ if (state.ok)
51
+ hosting = hostingOf(state.data, repo) ?? hosting;
52
+ }
53
+ return hosting?.readPending ? { apps: hosting.apps.map((a) => ({ ...a, previews: [] })), readAt: null, unread: hosting.unread } : hosting;
37
54
  }
38
55
  /** The reports each host's previews are read from, besides the deployments every read takes. */
39
56
  const REPORTS = { netlify: ['statuses', 'comments'], cloudflare: ['check-runs', 'comments'], render: [] };
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { runAgent } from './agent.js';
7
7
  import { runSkillCommand } from './skill.js';
8
8
  import { fenceExtension, guardedFileOperations, hostsAgreed, placeOf, previewRetryState } from './fence.js';
9
9
  import { accountsSaved, focusInstructions, focusState, severalApps, signInItem } from './focus.js';
10
- import { hostingOf } from './hosting.js';
10
+ import { settledHosting } from './hosting.js';
11
11
  import { requestJson } from './http.js';
12
12
  import { copyText, letTabsHear, openBrowser, progressPage, screenPrompt, setupAuth, signIn } from './login.js';
13
13
  import { checkoutJournal } from './leftovers.js';
@@ -234,6 +234,8 @@ export async function main(argv) {
234
234
  ]);
235
235
  if (!instructions.ok || !state.ok)
236
236
  throw new UserError(`Impeccable's setup API did not answer: ${instructions.error ?? state.error}.`);
237
+ // A hosting record behind GitHub is read again while the questions below are asked (hosting.ts `settledHosting`).
238
+ const hosting = settledHosting(state.data, token.repository.name, (timeoutMs) => requestJson(`${setupApi}/state`, { token: token.access_token, timeoutMs }, log));
237
239
  // When the checkout cannot tell, the setup state's own verdict decides. When it has a web front end Impeccable skipped, the person adds it first.
238
240
  if (stateSaysNoFrontEnd(state.data, token.repository.name)) {
239
241
  if (frontEnd === 'unknown')
@@ -395,7 +397,7 @@ export async function main(argv) {
395
397
  sandbox,
396
398
  pushAccess: push.outcome,
397
399
  severalApps: severalApps(state.data),
398
- hosting: hostingOf(state.data, token.repository.name),
400
+ hosting: await hosting,
399
401
  ...(flags.app ? { app: flags.app } : {}),
400
402
  start: checkout.start,
401
403
  ...(flags.signedInTestDrive ? { signedInTestDrive: { accountsSaved: accountsSaved(state.data, token.repository.name) } } : {}),
package/dist/tools.js CHANGED
@@ -16,7 +16,7 @@ import { checkoutNames, liveShaped, netlifySiteOf, previewHostName, provePreview
16
16
  import { closedPulls, deploymentsOf, hostingApp, hostingRead, liveHostOf, renderSitesOf } from './hosting.js';
17
17
  import { enableRenderPreviews, parseRenderDeployment, readRenderService, RENDER_DASHBOARD, renderSitesNamed } from './render.js';
18
18
  import { cloudflareFromHosting, cfRunner, cfStatus, CLOUDFLARE_APP_PAGE, connectCloudflareProject, enableCloudflarePreviews, pagesProject, pagesProjectName, readCloudflarePreviews } from './cloudflare.js';
19
- import { passCloudflareAccess } from './cloudflare-access.js';
19
+ import { passCloudflareAccess, TOKEN_NAME } from './cloudflare-access.js';
20
20
  import { netlifyFromHosting, commandRunner, connectNetlifySite, liveSite, netlifyStatus, netlifyTeams, readNetlifyPreviews, waitForNetlifyAccess } from './netlify.js';
21
21
  import { netlifyToken, passNetlifyLock, setSitePassword } from './netlify-password.js';
22
22
  import { connectVercelProject, lockedDeploymentHosts, prepareFirstDeployment, projectMadeDeployment, readLinkedProject, readOwnProject, vercelDashboardScope, vercelPreviewsFact, vercelProjectFor, vercelProjectPage, vercelRepoProject, vercelStatus, vercelTeams, waitForVercelAccess, waitForVercelLock } from './vercel.js';
@@ -213,6 +213,8 @@ function plainReason(text) {
213
213
  }
214
214
  /** Names as a reader lists them: "a", "a and b", "a, b and c". */
215
215
  const andList = (names) => (names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names.join(''));
216
+ /** The end screen's line for a Cloudflare service token the run created: what it made, on which Access application, and where it is revoked (REN-1187). */
217
+ const accessNote = (application) => `Impeccable created the Cloudflare service token "${TOKEN_NAME}" and its policy on the Access application "${application}". Revoke it in Zero Trust → Access controls → Service credentials; reviews stop until a new one is saved.`;
216
218
  /** The end screen's one line for the Netlify sites whose password the run set and saved, set but did not save, or saved as it was: every site in one line (#419). */
217
219
  const PASSWORD_NOTES = {
218
220
  set: (sites) => sites.length === 1
@@ -2840,10 +2842,12 @@ export function createTools(ctx) {
2840
2842
  }
2841
2843
  if (testAccounts && response.ok)
2842
2844
  retry.signedIn?.saved.add(appsRepo ? String(object(p.body)?.appKey) : '');
2843
- // The accounts the save signed in with, said on screen: the tasks show no status line (#417).
2845
+ // The accounts the save signed in with, said on screen: the tasks show no status line (#417). The web page moves on with it, not at the model's next status (REN-1183).
2844
2846
  const signedIn = testAccounts && response.ok ? signedInLine(response.data, appsRepo ? appName(String(object(p.body)?.appKey)) : undefined) : undefined;
2845
- if (signedIn)
2847
+ if (signedIn) {
2846
2848
  reportDone(signedIn);
2849
+ ctx.sync.update({ status: signedIn });
2850
+ }
2847
2851
  // A check that answered, a preview not running yet included, ran for its app; one still turned away did not.
2848
2852
  if (check && (response.ok || response.status === 409))
2849
2853
  checkedApps.add(typeof object(p.body)?.appKey === 'string' ? String(object(p.body)?.appKey) : '');
@@ -4202,6 +4206,9 @@ export function createTools(ctx) {
4202
4206
  });
4203
4207
  }
4204
4208
  const { value, ...told } = answer;
4209
+ // A token the run made stays in the person's Cloudflare account, saved or not: the end screen says so, and where it is revoked (REN-1187).
4210
+ if (value && told.token === 'created' && told.application)
4211
+ ctx.onKeyCreated?.(accessNote(told.application), `cloudflare-access:${told.application}`);
4205
4212
  if (value) {
4206
4213
  // What is in hand follows the store, which holds one token: a new one is the one saved now, and the projects the last one opened no longer stand.
4207
4214
  if (value !== accessToken)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
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,