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,40 +1,48 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
- import { parseGitRepo, getGitRoot } from "../lib/git.js";
3
+ import { parseGitRepo } from "../lib/git.js";
4
4
  import { isAgent } from "../lib/agent.js";
5
- import { writeNetlifyConfig } from "../lib/config.js";
6
- import { ensureNetlifyCli, ensureNetlifyAuth, readLocalNetlifySite, writeLocalNetlifySite, getNetlifySiteById, getNetlifySites, getNetlifyDeploys, matchSitesByRepo, } from "../lib/netlify.js";
5
+ import { ensureNetlifyCli, ensureNetlifyAuth, readLocalNetlifySite, writeLocalNetlifySite, getNetlifySiteById, getNetlifySites, getNetlifyDeploys, getNetlifyDeployById, matchSitesByRepo, } from "../lib/netlify.js";
6
+ import { resolveDeploymentUrl } from "./shared.js";
7
+ // --- Netlify state mapping ---
8
+ function normalizeNetlifyState(state) {
9
+ switch (state) {
10
+ case "ready":
11
+ return "ready";
12
+ case "error":
13
+ return "error";
14
+ case "enqueued":
15
+ return "queued";
16
+ default:
17
+ // building, uploading, preparing, processing, etc.
18
+ return "building";
19
+ }
20
+ }
7
21
  // --- Auto-detection ---
8
22
  /**
9
23
  * Auto-detect the Netlify site for the current git repo.
10
24
  *
11
25
  * 1. `.netlify/state.json` at git root (instant, no API)
12
26
  * 2. Fetch all sites, exact match on git remote
13
- * 3. Multiple matches pick from matches
14
- * 4. Zero matches pick from all sites
27
+ * 3. Multiple matches -> pick from matches
28
+ * 4. Zero matches -> pick from all sites
15
29
  */
