inflight-cli 2.1.6 → 2.1.7

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,10 +1,27 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
- import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead } from "../lib/git.js";
4
+ import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead, } from "../lib/git.js";
5
5
  import open from "open";
6
6
  import { providers } from "../providers/index.js";
7
- import { apiGetMe, apiCreateVersion } from "../lib/api.js";
7
+ import { apiGetMe, apiCreateVersion, apiGetRecentProjects } from "../lib/api.js";
8
+ function formatRelativeTime(timestampMs) {
9
+ const seconds = Math.floor((Date.now() - timestampMs) / 1000);
10
+ if (seconds < 60)
11
+ return "just now";
12
+ const minutes = Math.floor(seconds / 60);
13
+ if (minutes < 60)
14
+ return `${minutes}m ago`;
15
+ const hours = Math.floor(minutes / 60);
16
+ if (hours < 24)
17
+ return `${hours}h ago`;
18
+ const days = Math.floor(hours / 24);
19
+ if (days === 1)
20
+ return "yesterday";
21
+ if (days < 7)
22
+ return `${days}d ago`;
23
+ return new Date(timestampMs).toLocaleDateString("en-US", { month: "short", day: "numeric" });
24
+ }
8
25
  /**
9
26
  * Checks local git state and prompts the user to commit/push if needed.
10
27
  * Returns { justPushed: true } if changes were pushed, { justPushed: false } otherwise.
@@ -303,16 +320,75 @@ export async function shareCommand(opts = {}) {
303
320
  if (!stagingUrl.startsWith("http")) {
304
321
  stagingUrl = `https://${stagingUrl}`;
305
322
  }
323
+ // ── Step 4: Project selection ──
324
+ let selectedProjectId;
325
+ let overrideVersionId;
326
+ const { projects: recentProjects } = await apiGetRecentProjects(auth.apiKey, workspaceId).catch(() => ({
327
+ projects: [],
328
+ }));
329
+ if (recentProjects.length > 0) {
330
+ const projectChoice = await p.select({
331
+ message: "Add to an existing version or start fresh?",
332
+ options: [
333
+ ...recentProjects.map((proj) => ({
334
+ value: proj.projectId,
335
+ label: `"${proj.latestVersion.title}"`,
336
+ hint: `created ${formatRelativeTime(proj.latestVersion.createdAt)}`,
337
+ })),
338
+ { value: "new", label: "Start fresh" },
339
+ ],
340
+ });
341
+ if (p.isCancel(projectChoice)) {
342
+ p.cancel("Cancelled.");
343
+ process.exit(0);
344
+ }
345
+ if (projectChoice !== "new") {
346
+ selectedProjectId = projectChoice;
347
+ // Check for override opportunity — only when latest version has no feedback
348
+ const selectedProject = recentProjects.find((proj) => proj.projectId === selectedProjectId);
349
+ if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
350
+ const overrideChoice = await p.select({
351
+ message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
352
+ options: [
353
+ {
354
+ value: "override",
355
+ label: "Update its staging URL",
356
+ hint: `replace with ${new URL(stagingUrl).hostname}`,
357
+ },
358
+ {
359
+ value: "new_version",
360
+ label: "Add a new version",
361
+ hint: "keep both in version history",
362
+ },
363
+ ],
364
+ });
365
+ if (p.isCancel(overrideChoice)) {
366
+ p.cancel("Cancelled.");
367
+ process.exit(0);
368
+ }
369
+ if (overrideChoice === "override") {
370
+ overrideVersionId = selectedProject.latestVersion.id;
371
+ }
372
+ }
373
+ }
374
+ }
306
375
  await apiCreateVersion({
307
376
  apiKey: auth.apiKey,
308
377
  workspaceId,
309
378
  stagingUrl,
310
379
  gitInfo,
380
+ ...(selectedProjectId && { projectId: selectedProjectId }),
381
+ ...(overrideVersionId && { overrideVersionId }),
311
382
  }).catch((e) => {
312
383
  p.log.error(e.message);
313
384
  process.exit(1);
314
385
  });
315
386
  p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
316
- p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
387
+ if (overrideVersionId) {
388
+ p.outro(pc.green("✓ Staging URL updated") + " — opening in browser...");
389
+ }
390
+ else {
391
+ p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
392
+ }
317
393
  await open(stagingUrl);
318
394
  }
package/dist/lib/api.d.ts CHANGED
@@ -9,16 +9,31 @@ export interface CreateVersionResult {
9
9
  versionId: string;
10
10
  inflightUrl: string;
11
11
  }
12
+ export interface RecentProject {
13
+ projectId: string;
14
+ latestVersion: {
15
+ id: string;
16
+ title: string;
17
+ stagingUrl: string | null;
18
+ commentCount: number;
19
+ createdAt: number;
20
+ };
21
+ }
12
22
  export declare function apiGetMe(apiKey: string): Promise<{
13
23
  name: string | null;
14
24
  email: string | null;
15
25
  workspaces: Workspace[];
16
26
  }>;
27
+ export declare function apiGetRecentProjects(apiKey: string, workspaceId: string): Promise<{
28
+ projects: RecentProject[];
29
+ }>;
17
30
  export declare function apiCreateVersion(opts: {
18
31
  apiKey: string;
19
32
  workspaceId: string;
20
33
  stagingUrl: string;
21
34
  gitInfo: GitInfo;
35
+ projectId?: string;
36
+ overrideVersionId?: string;
22
37
  }): Promise<CreateVersionResult>;
23
38
  export interface WidgetLocationResult {
24
39
  file: string | null;
package/dist/lib/api.js CHANGED
@@ -7,6 +7,16 @@ export async function apiGetMe(apiKey) {
7
7
  throw new Error("Invalid API key");
8
8
  return res.json();
9
9
  }
10
+ export async function apiGetRecentProjects(apiKey, workspaceId) {
11
+ const res = await fetch(`${API_URL}/api/cli/projects/recent?workspaceId=${encodeURIComponent(workspaceId)}`, {
12
+ headers: { Authorization: `Bearer ${apiKey}` },
13
+ });
14
+ if (!res.ok) {
15
+ const body = await res.json().catch(() => ({ error: res.statusText }));
16
+ throw new Error(body.error ?? `API error ${res.status}`);
17
+ }
18
+ return res.json();
19
+ }
10
20
  export async function apiCreateVersion(opts) {
11
21
  const res = await fetch(`${API_URL}/api/cli/version/create`, {
12
22
  method: "POST",
@@ -18,6 +28,8 @@ export async function apiCreateVersion(opts) {
18
28
  workspaceId: opts.workspaceId,
19
29
  stagingUrl: opts.stagingUrl,
20
30
  gitInfo: opts.gitInfo,
31
+ ...(opts.projectId && { projectId: opts.projectId }),
32
+ ...(opts.overrideVersionId && { overrideVersionId: opts.overrideVersionId }),
21
33
  }),
22
34
  });
23
35
  if (!res.ok) {
@@ -3,7 +3,7 @@ import pc from "picocolors";
3
3
  import { execSync } from "child_process";
4
4
  import { parseGitRepo, getGitRoot } from "../lib/git.js";
5
5
  import { writeVercelConfig } from "../lib/config.js";
6
- import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectDetail, fetchAllProjectsWithLinks, matchProjectsByRepo, createVercelProject, getBranchAlias, getRecentDeployments, } from "../lib/vercel.js";
6
+ import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectDetail, fetchAllProjectsWithLinks, matchProjectsByRepo, createVercelProject, getRecentDeployments, } from "../lib/vercel.js";
7
7
  // --- Auto-detection ---
8
8
  /**
9
9
  * Auto-detect the Vercel project for the current git repo.
@@ -29,7 +29,7 @@ async function autoDetectProject(cwd, gitInfo, token) {
29
29
  if (localProject) {
30
30
  const detail = await getVercelProjectDetail(token, localProject.projectId, localProject.orgId);
31
31
  if (detail) {
32
- p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
32
+ // p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
33
33
  return { teamId: localProject.orgId, projectId: detail.id, projectName: detail.name };
34
34
  }
35
35
  }
@@ -196,61 +196,57 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
196
196
  const project = await autoDetectProject(cwd, gitInfo, token);
197
197
  if (!project)
198
198
  return null;
199
- // Fetch branch alias — if just pushed, poll until it appears
200
- let branchAlias = await getBranchAlias(token, project.teamId, project.projectId, gitInfo.branch);
201
- if (!branchAlias && opts?.justPushed) {
199
+ const commitSha = gitInfo.commitShort?.slice(0, 7) ?? null;
200
+ // Try to find a deployment matching the current commit
201
+ let deployments = await getRecentDeployments(token, project.teamId, project.projectId, {
202
+ branch: gitInfo.branch ?? undefined,
203
+ });
204
+ let commitDeploy = deployments.find((d) => d.commitSha === commitSha);
205
+ // If just pushed, poll until the commit deployment appears
206
+ if (!commitDeploy && opts?.justPushed) {
202
207
  const pollSpinner = p.spinner();
203
208
  pollSpinner.start("Waiting for Vercel deployment...");
204
209
  for (let i = 0; i < 30; i++) {
205
210
  await new Promise((r) => setTimeout(r, 2000));
206
- branchAlias = await getBranchAlias(token, project.teamId, project.projectId, gitInfo.branch);
207
- if (branchAlias)
211
+ deployments = await getRecentDeployments(token, project.teamId, project.projectId, {
212
+ branch: gitInfo.branch ?? undefined,
213
+ });
214
+ commitDeploy = deployments.find((d) => d.commitSha === commitSha);
215
+ if (commitDeploy)
208
216
  break;
209
217
  }
210
- if (branchAlias) {
218
+ if (commitDeploy) {
211
219
  pollSpinner.stop("Deployment found!");
212
220
  }
213
221
  else {
214
222
  pollSpinner.stop("Deployment is still building...");
215
223
  }
216
224
  }
217
- if (branchAlias) {
218
- const stateLabel = branchAlias.state !== "READY" ? ` ${pc.yellow(`(${branchAlias.state.toLowerCase()})`)}` : "";
219
- p.log.info(`Branch preview (auto-updates with each push):\n → ${pc.cyan(branchAlias.url)}${stateLabel}`);
220
- const choice = await p.select({
221
- message: "Use this URL, or pick a specific deployment?",
222
- options: [
223
- { value: "branch", label: "Use branch preview (recommended)" },
224
- { value: "recent", label: "Pick a specific deployment" },
225
- { value: "manual", label: "Paste a URL manually" },
226
- ],
227
- });
228
- if (p.isCancel(choice)) {
229
- p.cancel("Cancelled.");
230
- process.exit(0);
231
- }
232
- if (choice === "branch")
233
- return branchAlias.url;
234
- if (choice === "manual")
235
- return null;
225
+ // If we found a commit-specific deployment, use it automatically
226
+ if (commitDeploy) {
227
+ const stateLabel = commitDeploy.state !== "READY" ? ` ${pc.yellow(`(${commitDeploy.state.toLowerCase()})`)}` : "";
228
+ const message = commitDeploy.commitMessage
229
+ ? pc.dim(` ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
230
+ : "";
231
+ p.log.info(`Deployment for ${pc.bold(commitSha)}${message}:\n → ${pc.cyan(commitDeploy.url)}${stateLabel}`);
232
+ return commitDeploy.url;
236
233
  }
237
- // Show recent deployments
238
- const recent = await getRecentDeployments(token, project.teamId, project.projectId);
239
- if (recent.length === 0) {
234
+ // Fallback: no commit deployment found — let user pick from recent or paste manually
235
+ if (deployments.length === 0) {
240
236
  p.log.warn("No deployments found. Paste a URL instead.");
241
237
  return null;
242
238
  }
243
- const maxBranch = Math.max(...recent.map((d) => (d.branch ?? "unknown").length));
239
+ const maxBranch = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
244
240
  const selected = await p.select({
245
- message: "Select a specific deployment",
241
+ message: "No deployment found for current commit. Select one:",
246
242
  options: [
247
- ...recent.map((d) => {
243
+ ...deployments.map((d) => {
248
244
  const branch = (d.branch ?? "unknown").padEnd(maxBranch);
249
245
  const ago = timeAgo(d.createdAt).padEnd(8);
250
246
  const state = d.state !== "READY" ? ` ${pc.yellow(`(${d.state.toLowerCase()})`)}` : "";
251
247
  const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
252
- const message = truncate(firstLine, 55);
253
- return { value: d.url, label: `${branch} ${ago}${state} ${pc.dim(message)}` };
248
+ const msg = truncate(firstLine, 55);
249
+ return { value: d.url, label: `${branch} ${ago}${state} ${pc.dim(msg)}` };
254
250
  }),
255
251
  { value: "manual", label: "Paste a URL manually" },
256
252
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inflight-cli",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "Get feedback directly on your staging URL",
5
5
  "bin": {
6
6
  "inflight": "dist/index.js",