@impeccable/reviews 0.1.3 → 0.1.5
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/README.md +8 -4
- package/dist/agent.js +2 -2
- package/dist/bin.js +0 -0
- package/dist/fence.js +4 -0
- package/dist/host-questions.js +47 -1
- package/dist/hosted.js +6 -3
- package/dist/log.js +6 -1
- package/dist/login.js +49 -21
- package/dist/main.js +15 -1
- package/dist/netlify-password.js +65 -7
- package/dist/playbook/cases/netlify-previews.md +3 -2
- package/dist/playbook/cases/netlify-setup.md +3 -2
- package/dist/playbook/cases/preview-protection.md +7 -5
- package/dist/playbook/cases/pull-requests.md +2 -2
- package/dist/playbook/cases/server-previews.md +2 -2
- package/dist/playbook/cases/vercel-previews.md +1 -0
- package/dist/playbook/cases/vercel-setup.md +6 -5
- package/dist/playbook/github-previews.js +101 -0
- package/dist/playbook/netlify-pattern.js +24 -0
- package/dist/playbook/netlify-previews.js +144 -0
- package/dist/playbook/probe-preview.js +67 -0
- package/dist/playbook/protection-secrets.js +15 -0
- package/dist/playbook/public-preview-request.js +96 -0
- package/dist/playbook/publication-wait.js +46 -0
- package/dist/playbook/vercel-protection.js +121 -0
- package/dist/playbook.js +5 -1
- package/dist/push-access.js +77 -0
- package/dist/scan.js +82 -14
- package/dist/tools.js +160 -57
- package/dist/transcript.js +23 -12
- package/dist/ui/bridge.js +1 -1
- package/dist/ui/look.js +12 -3
- package/dist/ui/screens.js +18 -23
- package/dist/vercel.js +27 -3
- package/package.json +3 -3
package/dist/ui/look.js
CHANGED
|
@@ -10,6 +10,8 @@ import { fitsTask, taskInProgress, workStarted } from '../stages.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export const PRODUCT = 'Impeccable for PRs';
|
|
12
12
|
export const FEEDBACK = 'support@renaissance-geek.ai';
|
|
13
|
+
/** How long the outro waits for a key before it closes itself (screens.ts), which its fine print says. */
|
|
14
|
+
export const CLOSES_AFTER_MINUTES = 20;
|
|
13
15
|
/** The box fills the terminal up to this size, then sits in the middle of it. */
|
|
14
16
|
const BOX_WIDTH = 100;
|
|
15
17
|
const BOX_HEIGHT = 32;
|
|
@@ -189,6 +191,8 @@ function questionPage(qv, inner, st) {
|
|
|
189
191
|
const field = Math.min(FIELD_WIDTH, inner);
|
|
190
192
|
const edged = q.hint && visibleWidth(q.hint) <= field - 6 ? q.hint : '';
|
|
191
193
|
const label = q.hint && !edged ? wrapTextWithAnsi(q.hint, field - 2).map((line) => ` ${st.soft(line)}`) : [];
|
|
194
|
+
// What never goes into the field stays under it, whatever is typed.
|
|
195
|
+
const caution = q.caution && !choices ? balanced(q.caution, field).map((line) => center(st.soft(line), inner)) : [];
|
|
192
196
|
const lines = (choices ?? []).map((option, i) => {
|
|
193
197
|
const here = i === qv.cursor;
|
|
194
198
|
const mark = q.kind === 'single' ? (here ? '●' : '○') : qv.picked.has(i) ? '[x]' : '[ ]';
|
|
@@ -199,7 +203,7 @@ function questionPage(qv, inner, st) {
|
|
|
199
203
|
heading: q.title ?? 'A question for you',
|
|
200
204
|
text: said(q.prompt, inner, st),
|
|
201
205
|
rule: divider(inner, st),
|
|
202
|
-
action: choices ? [...hint, ...block(lines, inner, widest), ...after] : [...block([...label, ...fieldBox(edged, qv.field(field - 2), field, true, st)], inner, field), ...after],
|
|
206
|
+
action: choices ? [...hint, ...block(lines, inner, widest), ...after] : [...block([...label, ...fieldBox(edged, qv.field(field - 2), field, true, st)], inner, field), ...caution, ...after],
|
|
203
207
|
at: choices ? hint.length + qv.cursor : label.length + 1,
|
|
204
208
|
asks: true,
|
|
205
209
|
ways: [['Continue', 'Enter'], ...(q.skippable ? [['Skip', 'Esc']] : [])],
|
|
@@ -261,6 +265,7 @@ function outroPage(scene, inner, st) {
|
|
|
261
265
|
rule: divider(inner, st),
|
|
262
266
|
action: pullAction(outro.link, scene.copied, inner, st),
|
|
263
267
|
ways: scene.leaving ? undefined : [['Close', 'Enter']],
|
|
268
|
+
foot: closesItself(scene, inner, st),
|
|
264
269
|
};
|
|
265
270
|
}
|
|
266
271
|
const width = Math.min(TEXT_WIDTH, inner);
|
|
@@ -272,9 +277,11 @@ function outroPage(scene, inner, st) {
|
|
|
272
277
|
rule: divider(inner, st),
|
|
273
278
|
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)] : [])],
|
|
274
279
|
ways: scene.leaving ? undefined : [['Close', 'Enter']],
|
|
275
|
-
foot: outro.outcome === 'stopped' ? [center(st.faint(`Questions? Write to ${FEEDBACK}`), inner)] :
|
|
280
|
+
foot: outro.outcome === 'stopped' ? [center(st.faint(`Questions? Write to ${FEEDBACK}`), inner)] : closesItself(scene, inner, st),
|
|
276
281
|
};
|
|
277
282
|
}
|
|
283
|
+
/** The outro's fine print that it closes on its own, until it is closing; a stopped run's foot keeps the way to write to us instead, one line either way. */
|
|
284
|
+
const closesItself = (scene, inner, st) => (scene.leaving ? [] : [center(st.faint(`Closes on its own after ${CLOSES_AFTER_MINUTES} minutes.`), inner)]);
|
|
278
285
|
function pageOf(f, inner) {
|
|
279
286
|
const st = f.style;
|
|
280
287
|
if (f.question)
|
|
@@ -291,11 +298,13 @@ function pageOf(f, inner) {
|
|
|
291
298
|
// P still takes a pasted code on a remote shell; --help says so, the screen does not.
|
|
292
299
|
const under = scene.note ?? (scene.copied ? 'Copied to clipboard!' : '');
|
|
293
300
|
const link = block(urlRows(scene.link, ruleWidth(inner), st.soft, st), inner);
|
|
301
|
+
// 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), ''] : [];
|
|
294
303
|
return {
|
|
295
304
|
heading: scene.again ? 'Sign in again' : START.heading,
|
|
296
305
|
text: said(scene.again ? AGAIN[scene.again] : 'Sign in with GitHub in your browser, then approve this repository.', inner, st),
|
|
297
306
|
rule: divider(inner, st),
|
|
298
|
-
action: [...link, ...(under ? ['', center(st.full(under), inner)] : []), ...(scene.copied === false ? ['', copyWay(inner, st)] : [])],
|
|
307
|
+
action: [...checkout, ...link, ...(under ? ['', center(st.full(under), inner)] : []), ...(scene.copied === false ? ['', copyWay(inner, st)] : [])],
|
|
299
308
|
};
|
|
300
309
|
}
|
|
301
310
|
case 'run':
|
package/dist/ui/screens.js
CHANGED
|
@@ -5,7 +5,7 @@ import { copyText } from '../login.js';
|
|
|
5
5
|
import { workStarted } from '../stages.js';
|
|
6
6
|
import { mountUi } from './bridge.js';
|
|
7
7
|
import { createFrameWriter, frameState } from './frames.js';
|
|
8
|
-
import { drawFrame, OUTCOME, PRODUCT, palette } from './look.js';
|
|
8
|
+
import { CLOSES_AFTER_MINUTES, drawFrame, OUTCOME, PRODUCT, palette } from './look.js';
|
|
9
9
|
import { RunState } from './run-state.js';
|
|
10
10
|
/** What the command does before a screen ends it at once (Ctrl+C before a run, or a signal): report the stop (main.ts), briefly. */
|
|
11
11
|
let beforeExit = async () => { };
|
|
@@ -18,6 +18,8 @@ const TICK_MS = 80;
|
|
|
18
18
|
const FIRST_STATUS = 'Reading the repository';
|
|
19
19
|
/** A first Esc on the run screen only asks for a second one within this long. */
|
|
20
20
|
const STOP_WINDOW_MS = 3000;
|
|
21
|
+
/** The test drive and the outro close themselves after this long without a key, as Enter would: someone who walked away does not come back to a wizard still open. */
|
|
22
|
+
const CLOSE_AFTER_MS = CLOSES_AFTER_MINUTES * 60_000;
|
|
21
23
|
/** This package's version, for the corner of the frame. */
|
|
22
24
|
const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
23
25
|
const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -337,7 +339,7 @@ class Wizard {
|
|
|
337
339
|
if (view.kind === 'intro')
|
|
338
340
|
return { kind: 'intro', note: view.note };
|
|
339
341
|
if (view.kind === 'signin')
|
|
340
|
-
return { kind: 'signin', link: view.link, copied: view.copied, again: view.again, note: view.pasting ? undefined : view.note };
|
|
342
|
+
return { kind: 'signin', link: view.link, copied: view.copied, again: view.again, note: view.pasting ? undefined : view.note, checkout: view.opts.checkout };
|
|
341
343
|
if (view.kind === 'pull')
|
|
342
344
|
return { kind: 'pull', url: view.url, copied: view.copied };
|
|
343
345
|
if (view.kind === 'outro')
|
|
@@ -484,28 +486,25 @@ async function handOver(url, hand) {
|
|
|
484
486
|
const copied = hand.copy(url).catch(() => false);
|
|
485
487
|
return (await Promise.race([copied, new Promise((r) => setTimeout(() => r(false), 1000).unref())])) === true;
|
|
486
488
|
}
|
|
487
|
-
/** 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. Without a terminal there is no such screen: the summary has it. */
|
|
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. */
|
|
488
490
|
export async function showPullRequest(url, hand) {
|
|
489
491
|
const screen = host();
|
|
490
492
|
if (!screen)
|
|
491
493
|
return;
|
|
492
494
|
const copied = await handOver(url, hand);
|
|
493
495
|
await new Promise((resolve) => {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
copy: () => hand.copy(url),
|
|
499
|
-
done: () => {
|
|
500
|
-
screen.remove(view);
|
|
501
|
-
resolve();
|
|
502
|
-
},
|
|
496
|
+
const done = () => {
|
|
497
|
+
clearTimeout(timer);
|
|
498
|
+
screen.remove(view);
|
|
499
|
+
resolve();
|
|
503
500
|
};
|
|
501
|
+
const timer = setTimeout(done, CLOSE_AFTER_MS).unref();
|
|
502
|
+
const view = { kind: 'pull', url, copied, copy: () => hand.copy(url), done };
|
|
504
503
|
screen.views = [];
|
|
505
504
|
screen.push(view);
|
|
506
505
|
});
|
|
507
506
|
}
|
|
508
|
-
/** 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. Its summary then stays in the scrollback. */
|
|
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. */
|
|
509
508
|
export async function showOutro(o, hand) {
|
|
510
509
|
const screen = host();
|
|
511
510
|
if (screen) {
|
|
@@ -513,17 +512,13 @@ export async function showOutro(o, hand) {
|
|
|
513
512
|
// The link C copies: the pull request, or the one link a screen without it lists.
|
|
514
513
|
const link = o.link ?? (o.links.length === 1 ? o.links[0].url : undefined);
|
|
515
514
|
await new Promise((resolve) => {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
copied,
|
|
521
|
-
copy: link ? () => (hand?.copy ?? copyText)(link) : undefined,
|
|
522
|
-
done: () => {
|
|
523
|
-
view.leaving = true;
|
|
524
|
-
void screen.close().then(resolve);
|
|
525
|
-
},
|
|
515
|
+
const done = () => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
view.leaving = true;
|
|
518
|
+
void screen.close().then(resolve);
|
|
526
519
|
};
|
|
520
|
+
const timer = setTimeout(done, CLOSE_AFTER_MS).unref();
|
|
521
|
+
const view = { kind: 'outro', outro: o, leaving: false, copied, copy: link ? () => (hand?.copy ?? copyText)(link) : undefined, done };
|
|
527
522
|
screen.views = [];
|
|
528
523
|
screen.push(view);
|
|
529
524
|
});
|
package/dist/vercel.js
CHANGED
|
@@ -110,7 +110,7 @@ export async function readVercelProject(vercel, link) {
|
|
|
110
110
|
const branch = record(project.link)?.productionBranch;
|
|
111
111
|
const protection = scope(project);
|
|
112
112
|
const hosts = [target, ...latest].flatMap((d) => (typeof d?.url === 'string' && d.url ? [d.url.replace(/^https?:\/\//, '').toLowerCase()] : []));
|
|
113
|
-
return { production: exists ? 'exists' : 'none', ...(typeof branch === 'string' && branch ? { productionBranch: branch } : {}), protection: typeof protection === 'string' ? protection : null, deploymentHosts: new Set(hosts) };
|
|
113
|
+
return { ...(typeof project.name === 'string' && project.name ? { name: project.name } : {}), production: exists ? 'exists' : 'none', ...(typeof branch === 'string' && branch ? { productionBranch: branch } : {}), protection: typeof protection === 'string' ? protection : null, deploymentHosts: new Set(hosts) };
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* The linked project before its first push: read through `vercel api`, and, with no production deployment, locked
|
|
@@ -118,15 +118,17 @@ export async function readVercelProject(vercel, link) {
|
|
|
118
118
|
* branch it builds and Standard Protection serves production domains to anyone. A project that deploys production
|
|
119
119
|
* already (live or building), or is locked already, is left as it is.
|
|
120
120
|
*/
|
|
121
|
-
export async function prepareFirstDeployment(vercel, link) {
|
|
121
|
+
export async function prepareFirstDeployment(vercel, link, lock = true) {
|
|
122
122
|
const project = await readVercelProject(vercel, link);
|
|
123
123
|
if (!project)
|
|
124
124
|
return { production: 'unknown', locked: false, reason: 'The Vercel CLI could not read the project (vercel api): it is signed out, or this CLI has no api command.' };
|
|
125
|
-
const branch = project.productionBranch ? { productionBranch: project.productionBranch } : {};
|
|
125
|
+
const branch = { ...(project.productionBranch ? { productionBranch: project.productionBranch } : {}), ...(project.name ? { project: project.name } : {}) };
|
|
126
126
|
if (project.production === 'exists')
|
|
127
127
|
return { production: 'exists', ...branch };
|
|
128
128
|
if (project.protection === 'all')
|
|
129
129
|
return { production: 'none', locked: true, ...branch };
|
|
130
|
+
if (!lock)
|
|
131
|
+
return { production: 'none', locked: false, ...branch, reason: "Vercel's login is not in front of the project's addresses yet." };
|
|
130
132
|
try {
|
|
131
133
|
if (scope(await printedProject(vercel, lockProjectApi(link))) === 'all')
|
|
132
134
|
return { production: 'none', locked: true, ...branch };
|
|
@@ -136,6 +138,28 @@ export async function prepareFirstDeployment(vercel, link) {
|
|
|
136
138
|
}
|
|
137
139
|
return { production: 'none', locked: false, ...branch, reason: "Vercel did not put its login in front of the project's addresses: that takes the Member role or Project Administrator." };
|
|
138
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* A page of the project in Vercel's dashboard: by its team and name when the run knows both, else the dashboard's link
|
|
143
|
+
* that has the person pick them.
|
|
144
|
+
*/
|
|
145
|
+
export function vercelProjectPage(page, team, project) {
|
|
146
|
+
return team && project ? `https://vercel.com/${encodeURIComponent(team)}/${encodeURIComponent(project)}/${page}` : `https://vercel.com/d?to=${encodeURIComponent(`/[team]/[project]/${page}`)}`;
|
|
147
|
+
}
|
|
148
|
+
/** Reads the project again every `wait.everyMs` until Vercel's login stands in front of every address of it, or it deploys production, or `wait.totalMs` has passed (a read that failed counts as neither): the person is locking it meanwhile. */
|
|
149
|
+
export async function waitForVercelLock(vercel, link, deps) {
|
|
150
|
+
for (const end = Date.now() + deps.wait.totalMs;;) {
|
|
151
|
+
const first = await prepareFirstDeployment(vercel, link, false);
|
|
152
|
+
// A read that failed says nothing yet, so the wait goes on.
|
|
153
|
+
if (first.production === 'exists' || first.locked || Date.now() >= end || deps.signal?.aborted)
|
|
154
|
+
return first;
|
|
155
|
+
try {
|
|
156
|
+
await (deps.sleep ?? ((ms) => sleep(ms, undefined, { signal: deps.signal })))(deps.wait.everyMs);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return first;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
139
163
|
/**
|
|
140
164
|
* The hosts of the linked project's deployments while Vercel's login stands in front of every address of it, read
|
|
141
165
|
* now: a Production deployment GitHub lists for a pushed branch is its preview only when it is one of these. Empty
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impeccable/reviews",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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,
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@anthropic-ai/sandbox-runtime": "0.0.78",
|
|
28
|
-
"@earendil-works/pi-coding-agent": "
|
|
29
|
-
"@earendil-works/pi-tui": "
|
|
28
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
29
|
+
"@earendil-works/pi-tui": "0.87.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.20.4",
|