16
- async function autoDetectSite(cwd, gitInfo, token) {
17
- const gitRoot = getGitRoot(cwd);
30
+ async function autoDetectSite(gitRoot, gitInfo, token) {
18
31
  // Cache result to .netlify/state.json so subsequent runs skip API calls
19
- const cacheResult = (site) => {
20
- if (gitRoot) {
21
- writeLocalNetlifySite(gitRoot, site.siteId);
22
- }
23
- return site;
32
+ const cacheResult = (project) => {
33
+ writeLocalNetlifySite(gitRoot, project.id);
34
+ return project;
24
35
  };
25
36
  // --- Fast path: .netlify/state.json ---
26
- if (gitRoot) {
27
- const localSite = readLocalNetlifySite(gitRoot);
28
- if (localSite) {
29
- const detail = await getNetlifySiteById(token, localSite.siteId);
30
- if (detail) {
31
- return {
32
- siteId: detail.id,
33
- siteName: detail.name,
34
- teamSlug: detail.account_slug,
35
- repoPath: detail.build_settings?.repo_path ?? null,
36
- };
37
- }
37
+ const localSite = readLocalNetlifySite(gitRoot);
38
+ if (localSite) {
39
+ const detail = await getNetlifySiteById(token, localSite.siteId);
40
+ if (detail) {
41
+ return {
42
+ id: detail.id,
43
+ name: detail.name,
44
+ teamId: detail.account_slug,
45
+ };
38
46
  }
39
47
  }
40
48
  // --- Fetch all sites ---
@@ -61,10 +69,9 @@ async function autoDetectSite(cwd, gitInfo, token) {
61
69
  if (matches.length === 1) {
62
70
  spinner?.stop(`Detected Netlify site: ${pc.bold(matches[0].name)}`);
63
71
  return cacheResult({
64
- siteId: matches[0].id,
65
- siteName: matches[0].name,
66
- teamSlug: matches[0].account_slug,
67
- repoPath: matches[0].build_settings?.repo_path ?? null,
72
+ id: matches[0].id,
73
+ name: matches[0].name,
74
+ teamId: matches[0].account_slug,
68
75
  });
69
76
  }
70
77
  if (matches.length > 1) {
@@ -95,251 +102,47 @@ async function pickFromList(sites) {
95
102
  }
96
103
  const match = selected;
97
104
  return {
98
- siteId: match.id,
99
- siteName: match.name,
100
- teamSlug: match.account_slug,
101
- repoPath: match.build_settings?.repo_path ?? null,
102
- };
103
- }
104
- // --- Manual picker (used by `inflight netlify` command) ---
105
- export async function pickNetlifySite(token) {
106
- const allSites = await getNetlifySites(token);
107
- if (allSites.length === 0) {
108
- if (!isAgent)
109
- p.log.error("No Netlify sites found.");
110
- return null;
111
- }
112
- if (isAgent)
113
- return null;
114
- const selected = await p.select({
115
- message: "Select a Netlify site",
116
- options: allSites.map((s) => ({
117
- value: s,
118
- label: s.account_name ? `${s.name} ${pc.dim(`(${s.account_name})`)}` : s.name,
119
- })),
120
- });
121
- if (p.isCancel(selected)) {
122
- p.cancel("Cancelled.");
123
- return null;
124
- }
125
- const match = selected;
126
- const config = {
127
- siteId: match.id,
128
- siteName: match.name,
129
- teamSlug: match.account_slug,
105
+ id: match.id,
106
+ name: match.name,
107
+ teamId: match.account_slug,
130
108
  };
131
- writeNetlifyConfig(config);
132
- return config;
133
109
  }
134
- // --- Main resolve function ---
135
- export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
136
- const cli = await ensureNetlifyCli((msg) => { if (!isAgent)
137
- p.log.step(msg); });
138
- if (!cli.ok) {
139
- if (!isAgent) {
140
- p.log.error("Could not install the Netlify CLI automatically.");
141
- if (cli.error) {
142
- p.log.message(pc.dim(cli.error.trim()));
143
- }
144
- 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")}.`);
145
- }
146
- process.exit(0);
147
- }
148
- const token = await ensureNetlifyAuth();
149
- if (!token) {
150
- if (!isAgent)
151
- p.log.error("Netlify login failed.");
152
- return null;
153
- }
154
- const site = await autoDetectSite(cwd, gitInfo, token);
155
- if (!site)
156
- return null;
157
- const commitSha = gitInfo.commitShort?.slice(0, 7) ?? null;
158
- // Check if the current git repo matches the Netlify site's linked repo
159
- const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
160
- const localRepoPath = gitRepo ? `${gitRepo.owner}/${gitRepo.name}`.toLowerCase() : null;
161
- const repoMatches = site.repoPath && localRepoPath ? site.repoPath.toLowerCase() === localRepoPath : false;
162
- // Only filter by branch if we're in the matching repo — otherwise show all deploys
163
- let deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
164
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
165
- });
166
- let commitDeploy = repoMatches ? deploys.find((d) => d.commitRef === commitSha) : undefined;
167
- // If just pushed, poll until the commit deployment appears
168
- if (!commitDeploy && repoMatches && opts?.justPushed) {
169
- const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
170
- pollSpinner?.start("Waiting for deploy");
171
- for (let i = 0; i < 30; i++) {
172
- await new Promise((r) => setTimeout(r, 2000));
173
- deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
174
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
175
- });
176
- commitDeploy = deploys.find((d) => d.commitRef === commitSha);
177
- if (commitDeploy)
178
- break;
179
- }
180
- if (commitDeploy) {
181
- pollSpinner?.clear();
182
- }
183
- else {
184
- pollSpinner?.stop("No deployment detected yet — Netlify may still be processing.");
185
- }
186
- }
187
- // If we found a commit-specific deployment, handle based on state
188
- if (commitDeploy) {
189
- const commitLabel = pc.bold(commitSha);
190
- const message = commitDeploy.commitMessage
191
- ? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
192
- : "";
193
- if (commitDeploy.state === "error") {
194
- if (!isAgent)
195
- p.log.error(`Netlify deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
196
- commitDeploy = undefined;
197
- // Fall through to the picker
198
- }
199
- else if (commitDeploy.state !== "ready") {
200
- // Any non-ready, non-error state (building, enqueued, uploading, preparing, processing, etc.)
201
- if (!isAgent)
202
- p.log.info(`Netlify deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state)}`);
203
- const buildingUrl = commitDeploy.deploySslUrl;
204
- const startTime = Date.now();
205
- const maxWaitMs = 120_000;
206
- const pollIntervalMs = 5_000;
207
- let resolved = false;
208
- const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
209
- spinner?.start("Waiting for build");
210
- try {
211
- while (Date.now() - startTime < maxWaitMs) {
212
- await new Promise((r) => setTimeout(r, pollIntervalMs));
213
- const freshDeps = await getNetlifyDeploys(token, site.siteId, site.siteName, {
214
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
215
- });
216
- const fresh = freshDeps.find((d) => d.commitRef === commitSha);
217
- if (!fresh)
218
- break;
219
- if (fresh.state === "ready") {
220
- spinner?.stop("Netlify deployment ready!");
221
- if (!isAgent)
222
- p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.deploySslUrl)}`);
223
- return fresh.deploySslUrl;
224
- }
225
- if (fresh.state === "error") {
226
- spinner?.stop(pc.red("Netlify deployment failed."));
227
- if (!isAgent)
228
- p.log.error(`Netlify deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
229
- commitDeploy = undefined;
230
- resolved = true;
231
- break;
232
- }
233
- }
234
- if (!resolved) {
235
- spinner?.stop("Still building...");
236
- if (isAgent)
237
- return buildingUrl;
238
- const action = await p.select({
239
- message: "Netlify deployment is still building.",
240
- options: [
241
- { value: "continue", label: "Use it anyway", hint: "URL may not be ready yet" },
242
- { value: "pick", label: "Select a different deployment" },
243
- ],
244
- });
245
- if (p.isCancel(action)) {
246
- p.cancel("Cancelled.");
247
- process.exit(0);
248
- }
249
- if (action === "continue") {
250
- p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
251
- return buildingUrl;
252
- }
253
- commitDeploy = undefined;
254
- // Fall through to picker
255
- }
256
- }
257
- catch (e) {
258
- spinner?.stop("Error checking deployment status.");
259
- if (!isAgent && e instanceof Error)
260
- p.log.message(pc.dim(e.message));
261
- }
262
- }
263
- else {
264
- // ready — use it
265
- if (!isAgent)
266
- p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(commitDeploy.deploySslUrl)}`);
267
- return commitDeploy.deploySslUrl;
268
- }
269
- }
270
- // Refresh deployments so the picker shows current states
271
- deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
272
- branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
273
- });
274
- // Fallback: no commit deployment — let user pick from recent or paste manually
275
- if (deploys.length === 0) {
276
- if (!isAgent)
277
- p.log.warn("No deployments found. Paste a URL instead.");
278
- return null;
279
- }
280
- const hasWorkingDeploy = deploys.some((d) => d.state !== "error");
281
- if (!hasWorkingDeploy) {
282
- if (!isAgent)
283
- p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
284
- return null;
285
- }
286
- if (isAgent) {
287
- const ready = deploys.find((d) => d.state === "ready");
288
- return ready?.deploySslUrl ?? deploys[0]?.deploySslUrl ?? null;
289
- }
290
- const maxBranch = Math.max(...deploys.map((d) => (d.branch ?? "unknown").length));
291
- const selected = await p.select({
292
- message: "No deployment found for current commit. Select one:",
293
- options: [
294
- ...deploys.map((d) => {
295
- const isFailed = d.state === "error";
296
- const branch = (d.branch ?? "unknown").padEnd(maxBranch);
297
- const ago = timeAgo(d.createdAt).padEnd(8);
298
- const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
299
- const msg = truncate(firstLine, 55);
300
- if (isFailed) {
301
- return {
302
- value: d.deploySslUrl,
303
- label: pc.dim(`${branch} ${ago} ${pc.red("(failed)")} ${msg}`),
304
- };
305
- }
306
- const state = d.state !== "ready" ? ` ${pc.yellow(`(${d.state})`)}` : "";
307
- return { value: d.deploySslUrl, label: `${branch} ${ago}${state} ${pc.dim(msg)}` };
308
- }),
309
- { value: "manual", label: "Paste a URL manually" },
310
- ],
311
- });
312
- if (p.isCancel(selected)) {
313
- p.cancel("Cancelled.");
314
- process.exit(0);
315
- }
316
- if (selected === "manual")
317
- return null;
318
- // Warn if the user picked a failed deployment
319
- const pickedDeploy = deploys.find((d) => d.deploySslUrl === selected);
320
- if (pickedDeploy && pickedDeploy.state === "error") {
321
- if (isAgent)
110
+ // --- Adapter ---
111
+ const netlifyAdapter = {
112
+ name: "Netlify",
113
+ ensureCli: (log) => ensureNetlifyCli(log),
114
+ ensureAuth: () => ensureNetlifyAuth(),
115
+ detectProject: (gitRoot, gitInfo, token) => autoDetectSite(gitRoot, gitInfo, token),
116
+ getDeployments: async (token, project, opts) => {
117
+ const raw = await getNetlifyDeploys(token, project.id, project.name, {
118
+ branch: opts?.branch,
119
+ });
120
+ return raw.map((d) => ({
121
+ id: d.id,
122
+ url: d.deploySslUrl,
123
+ state: normalizeNetlifyState(d.state),
124
+ branch: d.branch,
125
+ commitSha: d.commitRef,
126
+ commitMessage: d.commitMessage,
127
+ createdAt: d.createdAt,
128
+ }));
129
+ },
130
+ getDeploymentById: async (token, project, deployId) => {
131
+ const raw = await getNetlifyDeployById(token, deployId, project.name);
132
+ if (!raw)
322
133
  return null;
323
- p.log.warn("This deployment failed — the URL may not load.");
324
- const confirm = await p.confirm({ message: "Use it anyway?" });
325
- if (p.isCancel(confirm) || !confirm)
326
- return null;
327
- }
328
- return selected;
329
- }
330
- function truncate(str, max) {
331
- return str.length > max ? str.slice(0, max - 1) + "…" : str;
332
- }
333
- function timeAgo(timestamp) {
334
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
335
- if (seconds < 60)
336
- return "now";
337
- const minutes = Math.floor(seconds / 60);
338
- if (minutes < 60)
339
- return `${minutes}m ago`;
340
- const hours = Math.floor(minutes / 60);
341
- if (hours < 24)
342
- return `${hours}h ago`;
343
- const days = Math.floor(hours / 24);
344
- return `${days}d ago`;
134
+ return {
135
+ id: raw.id,
136
+ url: raw.deploySslUrl,
137
+ state: normalizeNetlifyState(raw.state),
138
+ branch: raw.branch,
139
+ commitSha: raw.commitRef,
140
+ commitMessage: raw.commitMessage,
141
+ createdAt: raw.createdAt,
142
+ };
143
+ },
144
+ };
145
+ // --- Main resolve function ---
146
+ export async function resolveNetlifyUrl(gitRoot, gitInfo, opts) {
147
+ return resolveDeploymentUrl(netlifyAdapter, gitRoot, gitInfo, opts);
345
148
  }
