inflight-cli 2.12.0 → 2.14.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.
@@ -1,9 +1,27 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { execSync } from "child_process";
4
- import { parseGitRepo, getGitRoot } from "../lib/git.js";
4
+ import { parseGitRepo } from "../lib/git.js";
5
5
  import { isAgent } from "../lib/agent.js";
6
- import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectById, getVercelProjects, matchVercelProjectsByRepo, createVercelProject, getVercelDeployments, } from "../lib/vercel.js";
6
+ import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectById, getVercelProjects, matchVercelProjectsByRepo, createVercelProject, getVercelDeployments, getVercelDeploymentById, } from "../lib/vercel.js";
7
+ import { resolveDeploymentUrl } from "./shared.js";
8
+ // --- Vercel state normalization ---
9
+ function normalizeVercelState(state) {
10
+ switch (state) {
11
+ case "READY":
12
+ return "ready";
13
+ case "ERROR":
14
+ case "CANCELED":
15
+ return "error";
16
+ case "BUILDING":
17
+ case "INITIALIZING":
18
+ return "building";
19
+ case "QUEUED":
20
+ return "queued";
21
+ default:
22
+ return "building";
23
+ }
24
+ }
7
25
  // --- Auto-detection ---
8
26
  /**
9
27
  * Auto-detect the Vercel project for the current git repo.
@@ -11,33 +29,25 @@ import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVe
11
29
  *
12
30
  * 1. `.vercel/project.json` at git root (instant, no API)
13
31
  * 2. Fetch all projects, exact match on git remote
14
- * 3. Multiple matches pick from matches
15
- * 4. Zero matches pick from all projects
32
+ * 3. Multiple matches -> pick from matches
33
+ * 4. Zero matches -> pick from all projects
16
34
  */
