@impeccable/reviews 0.1.16 → 0.1.18

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/ui/look.js CHANGED
@@ -80,18 +80,29 @@ function balanced(text, max) {
80
80
  const said = (text, inner, st) => text.split('\n\n').flatMap((part, i) => [...(i ? [''] : []), ...balanced(part, Math.min(TEXT_WIDTH, inner)).map((line) => center(st.soft(line), inner))]);
81
81
  /** Wrapped text whose first line starts with `first` and the rest line up under it. */
82
82
  const hang = (text, width, first) => wrapTextWithAnsi(text, Math.max(10, width - first.length)).map((line, i) => (i ? ' '.repeat(first.length) : first) + line);
83
- /** An address cut into rows no wider than `width`, after a slash where one falls in the row's last two thirds, each linking to the whole of it, so any row opens it. */
84
- function urlRows(url, width, ink, st) {
83
+ /** An address or a folder cut into rows no wider than `width` columns, after a slash where one falls in the row's last two thirds, so a name in it breaks only where it must. */
84
+ function slashRows(text, width) {
85
85
  const size = Math.max(10, width);
86
86
  const rows = [];
87
- for (let rest = url; rest;) {
88
- const slash = rest.length > size ? rest.lastIndexOf('/', size - 1) + 1 : 0;
89
- const cut = rest.length <= size ? rest.length : slash > size / 3 ? slash : size;
87
+ for (let rest = text; rest;) {
88
+ // The longest start of it that fits, by the columns its characters take, a wide one counting two.
89
+ let fit = 0;
90
+ for (let columns = 0; fit < rest.length;) {
91
+ const char = String.fromCodePoint(rest.codePointAt(fit));
92
+ if ((columns += visibleWidth(char)) > size)
93
+ break;
94
+ fit += char.length;
95
+ }
96
+ const head = rest.slice(0, Math.max(fit, 1));
97
+ const slash = fit < rest.length ? Math.max(head.lastIndexOf('/'), head.lastIndexOf('\\')) + 1 : 0;
98
+ const cut = fit >= rest.length ? rest.length : slash && visibleWidth(head.slice(0, slash)) > size / 3 ? slash : head.length;
90
99
  rows.push(rest.slice(0, cut));
91
100
  rest = rest.slice(cut);
92
101
  }
93
- return rows.map((row) => st.link(url, ink(row)));
102
+ return rows;
94
103
  }
104
+ /** An address cut into rows as slashRows cuts it, each linking to the whole of it, so any row opens it. */
105
+ const urlRows = (url, width, ink, st) => slashRows(url, width).map((row) => st.link(url, ink(row)));
95
106
  /** A label, softer, over its address. */
96
107
  const linkLines = (label, url, inner, st) => [center(st.soft(label), inner), ...block(urlRows(url, ruleWidth(inner), st.full, st), inner)];
97
108
  /** The way to put a link on the clipboard, while it is not there yet. */
@@ -102,6 +113,8 @@ const pullAction = (url, copied, inner, st) => [
102
113
  copied ? center(st.soft('Copied to clipboard!'), inner) : copyWay(inner, st),
103
114
  ];
104
115
  /** What waits on the person, under the supporting text: two lines at most, the rest counted. */
116
+ /** The hosts the run used, a line each under the result. */
117
+ const usedLines = (outro, inner, st) => (outro.used ?? []).flatMap((line) => said(line, inner, st));
105
118
  function leftLines(todo, inner, st) {
106
119
  const shown = todo.length > 2 ? [todo[0], `${todo.length - 1} more in the report`] : todo;
107
120
  return shown.map((line) => center(st.soft(`→ ${line}`), inner));
@@ -244,12 +257,10 @@ function runPage(scene, f, inner, st) {
244
257
  const waiting = row(`waiting for ${scene.wait.host ? `${scene.wait.host}'s preview` : 'the preview'} · ${scene.wait.timer}`);
245
258
  return ink(visibleWidth(waiting) <= inner ? waiting : row(scene.wait.timer));
246
259
  });
247
- const counted = scene.tasks.filter((t) => t.status !== 'skipped');
248
- const skipped = scene.tasks.length - counted.length;
249
260
  const widest = Math.max(...lines.map((line) => visibleWidth(line)));
261
+ // No count over the list: the list shows each task's state itself.
250
262
  return {
251
263
  heading: 'Setting up',
252
- text: said(`${counted.filter((t) => t.status === 'completed').length} of ${counted.length} done${skipped ? ` · ${skipped} skipped` : ''}`, inner, st),
253
264
  rule: divider(inner, st),
254
265
  action: block(lines, inner, widest),
255
266
  at: running >= 0 ? running : Math.max(0, scene.tasks.findIndex((t) => t.status === 'pending')),
@@ -261,7 +272,8 @@ function outroPage(scene, inner, st) {
261
272
  if (outro.link) {
262
273
  return {
263
274
  heading: outro.title,
264
- text: [...said(outro.result, inner, st), ...leftLines(outro.todo, inner, st), ...outro.warnings.map((line) => center(st.soft(`! ${line}`), inner))],
275
+ // A warning is read whole: wrapped, where centered on one line it would be cut.
276
+ text: [...said(outro.result, inner, st), ...usedLines(outro, inner, st), ...leftLines(outro.todo, inner, st), ...outro.warnings.flatMap((line) => said(`! ${line}`, inner, st))],
265
277
  rule: divider(inner, st),
266
278
  action: pullAction(outro.link, scene.copied, inner, st),
267
279
  ways: scene.leaving ? undefined : [['Close', 'Enter']],
@@ -273,7 +285,7 @@ function outroPage(scene, inner, st) {
273
285
  const links = outro.links.flatMap((l) => linkLines(l.label, l.url, inner, st));
274
286
  return {
275
287
  heading: outro.title,
276
- text: said(outro.result, inner, st),
288
+ text: [...said(outro.result, inner, st), ...usedLines(outro, inner, st)],
277
289
  rule: divider(inner, st),
278
290
  action: [...block(notes, inner), ...(notes.length && links.length ? [''] : []), ...links, ...(outro.links.length === 1 ? [scene.copied ? center(st.soft('Copied to clipboard!'), inner) : copyWay(inner, st)] : [])],
279
291
  ways: scene.leaving ? undefined : [['Close', 'Enter']],
@@ -299,7 +311,7 @@ function pageOf(f, inner) {
299
311
  const under = scene.note ?? (scene.copied ? 'Copied to clipboard!' : '');
300
312
  const link = block(urlRows(scene.link, ruleWidth(inner), st.soft, st), inner);
301
313
  // In the action, which scrolls and keeps every line, so a short terminal still shows which checkout this approves.
302
- const checkout = scene.checkout && !scene.again ? [...balanced(scene.checkout.repository, Math.min(TEXT_WIDTH, inner)).map((line) => center(st.full(line), inner)), ...said(`in ${scene.checkout.folder}`, inner, st), ''] : [];
314
+ const checkout = scene.checkout && !scene.again ? [...balanced(scene.checkout.repository, Math.min(TEXT_WIDTH, inner)).map((line) => center(st.full(line), inner)), ...slashRows(`in ${scene.checkout.folder}`, Math.min(TEXT_WIDTH, inner)).map((line) => center(st.soft(line), inner)), ''] : [];
303
315
  return {
304
316
  heading: scene.again ? 'Sign in again' : START.heading,
305
317
  text: said(scene.again ? AGAIN[scene.again] : 'Sign in with GitHub in your browser, then approve this repository.', inner, st),
@@ -83,9 +83,4 @@ export class RunState {
83
83
  return { previous: this.current, current: `Still working… ${elapsed(silence)}` };
84
84
  return { previous: this.previous, current: this.current };
85
85
  }
86
- /** Completed tasks out of all but the skipped ones. */
87
- progress() {
88
- const counted = this.tasks.filter((t) => t.status !== 'skipped');
89
- return { completed: counted.filter((t) => t.status === 'completed').length, total: counted.length };
90
- }
91
86
  }
@@ -303,7 +303,7 @@ class Wizard {
303
303
  if (choices && question.picked.size)
304
304
  return done({ skipped: false, values: choices.filter((_, i) => question.picked.has(i)).map((o) => o.value) });
305
305
  if (choices)
306
- question.note = 'Pick at least one, or skip.';
306
+ question.note = q.skippable ? 'Pick at least one, or skip.' : 'Pick at least one.';
307
307
  else {
308
308
  const value = question.field.getValue().trim();
309
309
  if (value)
@@ -478,7 +478,7 @@ export function showRun(opts) {
478
478
  }
479
479
  /** The outro in plain lines, for the scrollback: what stays once the screen has gone. */
480
480
  function summary(o) {
481
- return ['', `${OUTCOME[o.outcome]} ${o.title}. ${o.result}`, ...o.links.map((l) => `${l.label}: ${l.url}`), ...o.todo.map((line) => `→ ${line}`), ...o.warnings.map((warning) => `! ${warning}`)];
481
+ return ['', `${OUTCOME[o.outcome]} ${o.title}. ${o.result}`, ...(o.used ?? []), ...o.links.map((l) => `${l.label}: ${l.url}`), ...o.todo.map((line) => `→ ${line}`), ...o.warnings.map((warning) => `! ${warning}`)];
482
482
  }
483
483
  /** Opens `url` in the browser and puts it on the clipboard, true once it is there. Neither can fail the screen, which shows the link either way. */
484
484
  async function handOver(url, hand) {
@@ -493,9 +493,9 @@ export async function showPullRequest(url, hand) {
493
493
  return;
494
494
  const copied = await handOver(url, hand);
495
495
  await new Promise((resolve) => {
496
+ // The screen stays until the end screen takes its place, so no empty screen shows between the two.
496
497
  const done = () => {
497
498
  clearTimeout(timer);
498
- screen.remove(view);
499
499
  resolve();
500
500
  };
501
501
  const timer = setTimeout(done, CLOSE_AFTER_MS).unref();
@@ -507,7 +507,7 @@ export async function showPullRequest(url, hand) {
507
507
  /** The last screen: what happened, what is left, any warnings and its pull request (handed over as it appears) or the links, until a key other than ↑↓ PgUp PgDn is pressed, or for 20 minutes. Its summary then stays in the scrollback. */
508
508
  export async function showOutro(outro, hand) {
509
509
  // What is left and why it stopped can be the model's words, which reach the scrollback after the clipboard holds the pull request.
510
- const o = { ...outro, result: plainText(outro.result), todo: outro.todo.map(plainText), warnings: outro.warnings.map(plainText) };
510
+ const o = { ...outro, result: plainText(outro.result), todo: outro.todo.map(plainText), warnings: outro.warnings.map(plainText), ...(outro.used ? { used: outro.used.map(plainText) } : {}) };
511
511
  const screen = host();
512
512
  if (screen) {
513
513
  const copied = o.link && hand ? await handOver(o.link, hand) : false;
@@ -182,6 +182,7 @@ export async function passVercelLogin(deps) {
182
182
  else {
183
183
  // The project holds the secret from here on, whatever the reads below say: the screen says to copy it, never to make another.
184
184
  held = made.made;
185
+ deps.made?.(made.made, project.name);
185
186
  // Read back by its label, again for a few seconds while the settings do not list it yet.
186
187
  let ours;
187
188
  for (const end = Date.now() + wait.totalMs;;) {
package/dist/vercel.js CHANGED
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { setTimeout as sleep } from 'node:timers/promises';
4
4
  import { folderOf, matchesBranch, TEST_DRIVE_BRANCH } from './host-previews.js';
5
- import { asLink, readLink } from './vercel-bypass.js';
5
+ import { asLink, isBypassSecret, readLink, SECRET_NOTE } from './vercel-bypass.js';
6
6
  /** `vercel whoami`: the CLI prints the username, or says it has no credentials and exits 1. */
7
7
  export async function vercelStatus(vercel) {
8
8
  try {
@@ -147,7 +147,9 @@ export async function readLinkedProject(vercel, link) {
147
147
  // The live app: the production deployment's first alias, the project's own domain, as Vercel lists them (its automatic aliases follow). Its own hashed address is one build's, which the next production deployment leaves behind: never the before.
148
148
  const live = target && !failed(target) ? (Array.isArray(target.alias) ? target.alias : []).map(hostname).find(Boolean) : undefined;
149
149
  const root = folderOf(project.rootDirectory);
150
- return { ...(root !== undefined ? { rootDirectory: root } : {}), ...(typeof project.name === 'string' && project.name ? { name: project.name } : {}), production: exists ? 'exists' : 'none', ...(typeof branch === 'string' && branch ? { productionBranch: branch } : {}), ...(live ? { productionUrl: `https://${live}` } : {}), protection: typeof protection === 'string' ? protection : null, previewsProtected, deploymentHosts: new Set(hosts), ...(repository ? { repository } : {}), ignoredBuildStep: typeof ignore === 'string' && ignore.trim().length > 0, githubDeployments: record(project.gitProviderOptions)?.createDeployments !== 'disabled' };
150
+ // As impeccable_vercel_bypass picks its secret: one for automation, labelled for Impeccable.
151
+ const labelled = Object.entries(record(project.protectionBypass) ?? {}).some(([value, meta]) => isBypassSecret(value) && record(meta)?.scope === 'automation-bypass' && record(meta)?.note === SECRET_NOTE);
152
+ return { ...(labelled ? { bypassLabelled: true } : {}), ...(root !== undefined ? { rootDirectory: root } : {}), ...(typeof project.name === 'string' && project.name ? { name: project.name } : {}), production: exists ? 'exists' : 'none', ...(typeof branch === 'string' && branch ? { productionBranch: branch } : {}), ...(live ? { productionUrl: `https://${live}` } : {}), protection: typeof protection === 'string' ? protection : null, previewsProtected, deploymentHosts: new Set(hosts), ...(repository ? { repository } : {}), ignoredBuildStep: typeof ignore === 'string' && ignore.trim().length > 0, githubDeployments: record(project.gitProviderOptions)?.createDeployments !== 'disabled' };
151
153
  }
152
154
  /**
153
155
  * The linked project as this repository's: read now, and connected to the repository on Vercel, which its Git link
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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,
@@ -1,16 +0,0 @@
1
- ### When this applies
2
-
3
- The pages need no server from this repository: plain HTML, or a build that writes a complete static site. Pages that need SSR, API routes or a backend here follow server-previews; an external API they call does not change that. A build that writes only JavaScript, CSS or types is a library, not a site.
4
-
5
- ### Steps
6
-
7
- 1. **Find the output**: the folder the build writes the pages to (framework config, scripts), or, with no build, the folder that holds the real `index.html`. Done when you can name that folder and the build command, or that there is none.
8
- 2. **Prove it** with impeccable_verify_preview in static mode. Done when it answers `verified: true` with the folder's `index.html` in `files`; a failure names its `stage`, which the report explains, and pull-requests' "No preview, no test drive" says how the run goes on without a proof. When it answers that this machine has no sandbox for the check (Linux, or `--no-sandbox`), the check is skipped: save the build the scripts name in step 3, and the preview job on the pull request proves it; put that under "What is not verified".
9
- 3. **Save it** with impeccable_write to `preview`: `intendedMethod: "static-upload"`, `outputDir` and the commands you proved, clearing a saved command you did not prove; when the check was skipped on this machine, the build the scripts name is what you save, and the preview job proves it. Done when the answer's `keys` show each key stored or unchanged.
10
-
11
- ### Reference
12
-
13
- - **No build**: `steps` is empty and the output is the page's folder, `.` for the checkout's root. Invent no manifest, build, runtime or server for plain HTML.
14
- - **A base path**: `<base>/index.html` must exist under the output folder; pass `basePath` to the check, and save it.
15
- - **A single-page app**: the write takes no `spa` key, and the entry check proves no deep link; report a route the preview cannot serve.
16
- - A verified result proves the entry files exist, nothing more: rendering, assets, deep links and external APIs go under "What is not verified".