@impeccable/reviews 0.1.20 → 0.1.22
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/cloudflare-access.js +4 -0
- package/dist/cloudflare.js +15 -18
- package/dist/fence.js +58 -26
- package/dist/host-questions.js +41 -17
- package/dist/main.js +26 -15
- package/dist/netlify-password.js +114 -37
- package/dist/playbook/cases/monorepo.md +3 -3
- package/dist/playbook/cases/netlify-setup.md +2 -2
- package/dist/playbook/cases/preview-protection.md +5 -4
- package/dist/playbook/cases/pull-requests.md +1 -1
- package/dist/playbook/cases/vercel-setup.md +2 -2
- package/dist/playbook.js +1 -1
- package/dist/preflight.js +1 -1
- package/dist/sandbox.js +4 -4
- package/dist/tools.js +294 -91
- package/dist/ui/bridge.js +5 -3
- package/dist/ui/look.js +36 -10
- package/dist/ui/run-state.js +7 -1
- package/dist/ui/screens.js +6 -6
- package/package.json +1 -1
package/dist/ui/bridge.js
CHANGED
|
@@ -121,7 +121,9 @@ async function askOnStdin(q) {
|
|
|
121
121
|
const out = process.stdout;
|
|
122
122
|
if (q.kind === 'confirm') {
|
|
123
123
|
out.write(`\n${q.title ? `${q.title}\n` : ''}${q.hint ? `${q.hint}\n` : ''}${q.prompt}\n`);
|
|
124
|
-
|
|
124
|
+
// Lowercased after "to", but for the I of "I'll install it myself".
|
|
125
|
+
const lower = (label) => label.toLowerCase().replace(/^i(?=['\s])/, 'I');
|
|
126
|
+
const how = `Type y to ${lower(q.confirmLabel ?? 'continue')}, or Enter to ${lower(q.skipLabel ?? 'skip')}:`;
|
|
125
127
|
out.write(`${how} `);
|
|
126
128
|
for (let line = await nextLine(); line !== null; line = await nextLine()) {
|
|
127
129
|
if (/^y(es)?$/i.test(line.trim()))
|
|
@@ -130,7 +132,7 @@ async function askOnStdin(q) {
|
|
|
130
132
|
return { skipped: true };
|
|
131
133
|
out.write(`That is not an answer to this one. ${how} `);
|
|
132
134
|
}
|
|
133
|
-
return { skipped: true };
|
|
135
|
+
return { skipped: true, closed: true };
|
|
134
136
|
}
|
|
135
137
|
if (q.kind === 'account') {
|
|
136
138
|
// An empty username keeps the one the field would hold; an empty password skips the account.
|
|
@@ -158,5 +160,5 @@ async function askOnStdin(q) {
|
|
|
158
160
|
out.write(`That is not an answer to this one. ${how} `);
|
|
159
161
|
}
|
|
160
162
|
// Nobody is there to answer.
|
|
161
|
-
return { skipped: true };
|
|
163
|
+
return { skipped: true, closed: true };
|
|
162
164
|
}
|
package/dist/ui/look.js
CHANGED
|
@@ -63,13 +63,30 @@ function block(lines, width, size = Math.max(0, ...lines.map((line) => visibleWi
|
|
|
63
63
|
const left = ' '.repeat(Math.max(0, Math.floor((width - Math.min(size, width)) / 2)));
|
|
64
64
|
return lines.map((line) => left + truncateToWidth(line, width - left.length, '…'));
|
|
65
65
|
}
|
|
66
|
+
/** A word wider than `max` columns (a host name, an address) cut after a dot or a slash in each row's last two thirds, so it breaks where a reader expects (#390); a word with none stays whole. */
|
|
67
|
+
function dotRows(word, max) {
|
|
68
|
+
const rows = [];
|
|
69
|
+
let rest = word;
|
|
70
|
+
while (visibleWidth(rest) > max) {
|
|
71
|
+
const head = rest.slice(0, max);
|
|
72
|
+
const cut = Math.max(head.lastIndexOf('.'), head.lastIndexOf('/')) + 1;
|
|
73
|
+
if (cut < max / 3)
|
|
74
|
+
break;
|
|
75
|
+
rows.push(rest.slice(0, cut));
|
|
76
|
+
rest = rest.slice(cut);
|
|
77
|
+
}
|
|
78
|
+
return [...rows, rest];
|
|
79
|
+
}
|
|
66
80
|
/** A paragraph of plain text wrapped to at most `max` columns: a sentence a line when each fits, else lines as even as the words allow. */
|
|
67
|
-
function balanced(
|
|
81
|
+
function balanced(given, max) {
|
|
82
|
+
const text = given.split(' ').map((word) => dotRows(word, max).join('\n')).join(' ');
|
|
68
83
|
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
69
84
|
if (sentences.length > 1 && sentences.every((line) => visibleWidth(line) <= max))
|
|
70
85
|
return sentences;
|
|
71
86
|
const lines = wrapTextWithAnsi(text, Math.max(10, max));
|
|
72
|
-
|
|
87
|
+
// Never narrower than its longest word, which would then break mid-word.
|
|
88
|
+
const longest = Math.max(0, ...text.split(/\s+/).map((word) => visibleWidth(word)));
|
|
89
|
+
for (let w = Math.max(longest, Math.ceil(visibleWidth(text) / lines.length)); lines.length > 1 && w < max; w++) {
|
|
73
90
|
const even = wrapTextWithAnsi(text, w);
|
|
74
91
|
if (even.length === lines.length)
|
|
75
92
|
return even;
|
|
@@ -240,24 +257,33 @@ function runPage(scene, f, inner, st) {
|
|
|
240
257
|
if (!workStarted(scene.tasks))
|
|
241
258
|
return { heading: 'Understanding your app', rule: loader(inner, f.motion?.frame ?? 0, st), action: status, ...around };
|
|
242
259
|
const running = scene.tasks.findIndex((t) => t.status === 'in_progress');
|
|
243
|
-
const
|
|
260
|
+
const rows = scene.tasks.map((t, i) => {
|
|
261
|
+
// A task back to pending after it started (Preview method while Test drive runs) keeps a started mark, never ○ (#390).
|
|
244
262
|
const mark = t.status === 'completed' ? '✓'
|
|
245
263
|
: t.status === 'in_progress' ? (i === running && f.motion?.at === 'task' ? glyph : '•')
|
|
246
264
|
: t.status === 'failed' ? '✗'
|
|
247
265
|
: t.status === 'skipped' ? '–'
|
|
248
|
-
: '
|
|
266
|
+
: t.started ? '•'
|
|
267
|
+
: '○';
|
|
249
268
|
const ink = t.status === 'in_progress' || t.status === 'failed' ? st.full : t.status === 'skipped' ? st.faint : st.soft;
|
|
250
269
|
const row = (detail) => `${mark} ${t.title}${detail ? ` · ${detail}` : ''}`;
|
|
251
|
-
|
|
252
|
-
|
|
270
|
+
return { ink, row, line: t.status === 'skipped' || t.status === 'failed' ? row(t.status) : row() };
|
|
271
|
+
});
|
|
272
|
+
// The list's left edge is set by the rows without the wait's words, so it stays put while a wait comes and goes (#390).
|
|
273
|
+
const widest = Math.max(...rows.map(({ line }) => visibleWidth(line)));
|
|
274
|
+
const room = inner - Math.max(0, Math.floor((inner - Math.min(widest, inner)) / 2));
|
|
275
|
+
const lines = rows.map(({ ink, row, line }, i) => {
|
|
253
276
|
if (i !== running || !scene.wait)
|
|
254
|
-
return ink(
|
|
277
|
+
return ink(line);
|
|
255
278
|
// The task in progress says what the run waits for and the time so far, never how long it may wait, so a screen
|
|
256
279
|
// waiting on a host's build is never still; the time alone where the words would push it off the row.
|
|
257
280
|
const waiting = row(`waiting for ${scene.wait.host ? `${scene.wait.host}'s preview` : 'the preview'} · ${scene.wait.timer}`);
|
|
258
|
-
|
|
281
|
+
if (visibleWidth(waiting) <= room)
|
|
282
|
+
return ink(waiting);
|
|
283
|
+
// Where even the time alone would pass the box from that edge, the title gives way, never the time.
|
|
284
|
+
const tail = ` · ${scene.wait.timer}`;
|
|
285
|
+
return ink(visibleWidth(row(scene.wait.timer)) <= room ? row(scene.wait.timer) : truncateToWidth(row(), Math.max(1, room - visibleWidth(tail)), '…') + tail);
|
|
259
286
|
});
|
|
260
|
-
const widest = Math.max(...lines.map((line) => visibleWidth(line)));
|
|
261
287
|
// No count over the list: the list shows each task's state itself.
|
|
262
288
|
return {
|
|
263
289
|
heading: 'Setting up',
|
|
@@ -323,7 +349,7 @@ function pageOf(f, inner) {
|
|
|
323
349
|
return runPage(scene, f, inner, st);
|
|
324
350
|
case 'pull':
|
|
325
351
|
return {
|
|
326
|
-
heading: 'Your first review is on its way',
|
|
352
|
+
heading: scene.app ? `${scene.app}'s first review is on its way` : 'Your first review is on its way',
|
|
327
353
|
text: said(SEE_THE_REVIEW, inner, st),
|
|
328
354
|
rule: divider(inner, st),
|
|
329
355
|
action: pullAction(scene.url, scene.copied, inner, st),
|
package/dist/ui/run-state.js
CHANGED
|
@@ -26,6 +26,8 @@ export class RunState {
|
|
|
26
26
|
current;
|
|
27
27
|
/** A bounded wait under way: the current line carries its timer, counting up. */
|
|
28
28
|
waiting;
|
|
29
|
+
/** The tasks that have been in progress, done or failed in this run. */
|
|
30
|
+
started = new Set();
|
|
29
31
|
/** When something last changed on screen: a status, the task list, an answered question. */
|
|
30
32
|
changedAt;
|
|
31
33
|
constructor(now = Date.now, first) {
|
|
@@ -59,7 +61,11 @@ export class RunState {
|
|
|
59
61
|
return this.waiting && { host: this.waiting.host, timer: timer(live ? this.now() - this.waiting.since : 0) };
|
|
60
62
|
}
|
|
61
63
|
setTasks(tasks) {
|
|
62
|
-
|
|
64
|
+
for (const t of tasks)
|
|
65
|
+
if (t.status === 'in_progress' || t.status === 'completed' || t.status === 'failed')
|
|
66
|
+
this.started.add(t.id);
|
|
67
|
+
// A task that has started never reads as not started again (#390).
|
|
68
|
+
this.tasks = tasks.map((t) => ({ ...t, ...(t.status === 'pending' && this.started.has(t.id) ? { started: true } : {}) }));
|
|
63
69
|
this.touch();
|
|
64
70
|
}
|
|
65
71
|
/** Something happened that the person saw, so the silence starts again. */
|
package/dist/ui/screens.js
CHANGED
|
@@ -117,7 +117,7 @@ class Wizard {
|
|
|
117
117
|
process.off('SIGHUP', this.onSignal);
|
|
118
118
|
mountUi(null);
|
|
119
119
|
for (const question of this.questions.splice(0))
|
|
120
|
-
question.resolve({ skipped: true });
|
|
120
|
+
question.resolve({ skipped: true, closed: true });
|
|
121
121
|
// Leaves the alternate screen without copying the last frame out of it: the scrollback gets the outro's summary only.
|
|
122
122
|
this.tui.stop({ preserveScreen: true });
|
|
123
123
|
this.restoreConsole();
|
|
@@ -240,7 +240,7 @@ class Wizard {
|
|
|
240
240
|
}
|
|
241
241
|
this.stopAsked = 0;
|
|
242
242
|
for (const question of this.questions.splice(0))
|
|
243
|
-
question.resolve({ skipped: true });
|
|
243
|
+
question.resolve({ skipped: true, closed: true });
|
|
244
244
|
this.state.status('Stopping the setup');
|
|
245
245
|
this.redraw();
|
|
246
246
|
run.cancel();
|
|
@@ -342,7 +342,7 @@ class Wizard {
|
|
|
342
342
|
if (view.kind === 'signin')
|
|
343
343
|
return { kind: 'signin', link: view.link, copied: view.copied, again: view.again, note: view.pasting ? undefined : view.note, checkout: view.opts.checkout };
|
|
344
344
|
if (view.kind === 'pull')
|
|
345
|
-
return { kind: 'pull', url: view.url, copied: view.copied };
|
|
345
|
+
return { kind: 'pull', url: view.url, ...(view.app ? { app: view.app } : {}), copied: view.copied };
|
|
346
346
|
if (view.kind === 'outro')
|
|
347
347
|
return { kind: 'outro', outro: view.outro, leaving: view.leaving, copied: view.copied };
|
|
348
348
|
return { kind: 'run', tasks: this.state.tasks, band: this.state.band(!this.questions.length, live), wait: this.state.timer(live) };
|
|
@@ -486,8 +486,8 @@ async function handOver(url, hand) {
|
|
|
486
486
|
const copied = hand.copy(url).catch(() => false);
|
|
487
487
|
return (await Promise.race([copied, new Promise((r) => setTimeout(() => r(false), 1000).unref())])) === true;
|
|
488
488
|
}
|
|
489
|
-
/** The test drive on a screen of its own once the run has ended, before the last screen shows the setup pull request, handed over as it appears, until a key other than ↑↓ PgUp PgDn is pressed, or for 20 minutes. Without a terminal there is no such screen: the summary has it. */
|
|
490
|
-
export async function showPullRequest(url, hand) {
|
|
489
|
+
/** The test drive on a screen of its own once the run has ended, before the last screen shows the setup pull request, handed over as it appears, until a key other than ↑↓ PgUp PgDn is pressed, or for 20 minutes; on a repository with several apps, each app's in turn, named (impeccable-reviews#397). Without a terminal there is no such screen: the summary has it. */
|
|
490
|
+
export async function showPullRequest(url, hand, app) {
|
|
491
491
|
const screen = host();
|
|
492
492
|
if (!screen)
|
|
493
493
|
return;
|
|
@@ -499,7 +499,7 @@ export async function showPullRequest(url, hand) {
|
|
|
499
499
|
resolve();
|
|
500
500
|
};
|
|
501
501
|
const timer = setTimeout(done, CLOSE_AFTER_MS).unref();
|
|
502
|
-
const view = { kind: 'pull', url, copied, copy: () => hand.copy(url), done };
|
|
502
|
+
const view = { kind: 'pull', url, ...(app ? { app } : {}), copied, copy: () => hand.copy(url), done };
|
|
503
503
|
screen.views = [];
|
|
504
504
|
screen.push(view);
|
|
505
505
|
});
|