@impeccable/reviews 0.1.0

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.
Files changed (46) hide show
  1. package/README.md +68 -0
  2. package/dist/agent.js +183 -0
  3. package/dist/bin.js +11 -0
  4. package/dist/fence.js +1071 -0
  5. package/dist/focus.js +98 -0
  6. package/dist/host-questions.js +71 -0
  7. package/dist/hosted.js +195 -0
  8. package/dist/http.js +36 -0
  9. package/dist/log.js +38 -0
  10. package/dist/login.js +276 -0
  11. package/dist/main.js +252 -0
  12. package/dist/netlify.js +333 -0
  13. package/dist/playbook/cases/ci-tunnel.md +21 -0
  14. package/dist/playbook/cases/first-preview-failure.md +28 -0
  15. package/dist/playbook/cases/netlify-previews.md +17 -0
  16. package/dist/playbook/cases/netlify-setup.md +24 -0
  17. package/dist/playbook/cases/preview-protection.md +21 -0
  18. package/dist/playbook/cases/pull-requests.md +31 -0
  19. package/dist/playbook/cases/sign-in.md +21 -0
  20. package/dist/playbook/cases/static-site.md +16 -0
  21. package/dist/playbook/cases/vercel-previews.md +16 -0
  22. package/dist/playbook/cases/vercel-setup.md +27 -0
  23. package/dist/playbook/cases/workflow-as-person.md +23 -0
  24. package/dist/playbook/github-previews.js +101 -0
  25. package/dist/playbook/netlify-pattern.js +24 -0
  26. package/dist/playbook/netlify-previews.js +144 -0
  27. package/dist/playbook/probe-preview.js +67 -0
  28. package/dist/playbook/protection-secrets.js +15 -0
  29. package/dist/playbook/public-preview-request.js +96 -0
  30. package/dist/playbook/publication-wait.js +46 -0
  31. package/dist/playbook/vercel-protection.js +121 -0
  32. package/dist/playbook.js +98 -0
  33. package/dist/preflight.js +211 -0
  34. package/dist/stages.js +34 -0
  35. package/dist/sync.js +142 -0
  36. package/dist/tools.js +2015 -0
  37. package/dist/transcript.js +139 -0
  38. package/dist/ui/bridge.js +145 -0
  39. package/dist/ui/frames.js +62 -0
  40. package/dist/ui/look.js +368 -0
  41. package/dist/ui/run-state.js +91 -0
  42. package/dist/ui/screens.js +544 -0
  43. package/dist/ui/sensitive.js +14 -0
  44. package/dist/vercel-bypass.js +136 -0
  45. package/dist/vercel.js +70 -0
  46. package/package.json +39 -0
