inflight-cli 2.7.0 → 2.9.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.
package/dist/index.js CHANGED
@@ -18,13 +18,24 @@ program
18
18
  .description("Get feedback directly on your staging URL")
19
19
  .version(version)
20
20
  .enablePositionalOptions();
21
- program.command("setup").description("Set up Inflight in your project").action(setupCommand);
21
+ program
22
+ .command("setup")
23
+ .description("Set up Inflight in your project")
24
+ .option("--json", "Output as JSON (auto-enabled for non-TTY)")
25
+ .option("--workspace <id>", "Workspace ID (skip selection)")
26
+ .action((opts) => setupCommand(opts));
22
27
  program.command("login").description("Authenticate with your Inflight account").action(loginCommand);
23
28
  program
24
29
  .command("share")
25
30
  .description("Get feedback on your staging URL")
26
31
  .option("--url <url>", "Staging URL (skips provider selection)")
27
- .option("--json", "Output result as JSON")
32
+ .option("--json", "Output result as JSON (auto-enabled for non-TTY)")
33
+ .option("--workspace <id>", "Workspace ID (skip selection)")
34
+ .option("--project <id>", "Project ID, or 'new' to create")
35
+ .option("--provider <id>", "Deployment provider: vercel, netlify")
36
+ .option("--deployment <url>", "Specific deployment URL")
37
+ .option("--override", "Override latest version instead of creating new")
38
+ .option("--skip-git-check", "Skip git state check (use after agent handled git)")
28
39
  .action((opts) => shareCommand(opts));
29
40
  program
