@impeccable/reviews 0.1.0 → 0.1.2
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/LICENSE +14 -0
- package/README.md +29 -8
- package/dist/agent.js +2 -2
- package/dist/bin.js +38 -0
- package/dist/cloudflare-access.js +321 -0
- package/dist/cloudflare.js +291 -0
- package/dist/fence.js +309 -46
- package/dist/focus.js +49 -21
- package/dist/host-questions.js +47 -1
- package/dist/hosted.js +95 -16
- package/dist/log.js +6 -1
- package/dist/login.js +9 -7
- package/dist/main.js +86 -27
- package/dist/netlify-password.js +89 -0
- package/dist/netlify.js +4 -1
- package/dist/playbook/cases/cloudflare-previews.md +17 -0
- package/dist/playbook/cases/monorepo.md +22 -0
- package/dist/playbook/cases/netlify-previews.md +1 -1
- package/dist/playbook/cases/netlify-setup.md +2 -2
- package/dist/playbook/cases/preview-protection.md +13 -11
- package/dist/playbook/cases/vercel-setup.md +7 -5
- package/dist/playbook.js +4 -3
- package/dist/preflight.js +56 -12
- package/dist/report.js +68 -0
- package/dist/sandbox.js +280 -0
- package/dist/scan.js +111 -0
- package/dist/tools.js +337 -63
- package/dist/ui/bridge.js +6 -3
- package/dist/ui/look.js +21 -7
- package/dist/ui/run-state.js +9 -9
- package/dist/ui/screens.js +31 -25
- package/dist/vercel.js +77 -0
- package/package.json +3 -2
package/dist/ui/bridge.js
CHANGED
|
@@ -120,17 +120,20 @@ async function askOnStdin(q) {
|
|
|
120
120
|
}
|
|
121
121
|
if (q.kind === 'account') {
|
|
122
122
|
// An empty username keeps the one the field would hold; an empty password skips the account.
|
|
123
|
-
|
|
123
|
+
const [first, second] = q.fields ?? ['Username or email', 'Password'];
|
|
124
|
+
out.write(`\n${q.title ? `${q.title}. ` : ''}${q.prompt}\n${q.hint ? `${q.hint}\n` : ''}${first}${q.prefill ? ` (Enter for ${q.prefill})` : ''}: `);
|
|
124
125
|
const username = (await nextLine())?.trim() || q.prefill;
|
|
125
126
|
if (!username)
|
|
126
127
|
return { skipped: true };
|
|
127
|
-
out.write(
|
|
128
|
+
out.write(`${second}, or Enter to skip: `);
|
|
128
129
|
const password = await (process.stdin.isTTY ? hiddenLine : nextLine)();
|
|
129
130
|
return password ? { skipped: false, username, password } : { skipped: true };
|
|
130
131
|
}
|
|
131
132
|
const how = q.kind === 'text' ? (q.skippable ? '(Enter to skip)' : '') : q.kind === 'single' ? `Type a number${q.skippable ? ', or Enter to skip' : ''}:` : `Type the numbers, separated by commas${q.skippable ? ', or Enter to skip' : ''}:`;
|
|
132
133
|
out.write(`\n${q.prompt}\n${q.hint ? `${q.hint}\n` : ''}`);
|
|
133
|
-
|
|
134
|
+
// A multi choice that opens with picks shows them; on a pipe the numbers typed are the whole answer.
|
|
135
|
+
const marked = q.kind === 'multi' && (q.options ?? []).some((o) => o.selected);
|
|
136
|
+
(q.options ?? []).forEach((o, i) => out.write(` ${i + 1}. ${marked ? `[${o.selected ? 'x' : ' '}] ` : ''}${o.label}\n`));
|
|
134
137
|
// A sensitive answer typed at a terminal is not echoed.
|
|
135
138
|
const read = q.sensitive && process.stdin.isTTY ? hiddenLine : nextLine;
|
|
136
139
|
out.write(how ? `${how} ` : '');
|
package/dist/ui/look.js
CHANGED
|
@@ -20,6 +20,11 @@ const FIELD_WIDTH = 48;
|
|
|
20
20
|
/** The running task's spinner: one glyph every third frame. */
|
|
21
21
|
const SPINNER = ['◐', '◓', '◑', '◒'];
|
|
22
22
|
const START = { heading: "Let's set up your app", text: 'This wizard detects your framework and wires up all the essentials.' };
|
|
23
|
+
/** The sign-in over a run, by why it signs in again. */
|
|
24
|
+
const AGAIN = {
|
|
25
|
+
'ran-out': 'Your sign-in ran out. Approve again in your browser to carry on.',
|
|
26
|
+
apps: 'Impeccable found your apps after you signed in. Approve again in your browser to set them up.',
|
|
27
|
+
};
|
|
23
28
|
/** The invitation to the test drive, on whichever end screen shows it; the words "test drive" stay off it. */
|
|
24
29
|
export const SEE_THE_REVIEW = 'See Impeccable review a change: we opened a small pull request just for that.';
|
|
25
30
|
const WORDMARK = 'I M P E C C A B L E';
|
|
@@ -162,7 +167,8 @@ function questionPage(qv, inner, st) {
|
|
|
162
167
|
const width = Math.min(FIELD_WIDTH, inner);
|
|
163
168
|
const label = (text) => ` ${st.soft(text)}`;
|
|
164
169
|
const hint = q.hint ? [...wrapTextWithAnsi(q.hint, width - 2).map(label), ''] : [];
|
|
165
|
-
const
|
|
170
|
+
const [first, second] = q.fields ?? ['Username or email', 'Password'];
|
|
171
|
+
const fields = [...fieldBox(first, qv.field(width - 2), width, qv.focus !== 1, st), ...fieldBox(second, qv.password?.() ?? '', width, qv.focus === 1, st)];
|
|
166
172
|
return {
|
|
167
173
|
heading: q.title ?? 'A test account',
|
|
168
174
|
text: said(q.prompt, inner, st),
|
|
@@ -224,9 +230,15 @@ function runPage(scene, f, inner, st) {
|
|
|
224
230
|
: t.status === 'skipped' ? '–'
|
|
225
231
|
: '○';
|
|
226
232
|
const ink = t.status === 'in_progress' || t.status === 'failed' ? st.full : t.status === 'skipped' ? st.faint : st.soft;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
const row = (detail) => `${mark} ${t.title}${detail ? ` · ${detail}` : ''}`;
|
|
234
|
+
if (t.status === 'skipped' || t.status === 'failed')
|
|
235
|
+
return ink(row(t.status));
|
|
236
|
+
if (i !== running || !scene.wait)
|
|
237
|
+
return ink(row());
|
|
238
|
+
// The task in progress says what the run waits for and the time so far, never how long it may wait, so a screen
|
|
239
|
+
// waiting on a host's build is never still; the time alone where the words would push it off the row.
|
|
240
|
+
const waiting = row(`waiting for ${scene.wait.host ? `${scene.wait.host}'s preview` : 'the preview'} · ${scene.wait.timer}`);
|
|
241
|
+
return ink(visibleWidth(waiting) <= inner ? waiting : row(scene.wait.timer));
|
|
230
242
|
});
|
|
231
243
|
const counted = scene.tasks.filter((t) => t.status !== 'skipped');
|
|
232
244
|
const skipped = scene.tasks.length - counted.length;
|
|
@@ -271,15 +283,17 @@ function pageOf(f, inner) {
|
|
|
271
283
|
switch (scene.kind) {
|
|
272
284
|
case 'wait':
|
|
273
285
|
return { heading: START.heading, text: said(START.text, inner, st), rule: divider(inner, st), action: [center(st.faint('One moment…'), inner)] };
|
|
274
|
-
case 'intro':
|
|
275
|
-
|
|
286
|
+
case 'intro': {
|
|
287
|
+
const note = scene.note ? ['', ...block(hang(scene.note, Math.min(TEXT_WIDTH, inner), '! ').map(st.soft), inner)] : [];
|
|
288
|
+
return { heading: START.heading, text: said(START.text, inner, st), rule: divider(inner, st), action: [center(st.full("Let's start with login"), inner), ...note], ways: [['Continue', 'Enter']] };
|
|
289
|
+
}
|
|
276
290
|
case 'signin': {
|
|
277
291
|
// P still takes a pasted code on a remote shell; --help says so, the screen does not.
|
|
278
292
|
const under = scene.note ?? (scene.copied ? 'Copied to clipboard!' : '');
|
|
279
293
|
const link = block(urlRows(scene.link, ruleWidth(inner), st.soft, st), inner);
|
|
280
294
|
return {
|
|
281
295
|
heading: scene.again ? 'Sign in again' : START.heading,
|
|
282
|
-
text: said(scene.again ?
|
|
296
|
+
text: said(scene.again ? AGAIN[scene.again] : 'Sign in with GitHub in your browser, then approve this repository.', inner, st),
|
|
283
297
|
rule: divider(inner, st),
|
|
284
298
|
action: [...link, ...(under ? ['', center(st.full(under), inner)] : []), ...(scene.copied === false ? ['', copyWay(inner, st)] : [])],
|
|
285
299
|
};
|
package/dist/ui/run-state.js
CHANGED
|
@@ -24,7 +24,7 @@ export class RunState {
|
|
|
24
24
|
tasks = [];
|
|
25
25
|
previous;
|
|
26
26
|
current;
|
|
27
|
-
/** A bounded wait under way: the current line carries its timer, counting up
|
|
27
|
+
/** A bounded wait under way: the current line carries its timer, counting up. */
|
|
28
28
|
waiting;
|
|
29
29
|
/** When something last changed on screen: a status, the task list, an answered question. */
|
|
30
30
|
changedAt;
|
|
@@ -51,12 +51,12 @@ export class RunState {
|
|
|
51
51
|
this.touch();
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* The wait
|
|
55
|
-
* `live` false gives the wait's start, "0:00
|
|
56
|
-
* a wait begins or ends, so the frames record the wait once rather than once a second.
|
|
54
|
+
* The wait under way, which the task in progress carries on the tasks screen: the host whose preview it is and the
|
|
55
|
+
* time so far, "1:05". `live` false gives the wait's start, "0:00", the same on every tick: a drawing that changes
|
|
56
|
+
* only when a wait begins or ends, so the frames record the wait once rather than once a second.
|
|
57
57
|
*/
|
|
58
58
|
timer(live = true) {
|
|
59
|
-
return this.waiting
|
|
59
|
+
return this.waiting && { host: this.waiting.host, timer: timer(live ? this.now() - this.waiting.since : 0) };
|
|
60
60
|
}
|
|
61
61
|
setTasks(tasks) {
|
|
62
62
|
this.tasks = tasks.map((t) => ({ ...t }));
|
|
@@ -70,14 +70,14 @@ export class RunState {
|
|
|
70
70
|
* The band's two lines as they read now: after a minute with nothing new the
|
|
71
71
|
* last line moves up, dimmed, under "Still working…" and the time since.
|
|
72
72
|
* `quiet` is false while the person is being asked. During a bounded wait
|
|
73
|
-
* the current line carries the timer, "1:05
|
|
73
|
+
* the current line carries the timer, "1:05", so the screen never
|
|
74
74
|
* looks stalled; `live` false holds the timer at the wait's start, for a
|
|
75
75
|
* drawing that changes only when the words do.
|
|
76
76
|
*/
|
|
77
77
|
band(quiet = true, live = true) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const wait = this.timer(live);
|
|
79
|
+
if (wait && this.current)
|
|
80
|
+
return { previous: this.previous, current: `${this.current} · ${wait.timer}` };
|
|
81
81
|
const silence = this.now() - this.changedAt;
|
|
82
82
|
if (quiet && silence >= SILENCE_MS)
|
|
83
83
|
return { previous: this.current, current: `Still working… ${elapsed(silence)}` };
|
package/dist/ui/screens.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { format } from 'node:util';
|
|
3
3
|
import { CURSOR_MARKER, getCapabilities, Input, KeybindingsManager, matchesKey, ProcessTerminal, setKeybindings, TUI_KEYBINDINGS, TuiAltScreen } from '@earendil-works/pi-tui';
|
|
4
|
-
import { readCheckout } from '../preflight.js';
|
|
5
4
|
import { copyText } from '../login.js';
|
|
6
5
|
import { workStarted } from '../stages.js';
|
|
7
6
|
import { mountUi } from './bridge.js';
|
|
8
7
|
import { createFrameWriter, frameState } from './frames.js';
|
|
9
8
|
import { drawFrame, OUTCOME, PRODUCT, palette } from './look.js';
|
|
10
9
|
import { RunState } from './run-state.js';
|
|
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
|
+
let beforeExit = async () => { };
|
|
12
|
+
export function onScreenExit(fn) {
|
|
13
|
+
beforeExit = fn;
|
|
14
|
+
}
|
|
11
15
|
/** How often what moves on screen moves: the loader, the spinner. */
|
|
12
16
|
const TICK_MS = 80;
|
|
13
17
|
/** The run screen's first line, before the agent has said anything. */
|
|
@@ -44,7 +48,7 @@ class Wizard {
|
|
|
44
48
|
onExit = () => this.stop();
|
|
45
49
|
onSignal = (signal) => {
|
|
46
50
|
this.stop();
|
|
47
|
-
process.kill(process.pid, signal);
|
|
51
|
+
void beforeExit('terminated').finally(() => process.kill(process.pid, signal));
|
|
48
52
|
};
|
|
49
53
|
constructor() {
|
|
50
54
|
// The alternate screen's own scrolling and search keys stay off: the wizard draws every row, and a typed answer keeps Home and End.
|
|
@@ -155,7 +159,9 @@ class Wizard {
|
|
|
155
159
|
this.state.touch();
|
|
156
160
|
resolve(answer);
|
|
157
161
|
};
|
|
158
|
-
|
|
162
|
+
// A multi choice opens with the options the question marks selected picked.
|
|
163
|
+
const picked = new Set(q.kind === 'multi' ? (q.options ?? []).flatMap((o, i) => (o.selected ? [i] : [])) : []);
|
|
164
|
+
this.questions.push({ q, resolve: answered, field, password: q.kind === 'account' ? new Input() : undefined, focus: 0, cursor: 0, picked });
|
|
159
165
|
this.redraw();
|
|
160
166
|
});
|
|
161
167
|
}
|
|
@@ -226,7 +232,8 @@ class Wizard {
|
|
|
226
232
|
const run = this.views.find((v) => v.kind === 'run');
|
|
227
233
|
if (!run) {
|
|
228
234
|
this.stop();
|
|
229
|
-
process.exit(130);
|
|
235
|
+
void beforeExit('ctrl_c').finally(() => process.exit(130));
|
|
236
|
+
return;
|
|
230
237
|
}
|
|
231
238
|
this.stopAsked = 0;
|
|
232
239
|
for (const question of this.questions.splice(0))
|
|
@@ -289,14 +296,19 @@ class Wizard {
|
|
|
289
296
|
else if (matchesKey(data, 'enter')) {
|
|
290
297
|
if (choices && q.kind === 'single')
|
|
291
298
|
return done({ skipped: false, value: choices[question.cursor].value });
|
|
292
|
-
|
|
299
|
+
// A multi choice answers what is picked; with nothing picked, Skip is the way to say none.
|
|
300
|
+
if (choices && question.picked.size)
|
|
293
301
|
return done({ skipped: false, values: choices.filter((_, i) => question.picked.has(i)).map((o) => o.value) });
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
if (choices)
|
|
303
|
+
question.note = 'Pick at least one, or skip.';
|
|
304
|
+
else {
|
|
305
|
+
const value = question.field.getValue().trim();
|
|
306
|
+
if (value)
|
|
307
|
+
return done({ skipped: false, value });
|
|
308
|
+
if (q.skippable)
|
|
309
|
+
return done({ skipped: true });
|
|
310
|
+
question.note = 'Type an answer first.';
|
|
311
|
+
}
|
|
300
312
|
}
|
|
301
313
|
else if (!choices) {
|
|
302
314
|
question.field.handleInput(data);
|
|
@@ -323,7 +335,7 @@ class Wizard {
|
|
|
323
335
|
if (!view)
|
|
324
336
|
return { kind: 'wait' };
|
|
325
337
|
if (view.kind === 'intro')
|
|
326
|
-
return { kind: 'intro' };
|
|
338
|
+
return { kind: 'intro', note: view.note };
|
|
327
339
|
if (view.kind === 'signin')
|
|
328
340
|
return { kind: 'signin', link: view.link, copied: view.copied, again: view.again, note: view.pasting ? undefined : view.note };
|
|
329
341
|
if (view.kind === 'pull')
|
|
@@ -394,30 +406,24 @@ function host() {
|
|
|
394
406
|
wizard ??= new Wizard();
|
|
395
407
|
return wizard;
|
|
396
408
|
}
|
|
397
|
-
function repository() {
|
|
398
|
-
try {
|
|
399
|
-
const checkout = readCheckout(process.cwd());
|
|
400
|
-
return `${checkout.owner}/${checkout.repo}`;
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
return undefined;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
409
|
/** Writes every screen into `dir` as it changes (--frames), for the eval. */
|
|
407
410
|
export function recordFrames(dir) {
|
|
408
411
|
frames = createFrameWriter(dir);
|
|
409
412
|
}
|
|
410
|
-
/** The first screen: what the command does, and the way on. Resolves true to go on, false to cancel. */
|
|
411
|
-
export function showIntro() {
|
|
413
|
+
/** The first screen: what the command does, a warning to read before anything runs (`note`), and the way on; without a terminal, a line naming `repository`, the owner/name being set up. Resolves true to go on, false to cancel. */
|
|
414
|
+
export function showIntro(repository, note) {
|
|
412
415
|
const screen = host();
|
|
413
416
|
if (!screen) {
|
|
414
|
-
console.log(`${PRODUCT}: setting up ${repository
|
|
417
|
+
console.log(`${PRODUCT}: setting up ${repository}.`);
|
|
418
|
+
if (note)
|
|
419
|
+
console.log(`! ${note}`);
|
|
415
420
|
console.log();
|
|
416
421
|
return Promise.resolve(true);
|
|
417
422
|
}
|
|
418
423
|
return new Promise((resolve) => {
|
|
419
424
|
const view = {
|
|
420
425
|
kind: 'intro',
|
|
426
|
+
note,
|
|
421
427
|
done: (ok) => {
|
|
422
428
|
screen.remove(view);
|
|
423
429
|
if (ok)
|
|
@@ -443,7 +449,7 @@ export function showSignIn(link, opts) {
|
|
|
443
449
|
console.log(link);
|
|
444
450
|
return { close() { } };
|
|
445
451
|
}
|
|
446
|
-
const view = { kind: 'signin', link, opts, pasting: false, again: screen.views.some((v) => v.kind === 'run') };
|
|
452
|
+
const view = { kind: 'signin', link, opts, pasting: false, again: screen.views.some((v) => v.kind === 'run') && (opts.why ?? 'ran-out') };
|
|
447
453
|
screen.push(view);
|
|
448
454
|
screen.copy(view);
|
|
449
455
|
return { close: () => screen.remove(view) };
|
package/dist/vercel.js
CHANGED
|
@@ -68,3 +68,80 @@ export async function waitForVercelAccess(vercel, deps) {
|
|
|
68
68
|
}
|
|
69
69
|
return result;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* `vercel api` on the project this folder is linked to: GET /v9/projects/{id} reads it, and PATCH changes a setting
|
|
73
|
+
* through the update-project endpoint (https://vercel.com/docs/rest-api/projects/update-an-existing-project); a team's
|
|
74
|
+
* project needs its team on the request.
|
|
75
|
+
*/
|
|
76
|
+
const projectApi = (link, change) => [
|
|
77
|
+
'api',
|
|
78
|
+
`/v9/projects/${encodeURIComponent(link.projectId)}${link.orgId.startsWith('team_') ? `?teamId=${encodeURIComponent(link.orgId)}` : ''}`,
|
|
79
|
+
...(change ? ['--method', 'PATCH', ...change] : []),
|
|
80
|
+
'--raw', '--non-interactive',
|
|
81
|
+
];
|
|
82
|
+
export const readProjectApi = (link) => projectApi(link);
|
|
83
|
+
/**
|
|
84
|
+
* Vercel Authentication on All Deployments: Vercel's login in front of every address of the project, its production
|
|
85
|
+
* domains included (https://vercel.com/docs/deployment-protection#all-deployments), as the endpoint takes it
|
|
86
|
+
* (https://vercel.com/docs/deployment-protection/methods-to-protect-deployments/vercel-authentication#how-to-manage-vercel-authentication-with-the-api).
|
|
87
|
+
*/
|
|
88
|
+
export const lockProjectApi = (link) => projectApi(link, ['--field', 'ssoProtection={"deploymentType":"all"}']);
|
|
89
|
+
const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
|
|
90
|
+
const printedProject = async (vercel, args) => {
|
|
91
|
+
const printed = await vercel(args);
|
|
92
|
+
return record(JSON.parse(printed.slice(printed.indexOf('{'), printed.lastIndexOf('}') + 1)));
|
|
93
|
+
};
|
|
94
|
+
const scope = (project) => record(project?.ssoProtection)?.deploymentType;
|
|
95
|
+
/** The linked project as `vercel api` prints it: its production deployment (the production target, or one among the latest) unless every one failed or was cancelled, its Git link's production branch, its protection and its deployments' hosts. Null when the CLI could not read it. */
|
|
96
|
+
export async function readVercelProject(vercel, link) {
|
|
97
|
+
let project;
|
|
98
|
+
try {
|
|
99
|
+
project = await printedProject(vercel, readProjectApi(link));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (project?.id !== link.projectId)
|
|
105
|
+
return null;
|
|
106
|
+
const target = record(record(project.targets)?.production);
|
|
107
|
+
const latest = (Array.isArray(project.latestDeployments) ? project.latestDeployments : []).map(record).filter((d) => d !== undefined);
|
|
108
|
+
const failed = (d) => d.readyState === 'ERROR' || d.readyState === 'CANCELED';
|
|
109
|
+
const exists = (target !== undefined && !failed(target)) || latest.some((d) => d.target === 'production' && !failed(d));
|
|
110
|
+
const branch = record(project.link)?.productionBranch;
|
|
111
|
+
const protection = scope(project);
|
|
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) };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The linked project before its first push: read through `vercel api`, and, with no production deployment, locked
|
|
117
|
+
* behind Vercel's login at every address, since Vercel makes a new project's first deployment Production whatever
|
|
118
|
+
* branch it builds and Standard Protection serves production domains to anyone. A project that deploys production
|
|
119
|
+
* already (live or building), or is locked already, is left as it is.
|
|
120
|
+
*/
|
|
121
|
+
export async function prepareFirstDeployment(vercel, link) {
|
|
122
|
+
const project = await readVercelProject(vercel, link);
|
|
123
|
+
if (!project)
|
|
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 } : {};
|
|
126
|
+
if (project.production === 'exists')
|
|
127
|
+
return { production: 'exists', ...branch };
|
|
128
|
+
if (project.protection === 'all')
|
|
129
|
+
return { production: 'none', locked: true, ...branch };
|
|
130
|
+
try {
|
|
131
|
+
if (scope(await printedProject(vercel, lockProjectApi(link))) === 'all')
|
|
132
|
+
return { production: 'none', locked: true, ...branch };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// What the answer below says.
|
|
136
|
+
}
|
|
137
|
+
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
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The hosts of the linked project's deployments while Vercel's login stands in front of every address of it, read
|
|
141
|
+
* now: a Production deployment GitHub lists for a pushed branch is its preview only when it is one of these. Empty
|
|
142
|
+
* when the project is not locked at every address, or could not be read.
|
|
143
|
+
*/
|
|
144
|
+
export async function lockedDeploymentHosts(vercel, link) {
|
|
145
|
+
const project = await readVercelProject(vercel, link);
|
|
146
|
+
return project?.protection === 'all' ? project.deploymentHosts : new Set();
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impeccable/reviews",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Set up a repository for Impeccable for PRs from your terminal.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"eval": "npm run build && node dist/eval/main.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@anthropic-ai/sandbox-runtime": "0.0.78",
|
|
27
28
|
"@earendil-works/pi-coding-agent": "~0.87.1",
|
|
28
29
|
"@earendil-works/pi-tui": "~0.87.1"
|
|
29
30
|
},
|