@@ -0,0 +1,39 @@
1
+ import type { GitInfo } from "../lib/git.js";
2
+ import type { ProviderResolveOptions } from "./index.js";
3
+ export interface Deployment {
4
+ id: string;
5
+ url: string;
6
+ /** Normalized: "ready" | "building" | "error" | "queued" */
7
+ state: "ready" | "building" | "error" | "queued";
8
+ branch: string | null;
9
+ commitSha: string | null;
10
+ commitMessage: string | null;
11
+ createdAt: number;
12
+ }
13
+ export interface DetectedProject {
14
+ id: string;
15
+ name: string;
16
+ /** teamId for Vercel, account_slug for Netlify — passed through to API calls */
17
+ teamId: string;
18
+ }
19
+ export interface ProviderAdapter {
20
+ /** Display name: "Vercel" | "Netlify" */
21
+ name: string;
22
+ /** Ensure CLI is installed, install if missing */
23
+ ensureCli(log?: (msg: string) => void): Promise<{
24
+ ok: boolean;
25
+ error?: string;
26
+ }>;
27
+ /** Ensure authenticated, return token or null */
28
+ ensureAuth(): Promise<string | null>;
29
+ /** Auto-detect or prompt for project. All provider-specific detection/picker logic lives here. */
30
+ detectProject(gitRoot: string, gitInfo: GitInfo, token: string): Promise<DetectedProject | null>;
31
+ /** Fetch deployments. Adapter normalizes states to Deployment format. */
32
+ getDeployments(token: string, project: DetectedProject, opts?: {
33
+ branch?: string;
34
+ sha?: string;
35
+ }): Promise<Deployment[]>;
36
+ /** Fetch a single deployment by ID. Used for polling build state. */
37
+ getDeploymentById(token: string, project: DetectedProject, deploymentId: string): Promise<Deployment | null>;
38
+ }
39
+ export declare function resolveDeploymentUrl(adapter: ProviderAdapter, gitRoot: string, gitInfo: GitInfo, opts?: ProviderResolveOptions): Promise<string | null>;
@@ -0,0 +1,249 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { isAgent, agentError, agentActionRequired } from "../lib/agent.js";
4
+ // --- Shared resolution logic ---
5
+ export async function resolveDeploymentUrl(adapter, gitRoot, gitInfo, opts) {
6
+ // 1. Ensure CLI
7
+ const cli = await adapter.ensureCli((msg) => {
8
+ if (!isAgent)
9
+ p.log.step(msg);
10
+ });
11
+ if (!cli.ok) {
12
+ if (isAgent) {
13
+ agentError({
14
+ type: "cli_install_failed",
15
+ message: `Could not install the ${adapter.name} CLI.`,
16
+ ...(cli.error && { detail: cli.error.trim() }),
17
+ suggestion: `Install the ${adapter.name} CLI globally, fix any errors, then re-run \`inflight share\`.`,
18
+ });
19
+ }
20
+ p.log.error(`Could not install the ${adapter.name} CLI automatically.`);
21
+ if (cli.error)
22
+ p.log.message(pc.dim(cli.error.trim()));
23
+ 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")}.`);
24
+ process.exit(0);
25
+ }
26
+ // 2. Ensure auth
27
+ const token = await adapter.ensureAuth();
28
+ if (!token) {
29
+ if (isAgent) {
30
+ agentError({
31
+ type: "auth_failed",
32
+ message: `${adapter.name} login failed.`,
33
+ suggestion: `Run \`${adapter.name.toLowerCase()} login\` to authenticate, then re-run \`inflight share\`.`,
34
+ });
35
+ }
36
+ p.log.error(`${adapter.name} login failed.`);
37
+ return null;
38
+ }
39
+ // 3. Detect project
40
+ const project = await adapter.detectProject(gitRoot, gitInfo, token);
41
+ if (!project) {
42
+ if (isAgent) {
43
+ agentError({
44
+ type: "no_project",
45
+ message: `Could not detect a ${adapter.name} project for this repo.`,
46
+ suggestion: `Make sure you're in the right directory. Link your repo to a ${adapter.name} project, or re-run with: \`inflight share --url <staging-url>\` to skip provider detection.`,
47
+ });
48
+ }
49
+ return null;
50
+ }
51
+ const branch = gitInfo.branch ?? undefined;
52
+ // 4. Fetch deployments for current branch, filtered by SHA if supported
53
+ let deployments = await adapter.getDeployments(token, project, { branch, sha: gitInfo.commitFull });
54
+ // 5. Try SHA match
55
+ let currentDeployment = deployments.find((d) => d.commitSha === gitInfo.commitFull);
56
+ // 6. If just pushed, poll for the new deployment to appear
57
+ if (!currentDeployment && opts?.justPushed) {
58
+ const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
59
+ pollSpinner?.start("Waiting for deploy");
60
+ for (let i = 0; i < 30; i++) {
61
+ await new Promise((r) => setTimeout(r, 2000));
62
+ const results = await adapter.getDeployments(token, project, { branch, sha: gitInfo.commitFull });
63
+ currentDeployment = results.find((d) => d.commitSha === gitInfo.commitFull);
64
+ if (currentDeployment)
65
+ break;
66
+ }
67
+ if (currentDeployment) {
68
+ pollSpinner?.clear();
69
+ }
70
+ else {
71
+ pollSpinner?.stop(`No deployment detected yet — ${adapter.name} may still be processing.`);
72
+ }
73
+ }
74
+ // 7. Handle commit-specific deployment states
75
+ if (currentDeployment) {
76
+ // Agent mode: return URL immediately — don't wait for build/error
77
+ if (isAgent)
78
+ return currentDeployment.url;
79
+ const commitLabel = pc.bold(gitInfo.commitShort);
80
+ const message = currentDeployment.commitMessage
81
+ ? pc.dim(` — ${truncate(currentDeployment.commitMessage.split("\n")[0], 50)}`)
82
+ : "";
83
+ switch (currentDeployment.state) {
84
+ case "ready":
85
+ p.log.info(`${adapter.name} deployment for ${commitLabel}${message}`);
86
+ return currentDeployment.url;
87
+ case "error":
88
+ p.log.error(`${adapter.name} deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
89
+ currentDeployment = undefined;
90
+ // Fall through to picker
91
+ break;
92
+ case "building":
93
+ case "queued": {
94
+ p.log.info(`${adapter.name} deployment for ${commitLabel}${message} — ${pc.yellow(currentDeployment.state)}`);
95
+ // Poll for build to finish (~2 min)
96
+ const buildingUrl = currentDeployment.url;
97
+ const startTime = Date.now();
98
+ const maxWaitMs = 120_000;
99
+ const pollIntervalMs = 3_000;
100
+ let resolved = false;
101
+ const spinner = p.spinner({ indicator: "timer" });
102
+ spinner.start("Waiting for build");
103
+ try {
104
+ while (Date.now() - startTime < maxWaitMs) {
105
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
106
+ const fresh = await adapter.getDeploymentById(token, project, currentDeployment.id);
107
+ if (!fresh)
108
+ break;
109
+ if (fresh.state === "ready") {
110
+ spinner.stop(`${adapter.name} deployment ready!`);
111
+ return fresh.url;
112
+ }
113
+ if (fresh.state === "error") {
114
+ spinner.stop(pc.red(`${adapter.name} deployment failed.`));
115
+ p.log.error(`${adapter.name} deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
116
+ currentDeployment = undefined;
117
+ resolved = true;
118
+ break;
119
+ }
120
+ }
121
+ if (!resolved) {
122
+ spinner.stop("Still building...");
123
+ const action = await p.select({
124
+ message: `${adapter.name} deployment is still building.`,
125
+ options: [
126
+ { value: "continue", label: "Use it anyway", hint: "URL may not be ready yet" },
127
+ { value: "pick", label: "Select a different deployment" },
128
+ ],
129
+ });
130
+ if (p.isCancel(action)) {
131
+ p.cancel("Cancelled.");
132
+ process.exit(0);
133
+ }
134
+ if (action === "continue") {
135
+ p.log.info(`${adapter.name} deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
136
+ return buildingUrl;
137
+ }
138
+ currentDeployment = undefined;
139
+ // Fall through to picker
140
+ }
141
+ }
142
+ catch (e) {
143
+ spinner.stop("Error checking deployment status.");
144
+ if (e instanceof Error)
145
+ p.log.message(pc.dim(e.message));
146
+ }
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ // 8. Picker fallback — fetch all branch deployments (no SHA filter)
152
+ deployments = await adapter.getDeployments(token, project, { branch });
153
+ if (deployments.length === 0) {
154
+ if (isAgent) {
155
+ agentError({
156
+ type: "no_deployments",
157
+ message: `No deployments found for this branch. Ask the user for their staging URL.`,
158
+ suggestion: "Re-run with: `inflight share --url <staging-url>`",
159
+ });
160
+ }
161
+ p.log.warn("No deployments found for this branch. Paste a URL instead.");
162
+ return null;
163
+ }
164
+ const hasWorkingDeployment = deployments.some((d) => d.state !== "error");
165
+ if (!hasWorkingDeployment) {
166
+ if (isAgent) {
167
+ agentError({
168
+ type: "all_deployments_failed",
169
+ message: "All recent deployments failed. Ask the user to fix the build and push again, or provide a URL.",
170
+ suggestion: "Re-run with: `inflight share --url <staging-url>`",
171
+ });
172
+ }
173
+ p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
174
+ return null;
175
+ }
176
+ if (isAgent) {
177
+ agentActionRequired({
178
+ type: "choose_deployment",
179
+ message: "No deployment found for the current commit. Ask the user which deployment to use.",
180
+ choices: [
181
+ ...deployments.map((d) => ({
182
+ id: d.url,
183
+ label: d.commitMessage ? truncate(d.commitMessage.split("\n")[0], 55) : "No commit message",
184
+ hint: [
185
+ timeAgo(d.createdAt),
186
+ d.branch ?? "unknown",
187
+ d.state === "error" ? "(failed)" : d.state !== "ready" ? `(${d.state})` : undefined,
188
+ ].filter(Boolean).join(" · "),
189
+ })),
190
+ { id: "manual", label: "Paste a URL" },
191
+ ],
192
+ nextCommand: `inflight share --skip-git-check --url <URL>`,
193
+ });
194
+ }
195
+ const maxBranch = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
196
+ const selected = await p.select({
197
+ message: "No deployment found for current commit. Select one:",
198
+ options: [
199
+ ...deployments.map((d) => {
200
+ const isFailed = d.state === "error";
201
+ const branchLabel = (d.branch ?? "unknown").padEnd(maxBranch);
202
+ const ago = timeAgo(d.createdAt).padEnd(8);
203
+ const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
204
+ const msg = truncate(firstLine, 55);
205
+ if (isFailed) {
206
+ return {
207
+ value: d.url,
208
+ label: pc.dim(`${branchLabel} ${ago} ${pc.red("(failed)")} ${msg}`),
209
+ };
210
+ }
211
+ const state = d.state !== "ready" ? ` ${pc.yellow(`(${d.state})`)}` : "";
212
+ return { value: d.url, label: `${branchLabel} ${ago}${state} ${pc.dim(msg)}` };
213
+ }),
214
+ { value: "manual", label: "Paste a URL manually" },
215
+ ],
216
+ });
217
+ if (p.isCancel(selected)) {
218
+ p.cancel("Cancelled.");
219
+ process.exit(0);
220
+ }
221
+ if (selected === "manual")
222
+ return null;
223
+ // Warn if the user picked a failed deployment
224
+ const pickedDeploy = deployments.find((d) => d.url === selected);
225
+ if (pickedDeploy?.state === "error") {
226
+ p.log.warn("This deployment failed — the URL may not load.");
227
+ const confirm = await p.confirm({ message: "Use it anyway?" });
228
+ if (p.isCancel(confirm) || !confirm)
229
+ return null;
230
+ }
231
+ return selected;
232
+ }
233
+ // --- Helpers ---
234
+ function truncate(str, max) {
235
+ return str.length > max ? str.slice(0, max - 1) + "…" : str;
236
+ }
237
+ function timeAgo(timestamp) {
238
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
239
+ if (seconds < 60)
240
+ return "now";
241
+ const minutes = Math.floor(seconds / 60);
242
+ if (minutes < 60)
243
+ return `${minutes}m ago`;
244
+ const hours = Math.floor(minutes / 60);
245
+ if (hours < 24)
246
+ return `${hours}h ago`;
247
+ const days = Math.floor(hours / 24);
248
+ return `${days}d ago`;
249
+ }
@@ -1,3 +1,3 @@
1
1
  import type { GitInfo } from "../lib/git.js";
2
2
  import type { ProviderResolveOptions } from "./index.js";
3
- export declare function resolveVercelUrl(cwd: string, gitInfo: GitInfo, opts?: ProviderResolveOptions): Promise<string | null>;
3
+ export declare function resolveVercelUrl(gitRoot: string, gitInfo: GitInfo, opts?: ProviderResolveOptions): Promise<string | null>;