30
41
  .command("workspace")
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Agent mode detection and structured JSON output.
3
+ *
4
+ * When !process.stdout.isTTY (piped output, agent calling CLI),
5
+ * all interactive prompts are replaced with auto-resolution or
6
+ * structured JSON responses the agent can parse and act on.
7
+ */
8
+ export declare const isAgent: boolean;
9
+ export interface AgentChoice {
10
+ id: string;
11
+ label: string;
12
+ hint?: string;
13
+ }
14
+ export declare function agentSuccess(data: Record<string, unknown>): never;
15
+ export declare function agentActionRequired(opts: {
16
+ type: string;
17
+ message: string;
18
+ choices: AgentChoice[];
19
+ nextCommand: string;
20
+ instructions?: Record<string, string>;
21
+ }): never;
22
+ export declare function agentError(opts: {
23
+ type: string;
24
+ message: string;
25
+ suggestion?: string;
26
+ }): never;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Agent mode detection and structured JSON output.
3
+ *
4
+ * When !process.stdout.isTTY (piped output, agent calling CLI),
5
+ * all interactive prompts are replaced with auto-resolution or
6
+ * structured JSON responses the agent can parse and act on.
7
+ */
8
+ export const isAgent = !process.stdout.isTTY;
9
+ export function agentSuccess(data) {
10
+ console.log(JSON.stringify({ status: "success", data }));
11
+ process.exit(0);
12
+ }
13
+ export function agentActionRequired(opts) {
14
+ console.log(JSON.stringify({
15
+ status: "action_required",
16
+ type: opts.type,
17
+ message: opts.message,
18
+ choices: opts.choices,
19
+ next_command: opts.nextCommand,
20
+ ...(opts.instructions && { instructions: opts.instructions }),
21
+ }));
22
+ process.exit(0);
23
+ }
24
+ export function agentError(opts) {
25
+ console.log(JSON.stringify({
26
+ status: "error",
27
+ type: opts.type,
28
+ message: opts.message,
29
+ ...(opts.suggestion && { suggestion: opts.suggestion }),
30
+ }));
31
+ process.exit(1);
32
+ }
@@ -0,0 +1,15 @@
1
+ import type { Workspace } from "./api.js";
2
+ /**
3
+ * Resolves the active workspace ID.
4
+ *
5
+ * Resolution order:
6
+ * 1. Explicit --workspace flag
7
+ * 2. Saved config from prior session
8
+ * 3. Single workspace → auto-select
9
+ * 4. Multiple → action_required (agent) or p.select (human)
10
+ * 5. Zero → error
11
+ */
12
+ export declare function resolveWorkspace(workspaces: Workspace[], opts?: {
13
+ explicitId?: string;
14
+ commandForNext?: string;
15
+ }): Promise<string>;
@@ -0,0 +1,77 @@
1
+ // apps/cli/src/lib/resolve-workspace.ts
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import { isAgent, agentActionRequired, agentError } from "./agent.js";
5
+ import { readWorkspaceConfig, writeWorkspaceConfig } from "./config.js";
6
+ /**
7
+ * Resolves the active workspace ID.
8
+ *
9
+ * Resolution order:
10
+ * 1. Explicit --workspace flag
11
+ * 2. Saved config from prior session
12
+ * 3. Single workspace → auto-select
13
+ * 4. Multiple → action_required (agent) or p.select (human)
14
+ * 5. Zero → error
15
+ */
16
+ export async function resolveWorkspace(workspaces, opts = {}) {
17
+ const { explicitId, commandForNext = "inflight share" } = opts;
18
+ // Explicit flag
19
+ if (explicitId) {
20
+ const match = workspaces.find((w) => w.id === explicitId);
21
+ if (!match) {
22
+ if (isAgent) {
23
+ agentError({
24
+ type: "invalid_workspace",
25
+ message: `Workspace "${explicitId}" not found.`,
26
+ suggestion: `Valid IDs: ${workspaces.map((w) => w.id).join(", ")}`,
27
+ });
28
+ }
29
+ p.log.error(`Workspace "${explicitId}" not found.`);
30
+ process.exit(1);
31
+ }
32
+ writeWorkspaceConfig({ workspaceId: match.id });
33
+ return match.id;
34
+ }
35
+ // Saved config — return silently (avoids duplicate log when setup calls share)
36
+ const savedConfig = readWorkspaceConfig();
37
+ const savedWorkspace = savedConfig ? workspaces.find((w) => w.id === savedConfig.workspaceId) : null;
38
+ if (savedWorkspace) {
39
+ return savedWorkspace.id;
40
+ }
41
+ // Zero
42
+ if (workspaces.length === 0) {
43
+ if (isAgent) {
44
+ agentError({ type: "no_workspaces", message: "No workspaces found. Create one at inflight.co first." });
45
+ }
46
+ p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
47
+ process.exit(1);
48
+ }
49
+ // Single → auto-select
50
+ if (workspaces.length === 1) {
51
+ const ws = workspaces[0];
52
+ writeWorkspaceConfig({ workspaceId: ws.id });
53
+ if (!isAgent)
54
+ p.log.success(`Workspace: ${pc.bold(ws.name)}`);
55
+ return ws.id;
56
+ }
57
+ // Multiple
58
+ if (isAgent) {
59
+ agentActionRequired({
60
+ type: "choose_workspace",
61
+ message: "Multiple workspaces found. Re-run with --workspace <id>.",
62
+ choices: workspaces.map((w) => ({ id: w.id, label: w.name })),
63
+ nextCommand: `${commandForNext} --workspace <ID>`,
64
+ });
65
+ }
66
+ const selected = await p.select({
67
+ message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
68
+ options: workspaces.map((w) => ({ value: w.id, label: w.name })),
69
+ });
70
+ if (p.isCancel(selected)) {
71
+ p.cancel("Cancelled.");
72
+ process.exit(0);
73
+ }
74
+ const workspaceId = selected;
75
+ writeWorkspaceConfig({ workspaceId });
76
+ return workspaceId;
77
+ }
@@ -1,6 +1,7 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { parseGitRepo, getGitRoot } from "../lib/git.js";
4
+ import { isAgent } from "../lib/agent.js";
4
5
  import { writeNetlifyConfig } from "../lib/config.js";
5
6
  import { ensureNetlifyCli, ensureNetlifyAuth, readLocalNetlifySite, writeLocalNetlifySite, getNetlifySiteById, getNetlifySites, getNetlifyDeploys, matchSitesByRepo, } from "../lib/netlify.js";
6
7
  // --- Auto-detection ---
@@ -37,20 +38,20 @@ async function autoDetectSite(cwd, gitInfo, token) {
37
38
  }
38
39
  }
39
40
  // --- Fetch all sites ---