17
- async function autoDetectProject(cwd, gitInfo, token) {
18
- const gitRoot = getGitRoot(cwd);
35
+ async function autoDetectProject(gitRoot, gitInfo, token) {
19
36
  // Cache result to .vercel/project.json so subsequent runs skip API calls
20
37
  const cacheResult = (project) => {
21
- if (gitRoot) {
22
- writeLocalVercelProject(gitRoot, project.teamId, project.projectId);
23
- }
38
+ writeLocalVercelProject(gitRoot, project.teamId, project.id);
24
39
  return project;
25
40
  };
26
41
  // --- Fast path: .vercel/project.json ---
27
- if (gitRoot) {
28
- const localProject = readLocalVercelProject(gitRoot);
29
- if (localProject) {
30
- const detail = await getVercelProjectById(token, localProject.projectId, localProject.orgId);
31
- if (detail) {
32
- // p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
33
- return {
34
- teamId: localProject.orgId,
35
- projectId: detail.id,
36
- projectName: detail.name,
37
- repoOwner: detail.link?.org ?? null,
38
- repoName: detail.link?.repo ?? null,
39
- };
40
- }
42
+ const localProject = readLocalVercelProject(gitRoot);
43
+ if (localProject) {
44
+ const detail = await getVercelProjectById(token, localProject.projectId, localProject.orgId);
45
+ if (detail) {
46
+ return {
47
+ teamId: localProject.orgId,
48
+ id: detail.id,
49
+ name: detail.name,
50
+ };
41
51
  }
42
52
  }
43
53
  // --- Fetch all projects ---
@@ -66,10 +76,8 @@ async function autoDetectProject(cwd, gitInfo, token) {
66
76
  spinner?.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
67
77
  return cacheResult({
68
78
  teamId: matches[0].teamId,
69
- projectId: matches[0].id,
70
- projectName: matches[0].name,
71
- repoOwner: matches[0].link?.org ?? null,
72
- repoName: matches[0].link?.repo ?? null,
79
+ id: matches[0].id,
80
+ name: matches[0].name,
73
81
  });
74
82
  }
75
83
  if (matches.length > 1) {
@@ -110,12 +118,11 @@ async function pickFromList(projects, createCtx) {
110
118
  const match = selected;
111
119
  return {
112
120
  teamId: match.teamId,
113
- projectId: match.id,
114
- projectName: match.name,
115
- repoOwner: match.link?.org ?? null,
116
- repoName: match.link?.repo ?? null,
121
+ id: match.id,
122
+ name: match.name,
117
123
  };
118
124
  }
125
+ // --- Create project flow ---
119
126
  async function createProjectFlow(projects, ctx) {
120
127
  if (isAgent)
121
128
  return null;
@@ -151,10 +158,8 @@ async function createProjectFlow(projects, ctx) {
151
158
  p.log.info("Push to git to trigger your first deployment.");
152
159
  return {
153
160
  teamId,
154
- projectId: created.id,
155
- projectName: created.name,
156
- repoOwner: gitRepo.owner,
157
- repoName: gitRepo.name,
161
+ id: created.id,
162
+ name: created.name,
158
163
  };
159
164
  }
160
165
  // Poll until a deployment appears
@@ -167,20 +172,16 @@ async function createProjectFlow(projects, ctx) {
167
172
  pollSpinner.stop("Vercel deployment started.");
168
173
  return {
169
174
  teamId,
170
- projectId: created.id,
171
- projectName: created.name,
172
- repoOwner: gitRepo.owner,
173
- repoName: gitRepo.name,
175
+ id: created.id,
176
+ name: created.name,
174
177
  };
175
178
  }
176
179
  }
177
180
  pollSpinner.stop("Vercel deployment may take a moment to appear.");
178
181
  return {
179
182
  teamId,
180
- projectId: created.id,
181
- projectName: created.name,
182
- repoOwner: gitRepo.owner,
183
- repoName: gitRepo.name,
183
+ id: created.id,
184
+ name: created.name,
184
185
  };
185
186
  }
186
187
  catch (e) {
@@ -188,221 +189,49 @@ async function createProjectFlow(projects, ctx) {
188
189
  return null;
189
190
  }
190
191
  }
191
- // --- Main resolve function ---
192
- export async function resolveVercelUrl(cwd, gitInfo, opts) {
193
- const cli = await ensureVercelCli((msg) => { if (!isAgent)
194
- p.log.step(msg); });
195
- if (!cli.ok) {
196
- if (!isAgent)
197
- p.log.error("Could not install the Vercel CLI automatically.");
198
- if (cli.error) {
199
- if (!isAgent)
200
- p.log.message(pc.dim(cli.error.trim()));
201
- }
202
- if (!isAgent)
203
- p.log.info(`Paste the error above into your AI agent — it can fix this for you.\n\nThen re-run ${pc.cyan("inflight share")}.`);
204
- process.exit(0);
205
- }
206
- const token = await ensureVercelAuth();
207
- if (!token) {
208
- if (!isAgent)
209
- p.log.error("Vercel login failed.");
210
- return null;
211
- }
212
- const project = await autoDetectProject(cwd, gitInfo, token);
213
- if (!project)
214
- return null;
215
- const commitSha = gitInfo.commitShort?.slice(0, 7) ?? null;
216
- // Check if the current git repo matches the Vercel project's linked repo
217
- const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
218
- const repoMatches = project.repoOwner && project.repoName && gitRepo
219
- ? project.repoOwner.toLowerCase() === gitRepo.owner.toLowerCase() &&
220
- project.repoName.toLowerCase() === gitRepo.name.toLowerCase()
221
- : false;
222
- // Only filter by branch if we're in the matching repo — otherwise show all deployments
223
- let deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
224
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
225
- });
226
- let commitDeploy = repoMatches ? deployments.find((d) => d.commitSha === commitSha) : undefined;
227
- // If just pushed, poll until the commit deployment appears
228
- if (!commitDeploy && repoMatches && opts?.justPushed) {
229
- const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
230
- pollSpinner?.start("Waiting for deploy");
231
- for (let i = 0; i < 30; i++) {
232
- await new Promise((r) => setTimeout(r, 2000));
233
- deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
234
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
235
- });
236
- commitDeploy = deployments.find((d) => d.commitSha === commitSha);
237
- if (commitDeploy)
238
- break;
239
- }
240
- if (commitDeploy) {
241
- pollSpinner?.clear();
242
- }
243
- else {
244
- pollSpinner?.stop("No deployment detected yet — Vercel may still be processing.");
245
- }
246
- }
247
- // If we found a commit-specific deployment, handle based on state
248
- if (commitDeploy) {
249
- const commitLabel = pc.bold(commitSha);
250
- const message = commitDeploy.commitMessage
251
- ? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
252
- : "";
253
- if (commitDeploy.state === "ERROR" || commitDeploy.state === "CANCELED") {
254
- if (!isAgent)
255
- p.log.error(`Vercel deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
256
- commitDeploy = undefined;
257
- // Fall through to the picker
258
- }
259
- else if (commitDeploy.state === "BUILDING" ||
260
- commitDeploy.state === "QUEUED" ||
261
- commitDeploy.state === "INITIALIZING") {
262
- // Show what we're waiting on
263
- if (!isAgent)
264
- p.log.info(`Vercel deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state.toLowerCase())}`);
265
- // Poll with a ticking timer for up to ~2 minutes
266
- const buildingUrl = commitDeploy.url;
267
- const startTime = Date.now();
268
- const maxWaitMs = 120_000;
269
- const pollIntervalMs = 3_000;
270
- let resolved = false;
271
- const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
272
- spinner?.start("Waiting for build");
273
- try {
274
- while (Date.now() - startTime < maxWaitMs) {
275
- await new Promise((r) => setTimeout(r, pollIntervalMs));
276
- const freshDeps = await getVercelDeployments(token, project.teamId, project.projectId, {
277
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
278
- });
279
- const fresh = freshDeps.find((d) => d.commitSha === commitSha);
280
- if (!fresh)
281
- break;
282
- if (fresh.state === "READY") {
283
- spinner?.stop("Vercel deployment ready!");
284
- return fresh.url;
285
- }
286
- if (fresh.state === "ERROR" || fresh.state === "CANCELED") {
287
- spinner?.stop(pc.red("Vercel deployment failed."));
288
- if (!isAgent)
289
- p.log.error(`Vercel deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
290
- commitDeploy = undefined;
291
- resolved = true;
292
- break;
293
- }
294
- }
295
- if (!resolved) {
296
- if (isAgent)
297
- return buildingUrl;
298
- spinner?.stop("Still building...");
299
- const action = await p.select({
300
- message: "Vercel deployment is still building.",
301
- options: [
302
- { value: "continue", label: "Use it anyway", hint: "URL may not be ready yet" },
303
- { value: "pick", label: "Select a different deployment" },
304
- ],
305
- });
306
- if (p.isCancel(action)) {
307
- p.cancel("Cancelled.");
308
- process.exit(0);
309
- }
310
- if (action === "continue") {
311
- if (!isAgent)
312
- p.log.info(`Vercel deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
313
- return buildingUrl;
314
- }
315
- commitDeploy = undefined;
316
- // Fall through to picker
317
- }
318
- }
319
- catch (e) {
320
- spinner?.stop("Error checking deployment status.");
321
- if (e instanceof Error)
322
- if (!isAgent)
323
- p.log.message(pc.dim(e.message));
324
- }
325
- }
326
- else {
327
- // READY or any other terminal state — use it
328
- if (!isAgent)
329
- p.log.info(`Vercel deployment for ${commitLabel}${message}`);
330
- return commitDeploy.url;
331
- }
332
- }
333
- // Refresh deployments so the picker shows current states
334
- deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
335
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
336
- });
337
- // Fallback: no commit deployment found — let user pick from recent or paste manually
338
- if (deployments.length === 0) {
339
- if (!isAgent)
340
- p.log.warn("No deployments found. Paste a URL instead.");
341
- return null;
342
- }
343
- const hasWorkingDeployment = deployments.some((d) => d.state !== "ERROR" && d.state !== "CANCELED");
344
- if (!hasWorkingDeployment) {
345
- if (!isAgent)
346
- p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
347
- return null;
348
- }
349
- if (isAgent) {
350
- const ready = deployments.find((d) => d.state === "READY");
351
- return ready?.url ?? deployments[0]?.url ?? null;
352
- }
353
- const maxBranchPick = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
354
- const selected = await p.select({
355
- message: "No deployment found for current commit. Select one:",
356
- options: [
357
- ...deployments.map((d) => {
358
- const isFailed = d.state === "ERROR" || d.state === "CANCELED";
359
- const branch = (d.branch ?? "unknown").padEnd(maxBranchPick);
360
- const ago = timeAgo(d.createdAt).padEnd(8);
361
- const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
362
- const msg = truncate(firstLine, 55);
363
- if (isFailed) {
364
- return {
365
- value: d.url,
366
- label: pc.dim(`${branch} ${ago} ${pc.red("(failed)")} ${msg}`),
367
- };
368
- }
369
- const state = d.state !== "READY" ? ` ${pc.yellow(`(${d.state.toLowerCase()})`)}` : "";
370
- return { value: d.url, label: `${branch} ${ago}${state} ${pc.dim(msg)}` };
371
- }),
372
- { value: "manual", label: "Paste a URL manually" },
373
- ],
374
- });
375
- if (p.isCancel(selected)) {
376
- p.cancel("Cancelled.");
377
- process.exit(0);
378
- }
379
- if (selected === "manual")
380
- return null;
381
- // Warn if the user picked a failed deployment
382
- const pickedDeploy = deployments.find((d) => d.url === selected);
383
- if (pickedDeploy && (pickedDeploy.state === "ERROR" || pickedDeploy.state === "CANCELED")) {
384
- if (isAgent)
385
- return null;
386
- p.log.warn("This deployment failed — the URL may not load.");
387
- const confirm = await p.confirm({ message: "Use it anyway?" });
388
- if (p.isCancel(confirm) || !confirm)
192
+ // --- Adapter ---
193
+ const vercelAdapter = {
194
+ name: "Vercel",
195
+ async ensureCli(log) {
196
+ return ensureVercelCli(log);
197
+ },
198
+ async ensureAuth() {
199
+ return ensureVercelAuth();
200
+ },
201
+ async detectProject(gitRoot, gitInfo, token) {
202
+ return autoDetectProject(gitRoot, gitInfo, token);
203
+ },
204
+ async getDeployments(token, project, opts) {
205
+ const raw = await getVercelDeployments(token, project.teamId, project.id, {
206
+ branch: opts?.branch,
207
+ sha: opts?.sha,
208
+ });
209
+ return raw.map((d) => ({
210
+ id: d.id,
211
+ url: d.url,
212
+ state: normalizeVercelState(d.state),
213
+ branch: d.branch,
214
+ commitSha: d.commitSha,
215
+ commitMessage: d.commitMessage,
216
+ createdAt: d.createdAt,
217
+ }));
218
+ },
219
+ async getDeploymentById(token, project, deploymentId) {
220
+ const raw = await getVercelDeploymentById(token, deploymentId, project.teamId);
221
+ if (!raw)
389
222
  return null;
390
- }
391
- return selected;
392
- }
393
- function truncate(str, max) {
394
- return str.length > max ? str.slice(0, max - 1) + "…" : str;
395
- }
396
- function timeAgo(timestamp) {
397
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
398
- if (seconds < 60)
399
- return "now";
400
- const minutes = Math.floor(seconds / 60);
401
- if (minutes < 60)
402
- return `${minutes}m ago`;
403
- const hours = Math.floor(minutes / 60);
404
- if (hours < 24)
405
- return `${hours}h ago`;
406
- const days = Math.floor(hours / 24);
407
- return `${days}d ago`;
223
+ return {
224
+ id: raw.id,
225
+ url: raw.url,
226
+ state: normalizeVercelState(raw.state),
227
+ branch: raw.branch,
228
+ commitSha: raw.commitSha,
229
+ commitMessage: raw.commitMessage,
230
+ createdAt: raw.createdAt,
231
+ };
232
+ },
233
+ };
234
+ // --- Public API ---
235
+ export async function resolveVercelUrl(gitRoot, gitInfo, opts) {
236
+ return resolveDeploymentUrl(vercelAdapter, gitRoot, gitInfo, opts);
408
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inflight-cli",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Get feedback directly on your staging URL",
5
5
  "bin": {
6
6
  "inflight": "dist/index.js",