@impeccable/reviews 0.1.7 → 0.1.8

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.
@@ -14,10 +14,12 @@ export const BYPASS_HEADER = 'x-vercel-protection-bypass';
14
14
  export const SECRET_NOTE = 'Impeccable reviews';
15
15
  /** Vercel makes its secrets of 32 letters and digits, and takes no other. */
16
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. */
17
+ /** The CLI's read of the linked project's protection, or with `name` that project's: `protectionBypass` maps each secret to its note and creation time. */
18
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`. */
19
+ const readArgs = (name) => (name ? ['project', 'protection', name, ...READ.slice(2)] : READ);
20
+ /** The CLI's own way to make a secret, which carries no label, for a CLI without `api`; with `name`, on that project. */
20
21
  export const MAKE_CLI = ['project', 'protection', 'enable', '--protection-bypass', '--non-interactive'];
22
+ const makeArgs = (name) => (name ? ['project', 'protection', 'enable', name, ...MAKE_CLI.slice(3)] : MAKE_CLI);
21
23
  /** `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
24
  export const makeApi = (link) => [
23
25
  'api',
@@ -30,21 +32,23 @@ const record = (value) => (value !== null && typeof value === 'object' && !Array
30
32
  /** The ids as Vercel makes them, and nothing else goes into a request's path: the file is in the checkout, which the repository and the sandboxed agent write. */
31
33
  const PROJECT_ID = /^prj_[A-Za-z0-9]+$/;
32
34
  const ORG_ID = /^(?:team_)?[A-Za-z0-9]+$/;
35
+ /** A project and its team or account as Vercel names them, or null for ids that are not Vercel's. */
36
+ export const asLink = (projectId, orgId) => typeof projectId === 'string' && PROJECT_ID.test(projectId) && typeof orgId === 'string' && ORG_ID.test(orgId) ? { projectId, orgId } : null;
33
37
  /** The link as the file has it; null with no file, or ids that are not Vercel's. What project the ids name is the CLI's to confirm (readOwnProject in vercel.ts). */
34
38
  export async function readLink(cwd) {
35
39
  try {
36
40
  const link = record(JSON.parse(await readFile(join(cwd, '.vercel', 'project.json'), 'utf8')));
37
- return typeof link?.projectId === 'string' && PROJECT_ID.test(link.projectId) && typeof link.orgId === 'string' && ORG_ID.test(link.orgId) ? { projectId: link.projectId, orgId: link.orgId } : null;
41
+ return asLink(link?.projectId, link?.orgId);
38
42
  }
39
43
  catch {
40
44
  return null;
41
45
  }
42
46
  }
43
47
  /** 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; or when what it read is not the project `link` names. */
44
- async function readProject(run, link) {
48
+ async function readProject(run, link, name) {
45
49
  let settings;
46
50
  try {
47
- const printed = await run(READ);
51
+ const printed = await run(readArgs(name));
48
52
  settings = record(JSON.parse(printed.slice(printed.indexOf('{'), printed.lastIndexOf('}') + 1)));
49
53
  }
50
54
  catch {
@@ -64,7 +68,7 @@ function pick(secrets, before) {
64
68
  return newest(secrets.filter((s) => s.note === SECRET_NOTE)) ?? (before && newest(secrets.filter((s) => !before.has(s.value))));
65
69
  }
66
70
  /** 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. */
67
- async function make(run, link) {
71
+ async function make(run, link, name) {
68
72
  if (link) {
69
73
  try {
70
74
  await run(makeApi(link));
@@ -75,7 +79,7 @@ async function make(run, link) {
75
79
  }
76
80
  }
77
81
  try {
78
- await run(MAKE_CLI);
82
+ await run(makeArgs(name));
79
83
  return true;
80
84
  }
81
85
  catch {
@@ -90,7 +94,7 @@ async function make(run, link) {
90
94
  */
91
95
  export async function passVercelLogin(deps) {
92
96
  const link = await deps.link();
93
- const project = await readProject(deps.run, link);
97
+ const project = await readProject(deps.run, link, deps.project);
94
98
  const from = project?.name ? { project: project.name } : {};
95
99
  const finish = async (secret, how) => {
96
100
  deps.remember(secret);
@@ -116,10 +120,10 @@ export async function passVercelLogin(deps) {
116
120
  if (!(await deps.consent()))
117
121
  return { status: 'skipped', reason: 'The person skipped creating a bypass secret.', ...from };
118
122
  const before = new Set(project.secrets.map((s) => s.value));
119
- if (!(await make(deps.run, link)))
123
+ if (!(await make(deps.run, link, deps.project)))
120
124
  stopped = 'The Vercel CLI could not make a bypass secret on the project: that takes the Member role or Project Administrator.';
121
125
  else {
122
- const ours = pick((await readProject(deps.run, link))?.secrets ?? [], before);
126
+ const ours = pick((await readProject(deps.run, link, deps.project))?.secrets ?? [], before);
123
127
  if (!ours)
124
128
  stopped = 'The secret the Vercel CLI made could not be read back.';
125
129
  else {
package/dist/vercel.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { setTimeout as sleep } from 'node:timers/promises';
4
- import { matchesBranch, TEST_DRIVE_BRANCH } from './host-previews.js';
5
- import { readLink } from './vercel-bypass.js';
4
+ import { folderOf, matchesBranch, TEST_DRIVE_BRANCH } from './host-previews.js';
5
+ import { asLink, readLink } 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 {
@@ -135,7 +135,8 @@ export async function readLinkedProject(vercel, link) {
135
135
  const ignore = project.commandForIgnoringBuildStep;
136
136
  // 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.
137
137
  const live = target && !failed(target) ? (Array.isArray(target.alias) ? target.alias : []).map(hostname).find(Boolean) : undefined;
138
- return { ...(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' };
138
+ const root = folderOf(project.rootDirectory);
139
+ 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' };
139
140
  }
140
141
  /**
141
142
  * The linked project as this repository's: read now, and connected to the repository on Vercel, which its Git link
@@ -150,6 +151,41 @@ export async function readOwnProject(vercel, link, repository) {
150
151
  return { reason: `The Vercel project this folder is linked to${project.name ? ` (${project.name})` : ''} is not connected to this repository on Vercel.` };
151
152
  return project;
152
153
  }
154
+ /** Vercel's project names, which go into the CLI's arguments. */
155
+ const PROJECT_NAME = /^[a-z0-9][a-z0-9._-]{0,99}$/;
156
+ /**
157
+ * The Vercel project that builds `directory` of `repository`, on a repository with several apps: of the projects Vercel
158
+ * lists as connected to the repository (GET /v10/projects?repoUrl=), in the team `team` names (the root link's) or the
159
+ * CLI's current scope, the one whose Root Directory is that folder. Never the newest, nor the one the root link names:
160
+ * none, or several, is a `reason`, so one app never borrows another's project.
161
+ */
162
+ export async function vercelProjectFor(vercel, repository, directory, team) {
163
+ const where = directory ? `the folder ${directory}` : "the repository's root";
164
+ let listed;
165
+ try {
166
+ const query = `repoUrl=${encodeURIComponent(`https://github.com/${repository}`)}&limit=100${team?.startsWith('team_') ? `&teamId=${encodeURIComponent(team)}` : ''}`;
167
+ const printed = await vercel(['api', `/v10/projects?${query}`, '--raw', '--non-interactive']);
168
+ listed = record(JSON.parse(printed.slice(printed.indexOf('{'), printed.lastIndexOf('}') + 1)))?.projects;
169
+ }
170
+ catch {
171
+ return { reason: "The Vercel CLI could not list this repository's projects (vercel api): it is signed out, or this CLI has no api command." };
172
+ }
173
+ if (!Array.isArray(listed))
174
+ return { reason: "The Vercel CLI could not list this repository's projects (vercel api): it is signed out, or this CLI has no api command." };
175
+ const builds = listed.map(record).filter((p) => {
176
+ const git = record(p?.link);
177
+ return git?.type === 'github' && `${git.org}/${git.repo}`.toLowerCase() === repository.toLowerCase() && folderOf(p.rootDirectory) === folderOf(directory);
178
+ });
179
+ const found = builds.flatMap((p) => {
180
+ const link = asLink(p.id, p.accountId);
181
+ return link && typeof p.name === 'string' && PROJECT_NAME.test(p.name) ? [{ link, name: p.name }] : [];
182
+ });
183
+ if (builds.length > 1)
184
+ return { reason: `Several Vercel projects build ${where} (${found.map((p) => p.name).join(', ') || 'unnamed'}), so which one is this app's cannot be told.` };
185
+ if (!found.length)
186
+ return { reason: `No Vercel project of this repository builds ${where} (its Root Directory), as far as the Vercel CLI here can see.` };
187
+ return found[0];
188
+ }
153
189
  /**
154
190
  * Whether the project made the deployment at `host`: one of its latest, as the project read lists them; else, since a
155
191
  * project lists its newest deployments alone while an older pull request's preview stays served, the deployment read
@@ -174,15 +210,16 @@ export async function projectMadeDeployment(vercel, link, project, host) {
174
210
  * (readOwnProject): another repository's settings are never the fact. Nothing here changes Vercel: the fact is reported.
175
211
  * Undefined where the checkout names no Vercel setting and is linked to no project.
176
212
  */
177
- export async function vercelPreviewsFact(cwd, repository, vercel) {
213
+ export async function vercelPreviewsFact(cwd, repository, vercel, app) {
178
214
  let setting;
179
215
  try {
180
- setting = record(record(JSON.parse(await readFile(join(cwd, 'vercel.json'), 'utf8')))?.git)?.deploymentEnabled;
216
+ // An app's project reads the vercel.json of the folder it builds.
217
+ setting = record(record(JSON.parse(await readFile(join(cwd, app?.directory ?? '', 'vercel.json'), 'utf8')))?.git)?.deploymentEnabled;
181
218
  }
182
219
  catch {
183
220
  // No vercel.json, or none that reads as JSON: Vercel's default, every branch.
184
221
  }
185
- const link = await readLink(cwd);
222
+ const link = app ? app.link : await readLink(cwd);
186
223
  if (setting === undefined && !link)
187
224
  return undefined;
188
225
  // The CLI runs only for a linked project, and only this repository's own counts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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,