40
- const spinner = p.spinner();
41
- spinner.start("Detecting Netlify site...");
41
+ const spinner = !isAgent ? p.spinner() : null;
42
+ spinner?.start("Detecting Netlify site...");
42
43
  let allSites;
43
44
  try {
44
45
  allSites = await getNetlifySites(token);
45
46
  }
46
47
  catch (e) {
47
- spinner.stop("Could not fetch Netlify sites.");
48
- if (e instanceof Error)
48
+ spinner?.stop("Could not fetch Netlify sites.");
49
+ if (!isAgent && e instanceof Error)
49
50
  p.log.message(pc.dim(e.message));
50
51
  return null;
51
52
  }
52
53
  if (allSites.length === 0) {
53
- spinner.stop("No Netlify sites found.");
54
+ spinner?.stop("No Netlify sites found.");
54
55
  return null;
55
56
  }
56
57
  // --- Try exact match on git remote ---
@@ -58,7 +59,7 @@ async function autoDetectSite(cwd, gitInfo, token) {
58
59
  if (gitRepo) {
59
60
  const matches = matchSitesByRepo(allSites, gitRepo.owner, gitRepo.name);
60
61
  if (matches.length === 1) {
61
- spinner.stop(`Detected Netlify site: ${pc.bold(matches[0].name)}`);
62
+ spinner?.stop(`Detected Netlify site: ${pc.bold(matches[0].name)}`);
62
63
  return cacheResult({
63
64
  siteId: matches[0].id,
64
65
  siteName: matches[0].name,
@@ -67,17 +68,19 @@ async function autoDetectSite(cwd, gitInfo, token) {
67
68
  });
68
69
  }
69
70
  if (matches.length > 1) {
70
- spinner.stop(`Found ${matches.length} Netlify sites for this repo.`);
71
+ spinner?.stop(`Found ${matches.length} Netlify sites for this repo.`);
71
72
  const picked = await pickFromList(matches);
72
73
  return picked ? cacheResult(picked) : null;
73
74
  }
74
75
  }
75
76
  // --- No match ---
76
- spinner.stop("Could not auto-detect Netlify site.");
77
+ spinner?.stop("Could not auto-detect Netlify site.");
77
78
  const picked = await pickFromList(allSites);
78
79
  return picked ? cacheResult(picked) : null;
79
80
  }
80
81
  async function pickFromList(sites) {
82
+ if (isAgent)
83
+ return null;
81
84
  const maxName = Math.max(...sites.map((s) => s.name.length));
82
85
  const selected = await p.select({
83
86
  message: "Select a Netlify site",
@@ -102,9 +105,12 @@ async function pickFromList(sites) {
102
105
  export async function pickNetlifySite(token) {
103
106
  const allSites = await getNetlifySites(token);
104
107
  if (allSites.length === 0) {
105
- p.log.error("No Netlify sites found.");
108
+ if (!isAgent)
109
+ p.log.error("No Netlify sites found.");
106
110
  return null;
107
111
  }
112
+ if (isAgent)
113
+ return null;
108
114
  const selected = await p.select({
109
115
  message: "Select a Netlify site",
110
116
  options: allSites.map((s) => ({
@@ -127,18 +133,22 @@ export async function pickNetlifySite(token) {
127
133
  }
128
134
  // --- Main resolve function ---
129
135
  export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
130
- const cli = await ensureNetlifyCli((msg) => p.log.step(msg));
136
+ const cli = await ensureNetlifyCli((msg) => { if (!isAgent)
137
+ p.log.step(msg); });
131
138
  if (!cli.ok) {
132
- p.log.error("Could not install the Netlify CLI automatically.");
133
- if (cli.error) {
134
- p.log.message(pc.dim(cli.error.trim()));
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")}.`);
135
145
  }
136
- 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")}.`);
137
146
  process.exit(0);
138
147
  }
139
148
  const token = await ensureNetlifyAuth();
140
149
  if (!token) {
141
- p.log.error("Netlify login failed.");
150
+ if (!isAgent)
151
+ p.log.error("Netlify login failed.");
142
152
  return null;
143
153
  }
144
154
  const site = await autoDetectSite(cwd, gitInfo, token);
@@ -156,8 +166,8 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
156
166
  let commitDeploy = repoMatches ? deploys.find((d) => d.commitRef === commitSha) : undefined;
157
167
  // If just pushed, poll until the commit deployment appears
158
168
  if (!commitDeploy && repoMatches && opts?.justPushed) {
159
- const pollSpinner = p.spinner({ indicator: "timer" });
160
- pollSpinner.start("Waiting for Netlify to pick up your push...");
169
+ const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
170
+ pollSpinner?.start("Waiting for deploy");
161
171
  for (let i = 0; i < 30; i++) {
162
172
  await new Promise((r) => setTimeout(r, 2000));
163
173
  deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
@@ -168,10 +178,10 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
168
178
  break;
169
179
  }
170
180
  if (commitDeploy) {
171
- pollSpinner.clear();
181
+ pollSpinner?.clear();
172
182
  }
173
183
  else {
174
- pollSpinner.stop("No deployment detected yet — Netlify may still be processing.");
184
+ pollSpinner?.stop("No deployment detected yet — Netlify may still be processing.");
175
185
  }
176
186
  }
177
187
  // If we found a commit-specific deployment, handle based on state
@@ -181,20 +191,22 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
181
191
  ? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
182
192
  : "";
183
193
  if (commitDeploy.state === "error") {
184
- p.log.error(`Netlify deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
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.`);
185
196
  commitDeploy = undefined;
186
197
  // Fall through to the picker
187
198
  }
188
199
  else if (commitDeploy.state !== "ready") {
189
200
  // Any non-ready, non-error state (building, enqueued, uploading, preparing, processing, etc.)
190
- p.log.info(`Netlify deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state)}`);
201
+ if (!isAgent)
202
+ p.log.info(`Netlify deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state)}`);
191
203
  const buildingUrl = commitDeploy.deploySslUrl;
192
204
  const startTime = Date.now();
193
205
  const maxWaitMs = 120_000;
194
206
  const pollIntervalMs = 5_000;
195
207
  let resolved = false;
196
- const spinner = p.spinner({ indicator: "timer" });
197
- spinner.start("Waiting for Netlify to finish building...");
208
+ const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
209
+ spinner?.start("Waiting for build");
198
210
  try {
199
211
  while (Date.now() - startTime < maxWaitMs) {
200
212
  await new Promise((r) => setTimeout(r, pollIntervalMs));
@@ -205,20 +217,24 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
205
217
  if (!fresh)
206
218
  break;
207
219
  if (fresh.state === "ready") {
208
- spinner.stop("Netlify deployment ready!");
209
- p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.deploySslUrl)}`);
220
+ spinner?.stop("Netlify deployment ready!");
221
+ if (!isAgent)
222
+ p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.deploySslUrl)}`);
210
223
  return fresh.deploySslUrl;
211
224
  }
212
225
  if (fresh.state === "error") {
213
- spinner.stop(pc.red("Netlify deployment failed."));
214
- p.log.error(`Netlify deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
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.`);
215
229
  commitDeploy = undefined;
216
230
  resolved = true;
217
231
  break;
218
232
  }
219
233
  }
220
234
  if (!resolved) {
221
- spinner.stop("Still building...");
235
+ spinner?.stop("Still building...");
236
+ if (isAgent)
237
+ return buildingUrl;
222
238
  const action = await p.select({
223
239
  message: "Netlify deployment is still building.",
224
240
  options: [
@@ -239,14 +255,15 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
239
255
  }
240
256
  }
241
257
  catch (e) {
242
- spinner.stop("Error checking deployment status.");
243
- if (e instanceof Error)
258
+ spinner?.stop("Error checking deployment status.");
259
+ if (!isAgent && e instanceof Error)
244
260
  p.log.message(pc.dim(e.message));
245
261
  }
246
262
  }
247
263
  else {
248
264
  // ready — use it
249
- p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(commitDeploy.deploySslUrl)}`);
265
+ if (!isAgent)
266
+ p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(commitDeploy.deploySslUrl)}`);
250
267
  return commitDeploy.deploySslUrl;
251
268
  }
252
269
  }
@@ -256,14 +273,20 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
256
273
  });
257
274
  // Fallback: no commit deployment — let user pick from recent or paste manually
258
275
  if (deploys.length === 0) {
259
- p.log.warn("No deployments found. Paste a URL instead.");
276
+ if (!isAgent)
277
+ p.log.warn("No deployments found. Paste a URL instead.");
260
278
  return null;
261
279
  }
262
280
  const hasWorkingDeploy = deploys.some((d) => d.state !== "error");
263
281
  if (!hasWorkingDeploy) {
264
- p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
282
+ if (!isAgent)
283
+ p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
265
284
  return null;
266
285
  }
286
+ if (isAgent) {
287
+ const ready = deploys.find((d) => d.state === "ready");
288
+ return ready?.deploySslUrl ?? deploys[0]?.deploySslUrl ?? null;
289
+ }
267
290
  const maxBranch = Math.max(...deploys.map((d) => (d.branch ?? "unknown").length));
268
291
  const selected = await p.select({
269
292
  message: "No deployment found for current commit. Select one:",
@@ -295,6 +318,8 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
295
318
  // Warn if the user picked a failed deployment
296
319
  const pickedDeploy = deploys.find((d) => d.deploySslUrl === selected);
297
320
  if (pickedDeploy && pickedDeploy.state === "error") {
321
+ if (isAgent)
322
+ return null;
298
323
  p.log.warn("This deployment failed — the URL may not load.");
299
324
  const confirm = await p.confirm({ message: "Use it anyway?" });
300
325
  if (p.isCancel(confirm) || !confirm)
@@ -2,6 +2,7 @@ import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { execSync } from "child_process";
4
4
  import { parseGitRepo, getGitRoot } from "../lib/git.js";
5
+ import { isAgent } from "../lib/agent.js";
5
6
  import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectById, getVercelProjects, matchVercelProjectsByRepo, createVercelProject, getVercelDeployments, } from "../lib/vercel.js";
6
7
  // --- Auto-detection ---
7
8
  /**
@@ -40,20 +41,21 @@ async function autoDetectProject(cwd, gitInfo, token) {
40
41
  }
41
42
  }
42
43
  // --- Fetch all projects ---
43
- const spinner = p.spinner();
44
- spinner.start("Detecting Vercel project...");
44
+ const spinner = !isAgent ? p.spinner() : null;
45
+ spinner?.start("Detecting Vercel project...");
45
46
  let allProjects;
46
47
  try {
47
48
  allProjects = await getVercelProjects(token);
48
49
  }
49
50
  catch (e) {
50
- spinner.stop("Could not fetch Vercel projects.");
51
+ spinner?.stop("Could not fetch Vercel projects.");
51
52
  if (e instanceof Error)
52
- p.log.message(pc.dim(e.message));
53
+ if (!isAgent)
54
+ p.log.message(pc.dim(e.message));
53
55
  return null;
54
56
  }
55
57
  if (allProjects.length === 0) {
56
- spinner.stop("No Vercel projects found.");
58
+ spinner?.stop("No Vercel projects found.");
57
59
  return null;
58
60
  }
59
61
  // --- Try exact match on git remote ---
@@ -61,7 +63,7 @@ async function autoDetectProject(cwd, gitInfo, token) {
61
63
  if (gitRepo) {
62
64
  const matches = matchVercelProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
63
65
  if (matches.length === 1) {
64
- spinner.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
66
+ spinner?.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
65
67
  return cacheResult({
66
68
  teamId: matches[0].teamId,
67
69
  projectId: matches[0].id,
@@ -71,18 +73,20 @@ async function autoDetectProject(cwd, gitInfo, token) {
71
73
  });
72
74
  }
73
75
  if (matches.length > 1) {
74
- spinner.stop(`Found ${matches.length} Vercel projects for this repo.`);
76
+ spinner?.stop(`Found ${matches.length} Vercel projects for this repo.`);
75
77
  const picked = await pickFromList(allProjects);
76
78
  return picked ? cacheResult(picked) : null;
77
79
  }
78
80
  }
79
81
  // --- No match ---
80
- spinner.stop("Could not auto-detect Vercel project.");
82
+ spinner?.stop("Could not auto-detect Vercel project.");
81
83
  const canCreate = gitRepo && gitRepo.provider !== "unknown";
82
84
  const picked = await pickFromList(allProjects, canCreate ? { token, gitRepo } : undefined);
83
85
  return picked ? cacheResult(picked) : null;
84
86
  }
85
87
  async function pickFromList(projects, createCtx) {
88
+ if (isAgent)
89
+ return null;
86
90
  const maxName = Math.max(...projects.map((proj) => proj.name.length));
87
91
  const selected = await p.select({
88
92
  message: "Select a Vercel project",
@@ -113,6 +117,8 @@ async function pickFromList(projects, createCtx) {
113
117
  };
114
118
  }
115
119
  async function createProjectFlow(projects, ctx) {
120
+ if (isAgent)
121
+ return null;
116
122
  const { token, gitRepo } = ctx;
117
123
  // Pick team from unique teams
118
124
  const teams = [...new Map(projects.map((proj) => [proj.teamId, proj.teamName])).entries()];
@@ -184,18 +190,23 @@ async function createProjectFlow(projects, ctx) {
184
190
  }
185
191
  // --- Main resolve function ---
186
192
  export async function resolveVercelUrl(cwd, gitInfo, opts) {
187
- const cli = await ensureVercelCli((msg) => p.log.step(msg));
193
+ const cli = await ensureVercelCli((msg) => { if (!isAgent)
194
+ p.log.step(msg); });
188
195
  if (!cli.ok) {
189
- p.log.error("Could not install the Vercel CLI automatically.");
196
+ if (!isAgent)
197
+ p.log.error("Could not install the Vercel CLI automatically.");
190
198
  if (cli.error) {
191
- p.log.message(pc.dim(cli.error.trim()));
199
+ if (!isAgent)
200
+ p.log.message(pc.dim(cli.error.trim()));
192
201
  }
193
- 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")}.`);
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")}.`);
194
204
  process.exit(0);
195
205
  }
196
206
  const token = await ensureVercelAuth();
197
207
  if (!token) {
198
- p.log.error("Vercel login failed.");
208
+ if (!isAgent)
209
+ p.log.error("Vercel login failed.");
199
210
  return null;
200
211
  }
201
212
  const project = await autoDetectProject(cwd, gitInfo, token);
@@ -215,8 +226,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
215
226
  let commitDeploy = repoMatches ? deployments.find((d) => d.commitSha === commitSha) : undefined;
216
227
  // If just pushed, poll until the commit deployment appears
217
228
  if (!commitDeploy && repoMatches && opts?.justPushed) {
218
- const pollSpinner = p.spinner({ indicator: "timer" });
219
- pollSpinner.start("Waiting for Vercel to pick up your push");
229
+ const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
230
+ pollSpinner?.start("Waiting for deploy");
220
231
  for (let i = 0; i < 30; i++) {
221
232
  await new Promise((r) => setTimeout(r, 2000));
222
233
  deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
@@ -227,10 +238,10 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
227
238
  break;
228
239
  }
229
240
  if (commitDeploy) {
230
- pollSpinner.clear();
241
+ pollSpinner?.clear();
231
242
  }
232
243
  else {
233
- pollSpinner.stop("No deployment detected yet — Vercel may still be processing.");
244
+ pollSpinner?.stop("No deployment detected yet — Vercel may still be processing.");
234
245
  }
235
246
  }
236
247
  // If we found a commit-specific deployment, handle based on state
@@ -240,7 +251,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
240
251
  ? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
241
252
  : "";
242
253
  if (commitDeploy.state === "ERROR" || commitDeploy.state === "CANCELED") {
243
- p.log.error(`Vercel deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
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.`);
244
256
  commitDeploy = undefined;
245
257
  // Fall through to the picker
246
258
  }
@@ -248,15 +260,16 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
248
260
  commitDeploy.state === "QUEUED" ||
249
261
  commitDeploy.state === "INITIALIZING") {
250
262
  // Show what we're waiting on
251
- p.log.info(`Vercel deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state.toLowerCase())}`);
263
+ if (!isAgent)
264
+ p.log.info(`Vercel deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state.toLowerCase())}`);
252
265
  // Poll with a ticking timer for up to ~2 minutes
253
266
  const buildingUrl = commitDeploy.url;
254
267
  const startTime = Date.now();
255
268
  const maxWaitMs = 120_000;
256
269
  const pollIntervalMs = 3_000;
257
270
  let resolved = false;
258
- const spinner = p.spinner({ indicator: "timer" });
259
- spinner.start("Waiting for Vercel to finish building");
271
+ const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
272
+ spinner?.start("Waiting for build");
260
273
  try {
261
274
  while (Date.now() - startTime < maxWaitMs) {
262
275
  await new Promise((r) => setTimeout(r, pollIntervalMs));
@@ -267,19 +280,22 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
267
280
  if (!fresh)
268
281
  break;
269
282
  if (fresh.state === "READY") {
270
- spinner.stop("Vercel deployment ready!");
283
+ spinner?.stop("Vercel deployment ready!");
271
284
  return fresh.url;
272
285
  }
273
286
  if (fresh.state === "ERROR" || fresh.state === "CANCELED") {
274
- spinner.stop(pc.red("Vercel deployment failed."));
275
- p.log.error(`Vercel deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
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.`);
276
290
  commitDeploy = undefined;
277
291
  resolved = true;
278
292
  break;
279
293
  }
280
294
  }
281
295
  if (!resolved) {
282
- spinner.stop("Still building...");
296
+ if (isAgent)
297
+ return buildingUrl;
298
+ spinner?.stop("Still building...");
283
299
  const action = await p.select({
284
300
  message: "Vercel deployment is still building.",
285
301
  options: [
@@ -292,7 +308,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
292
308
  process.exit(0);
293
309
  }
294
310
  if (action === "continue") {
295
- p.log.info(`Vercel deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
311
+ if (!isAgent)
312
+ p.log.info(`Vercel deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
296
313
  return buildingUrl;
297
314
  }
298
315
  commitDeploy = undefined;
@@ -300,14 +317,16 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
300
317
  }
301
318
  }
302
319
  catch (e) {
303
- spinner.stop("Error checking deployment status.");
320
+ spinner?.stop("Error checking deployment status.");
304
321
  if (e instanceof Error)
305
- p.log.message(pc.dim(e.message));
322
+ if (!isAgent)
323
+ p.log.message(pc.dim(e.message));
306
324
  }
307
325
  }
308
326
  else {
309
327
  // READY or any other terminal state — use it
310
- p.log.info(`Vercel deployment for ${commitLabel}${message}`);
328
+ if (!isAgent)
329
+ p.log.info(`Vercel deployment for ${commitLabel}${message}`);
311
330
  return commitDeploy.url;
312
331
  }
313
332
  }
@@ -317,14 +336,20 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
317
336
  });
318
337
  // Fallback: no commit deployment found — let user pick from recent or paste manually
319
338
  if (deployments.length === 0) {
320
- p.log.warn("No deployments found. Paste a URL instead.");
339
+ if (!isAgent)
340
+ p.log.warn("No deployments found. Paste a URL instead.");
321
341
  return null;
322
342
  }
323
343
  const hasWorkingDeployment = deployments.some((d) => d.state !== "ERROR" && d.state !== "CANCELED");
324
344
  if (!hasWorkingDeployment) {
325
- p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
345
+ if (!isAgent)
346
+ p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
326
347
  return null;
327
348
  }
349
+ if (isAgent) {
350
+ const ready = deployments.find((d) => d.state === "READY");
351
+ return ready?.url ?? deployments[0]?.url ?? null;
352
+ }
328
353
  const maxBranchPick = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
329
354
  const selected = await p.select({
330
355
  message: "No deployment found for current commit. Select one:",
@@ -356,6 +381,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
356
381
  // Warn if the user picked a failed deployment
357
382
  const pickedDeploy = deployments.find((d) => d.url === selected);
358
383
  if (pickedDeploy && (pickedDeploy.state === "ERROR" || pickedDeploy.state === "CANCELED")) {
384
+ if (isAgent)
385
+ return null;
359
386
  p.log.warn("This deployment failed — the URL may not load.");
360
387
  const confirm = await p.confirm({ message: "Use it anyway?" });
361
388
  if (p.isCancel(confirm) || !confirm)