@@ -0,0 +1,136 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Getting past Vercel's Deployment Protection the way Vercel documents it for tools: Protection Bypass for Automation, a
5
+ * secret of the project that a request sends in the x-vercel-protection-bypass header (every plan has it; a team member
6
+ * or a project administrator makes one). The Vercel CLI on this machine, signed in as the person, reads and makes it in
7
+ * the command's own process: what those commands print is the secret, so the agent's shell never runs them (fence.ts),
8
+ * and no secret goes back to the model. The person consents once, on the tool's screen, before one is made; a secret
9
+ * opens the locked address before anything saves it.
10
+ */
11
+ /** The header Vercel reads the secret from. */
12
+ export const BYPASS_HEADER = 'x-vercel-protection-bypass';
13
+ /** The label the secret carries in the project's settings, so the person knows whose it is and can revoke it. */
14
+ export const SECRET_NOTE = 'Impeccable reviews';
15
+ /** Vercel makes its secrets of 32 letters and digits, and takes no other. */
16
+ export const isBypassSecret = (value) => typeof value === 'string' && /^[A-Za-z0-9]{32}$/.test(value);
17
+ /** The CLI's read of the linked project's protection: `protectionBypass` maps each secret to its note and creation time. */
18
+ export const READ = ['project', 'protection', '--format', 'json', '--non-interactive'];
19
+ /** The CLI's own way to make a secret, which carries no label, for a CLI without `api`. */
20
+ export const MAKE_CLI = ['project', 'protection', 'enable', '--protection-bypass', '--non-interactive'];
21
+ /** `vercel api` (beta) for PATCH /v1/projects/{id}/protection-bypass with `generate.note`, so the secret carries the label; a team's project needs its team on the request. */
22
+ export const makeApi = (link) => [
23
+ 'api',
24
+ `/v1/projects/${encodeURIComponent(link.projectId)}/protection-bypass${link.orgId.startsWith('team_') ? `?teamId=${encodeURIComponent(link.orgId)}` : ''}`,
25
+ '--method', 'PATCH',
26
+ '--field', `generate=${JSON.stringify({ note: SECRET_NOTE })}`,
27
+ '--raw', '--non-interactive',
28
+ ];
29
+ const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
30
+ export async function readLink(cwd) {
31
+ try {
32
+ const link = record(JSON.parse(await readFile(join(cwd, '.vercel', 'project.json'), 'utf8')));
33
+ return typeof link?.projectId === 'string' && typeof link.orgId === 'string' ? { projectId: link.projectId, orgId: link.orgId } : null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /** The linked project's name and automation secrets; null when the CLI could not say: signed out, not linked, or the role does not allow it. */
40
+ async function readProject(run) {
41
+ let settings;
42
+ try {
43
+ const printed = await run(READ);
44
+ settings = record(JSON.parse(printed.slice(printed.indexOf('{'), printed.lastIndexOf('}') + 1)));
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ if (typeof settings?.projectId !== 'string')
50
+ return null;
51
+ const secrets = Object.entries(record(settings.protectionBypass) ?? {}).flatMap(([value, meta]) => {
52
+ const m = record(meta);
53
+ return isBypassSecret(value) && m?.scope === 'automation-bypass' ? [{ value, ...(typeof m.note === 'string' ? { note: m.note } : {}), createdAt: Number(m.createdAt) || 0 }] : [];
54
+ });
55
+ return { name: typeof settings.name === 'string' ? settings.name : '', secrets };
56
+ }
57
+ /** Impeccable's secret among the project's: the newest labelled for it; else, after the CLI made one without a label, the newest of those that were not there `before`. */
58
+ function pick(secrets, before) {
59
+ const newest = (list) => [...list].sort((a, b) => b.createdAt - a.createdAt)[0];
60
+ return newest(secrets.filter((s) => s.note === SECRET_NOTE)) ?? (before && newest(secrets.filter((s) => !before.has(s.value))));
61
+ }
62
+ /** Makes the secret with the label through `vercel api`; a CLI without `api` (or one Vercel refuses) falls back to its own command, which the same role check stops. */
63
+ async function make(run, link) {
64
+ if (link) {
65
+ try {
66
+ await run(makeApi(link));
67
+ return true;
68
+ }
69
+ catch {
70
+ // The CLI's own command below fails the same way when the role is missing, and works when `api` is the one missing.
71
+ }
72
+ }
73
+ try {
74
+ await run(MAKE_CLI);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ /**
82
+ * Gets a bypass secret in place for the address Vercel's login stands in front of, and saves only one that opens it. The
83
+ * project's own labelled secret needs no question; with none, the person's yes has the CLI make one, read back by its
84
+ * label (or, from the CLI's own command, as the one that was not there before); when the CLI cannot read or make one, or
85
+ * what it gave opens nothing, the person may paste one.
86
+ */
87
+ export async function passVercelLogin(deps) {
88
+ const project = await readProject(deps.run);
89
+ const from = project?.name ? { project: project.name } : {};
90
+ const finish = async (secret, how) => {
91
+ deps.remember(secret);
92
+ const proof = await deps.prove(secret);
93
+ if (!proof.proven)
94
+ return { reason: proof.reason ?? 'The address did not open with the secret.' };
95
+ const failed = await deps.save(secret);
96
+ return failed ? { status: 'save-failed', reason: failed, ...from } : { status: 'saved', secret: how, ...from, value: secret };
97
+ };
98
+ /** Why the CLI's way ended without a secret that opens the preview, for the paste screen's skip. */
99
+ let stopped;
100
+ if (!project)
101
+ stopped = 'The Vercel CLI could not read the project: it is signed out, this folder is not linked to the project, or the role does not allow it.';
102
+ else {
103
+ const own = pick(project.secrets);
104
+ if (own) {
105
+ const done = await finish(own.value, 'existing');
106
+ if ('status' in done)
107
+ return done;
108
+ stopped = done.reason;
109
+ }
110
+ else {
111
+ if (!(await deps.consent()))
112
+ return { status: 'skipped', reason: 'The person skipped creating a bypass secret.', ...from };
113
+ const before = new Set(project.secrets.map((s) => s.value));
114
+ if (!(await make(deps.run, await deps.link())))
115
+ stopped = 'The Vercel CLI could not make a bypass secret on the project: that takes the Member role or Project Administrator.';
116
+ else {
117
+ const ours = pick((await readProject(deps.run))?.secrets ?? [], before);
118
+ if (!ours)
119
+ stopped = 'The secret the Vercel CLI made could not be read back.';
120
+ else {
121
+ const done = await finish(ours.value, 'created');
122
+ if ('status' in done)
123
+ return done;
124
+ stopped = done.reason;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ const typed = await deps.paste();
130
+ if (typed === undefined)
131
+ return { status: 'skipped', reason: stopped, ...from };
132
+ if (!isBypassSecret(typed))
133
+ return { status: 'refused', reason: 'What was typed is not a Vercel bypass secret: Vercel makes them of 32 letters and digits.', ...from };
134
+ const done = await finish(typed, 'typed');
135
+ return 'status' in done ? done : { status: 'refused', reason: done.reason, ...from };
136
+ }
package/dist/vercel.js ADDED
@@ -0,0 +1,70 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ /** `vercel whoami`: the CLI prints the username, or says it has no credentials and exits 1. */
3
+ export async function vercelStatus(vercel) {
4
+ try {
5
+ const user = (await vercel(['whoami'])).trim().split('\n').at(-1)?.trim() ?? '';
6
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(user) ? { cli: 'signed-in', user } : { cli: 'unknown' };
7
+ }
8
+ catch (err) {
9
+ if (err.missing)
10
+ return { cli: 'missing' };
11
+ const said = String(err.stdout ?? '') + String(err.stderr ?? '');
12
+ return /credentials|log ?in|logged out/i.test(said) ? { cli: 'signed-out' } : { cli: 'unknown' };
13
+ }
14
+ }
15
+ /**
16
+ * `vercel teams ls --json`: the teams the person is a member of, each by its slug (what `--team` takes) and name, and
17
+ * which one is the CLI's current scope. The JSON is on stdout; the table the CLI prints otherwise goes to stderr.
18
+ */
19
+ export async function vercelTeams(vercel) {
20
+ try {
21
+ const listed = JSON.parse(await vercel(['teams', 'ls', '--json'])).teams;
22
+ return (Array.isArray(listed) ? listed : []).flatMap((team) => typeof team.slug === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(team.slug)
23
+ ? [{ value: team.slug, label: typeof team.name === 'string' && team.name ? team.name.slice(0, 100) : team.slug, current: team.current === true }]
24
+ : []);
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ /** Where Vercel's GitHub app is installed or given more repositories. */
31
+ export const VERCEL_APP_PAGE = 'https://github.com/apps/vercel/installations/new';
32
+ /**
33
+ * `vercel git connect` in the linked folder: it connects the Git remote (exit 0), says the repository is connected to
34
+ * the project already (exit 1), or fails to connect it, which is Vercel's GitHub app not reaching the repository
35
+ * ("Make sure … that you have access to the repository").
36
+ */
37
+ export async function connectVercelProject(vercel) {
38
+ let said;
39
+ try {
40
+ await vercel(['git', 'connect']);
41
+ return { connected: true };
42
+ }
43
+ catch (err) {
44
+ if (err.missing)
45
+ return { connected: false, reason: 'The Vercel CLI is not installed.' };
46
+ said = String(err.stdout ?? '') + String(err.stderr ?? '');
47
+ }
48
+ if (/already connected to your project/i.test(said))
49
+ return { connected: true };
50
+ if (/failed to connect|access to the repository|install github app/i.test(said))
51
+ return { connected: false, access: VERCEL_APP_PAGE, reason: "Vercel's GitHub app cannot reach the repository." };
52
+ return { connected: false, reason: 'Vercel did not connect the repository.' };
53
+ }
54
+ /** Tries the connection again every `wait.everyMs` until it is made or `wait.totalMs` has passed (or the run stopped): the person is giving Vercel's GitHub app access meanwhile. */
55
+ export async function waitForVercelAccess(vercel, deps) {
56
+ const now = deps.now ?? Date.now;
57
+ let result = { connected: false, access: VERCEL_APP_PAGE };
58
+ for (const end = now() + deps.wait.totalMs; !deps.signal?.aborted;) {
59
+ result = await connectVercelProject(vercel);
60
+ if (result.connected || !result.access || now() >= end)
61
+ return result;
62
+ try {
63
+ await (deps.sleep ?? ((ms) => sleep(ms, undefined, { signal: deps.signal })))(deps.wait.everyMs);
64
+ }
65
+ catch {
66
+ return result;
67
+ }
68
+ }
69
+ return result;
70
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@impeccable/reviews",
3
+ "version": "0.1.0",
4
+ "description": "Set up a repository for Impeccable for PRs from your terminal.",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "type": "module",
8
+ "bin": {
9
+ "impeccable-reviews": "dist/bin.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "!dist/smoke.js",
14
+ "!dist/eval"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.19.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json && tsc -p eval/tsconfig.json && node scripts/copy-static.mjs",
21
+ "prepack": "npm run build",
22
+ "test": "vitest run --dir test",
23
+ "smoke": "npm run build && node dist/smoke.js",
24
+ "eval": "npm run build && node dist/eval/main.js"
25
+ },
26
+ "dependencies": {
27
+ "@earendil-works/pi-coding-agent": "~0.87.1",
28
+ "@earendil-works/pi-tui": "~0.87.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.20.4",
32
+ "playwright": "~1.63.0",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "^5.0.2"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }