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.
@@ -3,47 +3,74 @@ import pc from "picocolors";
3
3
  import { readGlobalAuth, writeGlobalAuth } from "../lib/config.js";
4
4
  import { apiGetMe } from "../lib/api.js";
5
5
  import { API_URL, WEB_URL } from "../lib/env.js";
6
+ import { isAgent, agentError } from "../lib/agent.js";
6
7
  const POLL_INTERVAL_MS = 2000;
7
8
  const POLL_TIMEOUT_MS = 5 * 60 * 1000;
8
9
  export async function loginCommand() {
9
10
  const existingAuth = readGlobalAuth();
10
11
  if (existingAuth) {
11
- const spinner = p.spinner();
12
- spinner.start("Checking existing session...");
13
- const me = await apiGetMe(existingAuth.apiKey).catch(() => null);
14
- if (me?.email) {
15
- spinner.stop(pc.green(`✓ Logged in as ${pc.bold(me.email)}`));
16
- return;
12
+ if (!isAgent) {
13
+ const spinner = p.spinner();
14
+ spinner.start("Checking existing session...");
15
+ const me = await apiGetMe(existingAuth.apiKey).catch(() => null);
16
+ if (me?.email) {
17
+ spinner.stop(pc.green(`✓ Logged in as ${pc.bold(me.email)}`));
18
+ return;
19
+ }
20
+ spinner.stop("Session expired — re-authenticating...");
21
+ }
22
+ else {
23
+ const me = await apiGetMe(existingAuth.apiKey).catch(() => null);
24
+ if (me?.email)
25
+ return;
17
26
  }
18
- spinner.stop("Session expired — re-authenticating...");
19
27
  }
20
28
  const sessionId = crypto.randomUUID();
21
29
  const authUrl = `${WEB_URL}/cli/connect?session_id=${sessionId}`;
22
- p.log.info("Opening browser to authenticate with Inflight...");
23
- p.log.info(`Opening ${pc.cyan(authUrl)}`);
30
+ if (!isAgent) {
31
+ p.log.info("Opening browser to authenticate with Inflight...");
32
+ p.log.info(`Opening ${pc.cyan(authUrl)}`);
33
+ }
24
34
  const { default: open } = await import("open");
25
35
  await open(authUrl);
26
- const spinner = p.spinner();
27
- spinner.start("Waiting for browser authentication...");
28
- const apiKey = await pollForApiKey(sessionId);
29
- if (!apiKey) {
30
- spinner.stop("Authentication timed out.");
31
- p.log.error("No response after 5 minutes. Please try again.");
32
- process.exit(1);
36
+ if (!isAgent) {
37
+ const spinner = p.spinner();
38
+ spinner.start("Waiting for login");
39
+ const apiKey = await pollForApiKey(sessionId);
40
+ if (!apiKey) {
41
+ spinner.stop("Authentication timed out.");
42
+ p.log.error("No response after 5 minutes. Please try again.");
43
+ process.exit(1);
44
+ }
45
+ spinner.message("Validating...");
46
+ const me = await apiGetMe(apiKey).catch((e) => {
47
+ spinner.stop("Validation failed.");
48
+ p.log.error(e.message);
49
+ process.exit(1);
50
+ });
51
+ if (!me.email) {
52
+ spinner.stop("Validation failed.");
53
+ p.log.error("No email associated with this account.");
54
+ process.exit(1);
55
+ }
56
+ writeGlobalAuth({ apiKey });
57
+ spinner.stop(pc.green(`✓ Logged in as ${pc.bold(me.email)}`));
33
58
  }
34
- spinner.message("Validating...");
35
- const me = await apiGetMe(apiKey).catch((e) => {
36
- spinner.stop("Validation failed.");
37
- p.log.error(e.message);
38
- process.exit(1);
39
- });
40
- if (!me.email) {
41
- spinner.stop("Validation failed.");
42
- p.log.error("No email associated with this account. Please sign up at inflight.co first.");
43
- process.exit(1);
59
+ else {
60
+ const apiKey = await pollForApiKey(sessionId);
61
+ if (!apiKey) {
62
+ agentError({
63
+ type: "auth_timeout",
64
+ message: "Authentication timed out. Approve the browser prompt and try again.",
65
+ suggestion: "inflight login",
66
+ });
67
+ }
68
+ const me = await apiGetMe(apiKey).catch(() => null);
69
+ if (!me?.email) {
70
+ agentError({ type: "auth_failed", message: "Could not validate account." });
71
+ }
72
+ writeGlobalAuth({ apiKey });
44
73
  }
45
- writeGlobalAuth({ apiKey });
46
- spinner.stop(pc.green(`✓ Logged in as ${pc.bold(me.email)}`));
47
74
  }
48
75
  async function pollForApiKey(sessionId) {
49
76
  const deadline = Date.now() + POLL_TIMEOUT_MS;
@@ -55,14 +82,10 @@ async function pollForApiKey(sessionId) {
55
82
  if (api_key)
56
83
  return api_key;
57
84
  }
58
- // 404 = not ready yet, keep polling
59
- // 500+ = server error, stop
60
85
  if (res.status >= 500)
61
86
  return null;
62
87
  }
63
- catch {
64
- // Network error (offline, DNS, etc.) — keep polling
65
- }
88
+ catch { }
66
89
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
67
90
  }
68
91
  return null;
@@ -1 +1,5 @@
1
- export declare function setupCommand(): Promise<void>;
1
+ export interface SetupOptions {
2
+ json?: boolean;
3
+ workspace?: string;
4
+ }
5
+ export declare function setupCommand(opts?: SetupOptions): Promise<void>;
@@ -1,12 +1,14 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { execSync } from "child_process";
4
- import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
+ import { readGlobalAuth, writeWorkspaceConfig } from "../lib/config.js";
5
5
  import { apiGetMe, apiDetectWidgetLocation } from "../lib/api.js";
6
6
  import { loginCommand } from "./login.js";
7
7
  import { shareCommand } from "./share.js";
8
8
  import { gatherProjectContext, insertWidgetScript } from "../lib/framework.js";
9
9
  import { isGitRepo } from "../lib/git.js";
10
+ import { isAgent, agentSuccess, agentError } from "../lib/agent.js";
11
+ import { resolveWorkspace } from "../lib/resolve-workspace.js";
10
12
  function execSyncErrorDetail(err) {
11
13
  if (err !== null && typeof err === "object" && "stderr" in err) {
12
14
  const b = err.stderr;
@@ -21,10 +23,10 @@ function execSyncErrorDetail(err) {
21
23
  }
22
24
  return "";
23
25
  }
24
- export async function setupCommand() {
26
+ export async function setupCommand(opts = {}) {
25
27
  const cwd = process.cwd();
26
28
  // ── Pre-flight: warn if not a git repo ──
27
- if (!isGitRepo(cwd)) {
29
+ if (!isGitRepo(cwd) && !isAgent) {
28
30
  p.log.warn("This directory is not a git repository.\n" +
29
31
  " Inflight works best inside a git repo so it can track branches and commits.");
30
32
  }
@@ -35,52 +37,61 @@ export async function setupCommand() {
35
37
  await loginCommand();
36
38
  auth = readGlobalAuth();
37
39
  if (!auth) {
40
+ if (isAgent)
41
+ agentError({ type: "auth_failed", message: "Login failed." });
38
42
  p.log.error("Login failed.");
39
43
  process.exit(1);
40
44
  }
41
45
  }
42
46
  // ── Step 3: Resolve workspace ──
43
47
  const me = await apiGetMe(auth.apiKey).catch((e) => {
48
+ if (isAgent)
49
+ agentError({ type: "api_error", message: e.message });
44
50
  p.log.error(e.message);
45
51
  process.exit(1);
46
52
  });
47
- if (alreadyLoggedIn && me.email) {
53
+ if (!isAgent && alreadyLoggedIn && me.email) {
48
54
  p.log.success(`Logged in as ${pc.bold(me.email)}`);
49
55
  }
50
- const workspaces = me.workspaces;
51
- let workspaceId;
52
- // Check if a workspace is already configured and still valid
53
- const existingConfig = readWorkspaceConfig();
54
- const existingWorkspace = existingConfig ? workspaces.find((w) => w.id === existingConfig.workspaceId) : null;
55
- if (existingWorkspace) {
56
- workspaceId = existingWorkspace.id;
57
- p.log.success(`Workspace: ${pc.bold(existingWorkspace.name)} ${pc.dim("(change anytime with inflight workspace)")}`);
58
- }
59
- else if (workspaces.length === 0) {
60
- p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
61
- process.exit(1);
62
- }
63
- else if (workspaces.length === 1) {
64
- workspaceId = workspaces[0].id;
65
- p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
66
- }
67
- else {
68
- const selected = await p.select({
69
- message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
70
- options: workspaces.map((w) => ({ value: w.id, label: w.name })),
71
- });
72
- if (p.isCancel(selected)) {
73
- p.cancel("Cancelled.");
74
- process.exit(0);
75
- }
76
- workspaceId = selected;
77
- }
56
+ const workspaceId = await resolveWorkspace(me.workspaces, {
57
+ explicitId: opts.workspace,
58
+ commandForNext: "inflight setup",
59
+ });
78
60
  writeWorkspaceConfig({ workspaceId });
79
- const widgetId = workspaces.find((w) => w.id === workspaceId)?.widgetId;
61
+ if (!isAgent) {
62
+ const wsName = me.workspaces.find((w) => w.id === workspaceId)?.name;
63
+ if (wsName)
64
+ p.log.success(`Workspace: ${pc.bold(wsName)} ${pc.dim("(change anytime with inflight workspace)")}`);
65
+ }
66
+ const widgetId = me.workspaces.find((w) => w.id === workspaceId)?.widgetId;
80
67
  if (!widgetId) {
68
+ if (isAgent)
69
+ agentError({ type: "no_widget_id", message: "Could not find widget ID for this workspace." });
81
70
  p.log.error("Could not find widget ID for this workspace.");
82
71
  process.exit(1);
83
72
  }
73
+ // ── Agent mode: return JSON with instructions and exit ──
74
+ if (isAgent) {
75
+ const context = gatherProjectContext(cwd);
76
+ const alreadyHasWidget = Object.values(context.fileContents).some((c) => c.includes("inflight.co/widget.js"));
77
+ const scriptTag = `<script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`;
78
+ const nextSteps = alreadyHasWidget
79
+ ? ["Widget already installed. Run `inflight share` to share your staging URL."]
80
+ : [
81
+ "Insert the scriptTag into the project's root layout file, just before </body> (or as the last child of <body> in JSX/TSX files).",
82
+ "Common locations: app/layout.tsx (Next.js), index.html (Vite/CRA), app/root.tsx (Remix), src/app.html (SvelteKit).",
83
+ "Commit and push the change so it's included in the next deployment.",
84
+ "Then run `inflight share` to share the staging URL for feedback.",
85
+ ];
86
+ agentSuccess({
87
+ workspaceId,
88
+ workspaceName: me.workspaces.find((w) => w.id === workspaceId)?.name ?? null,
89
+ widgetId,
90
+ scriptTag,
91
+ alreadyInstalled: alreadyHasWidget,
92
+ nextSteps,
93
+ });
94
+ }
84
95
  // ── Step 4: Add widget script tag ──
85
96
  const hasWidget = (fileContents) => Object.values(fileContents).some((c) => c.includes("inflight.co/widget.js"));
86
97
  const context = gatherProjectContext(cwd);
@@ -1,5 +1,11 @@
1
1
  export interface ShareOptions {
2
2
  url?: string;
3
3
  json?: boolean;
4
+ workspace?: string;
5
+ project?: string;
6
+ provider?: string;
7
+ deployment?: string;
8
+ override?: boolean;
9
+ skipGitCheck?: boolean;
4
10
  }
5
11
  export declare function shareCommand(opts?: ShareOptions): Promise<void>;
@@ -1,11 +1,15 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
- import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
- import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead, } from "../lib/git.js";
3
+ import { readGlobalAuth } from "../lib/config.js";
4
+ import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead, getGitRoot, } from "../lib/git.js";
5
5
  import open from "open";
6
6
  import { providers } from "../providers/index.js";
7
7
  import { apiGetMe, apiCreateVersion, apiGetRecentProjects } from "../lib/api.js";
8
8
  import { scrollableSelect } from "../lib/scrollable-select.js";
9
+ import { isAgent, agentSuccess, agentActionRequired, agentError } from "../lib/agent.js";
10
+ import { resolveWorkspace } from "../lib/resolve-workspace.js";
11
+ import { readLocalVercelProject } from "../lib/vercel.js";
12
+ import { readLocalNetlifySite } from "../lib/netlify.js";
9
13
  function formatRelativeTime(timestampMs) {
10
14
  const seconds = Math.floor((Date.now() - timestampMs) / 1000);
11
15
  if (seconds < 60)
@@ -32,12 +36,29 @@ function isValidHostedUrl(url) {
32
36
  return false;
33
37
  }
34
38
  }
39
+ /**
40
+ * Builds a next_command string carrying forward relevant opts from the current run.
41
+ */
42
+ function buildNextCommand(base, opts) {
43
+ const parts = [base];
44
+ if (opts.workspace)
45
+ parts.push(`--workspace ${opts.workspace}`);
46
+ if (opts.provider)
47
+ parts.push(`--provider ${opts.provider}`);
48
+ if (opts.deployment)
49
+ parts.push(`--deployment ${opts.deployment}`);
50
+ if (opts.project)
51
+ parts.push(`--project ${opts.project}`);
52
+ if (opts.override)
53
+ parts.push("--override");
54
+ return parts.join(" ");
55
+ }
35
56
  /**
36
57
  * Checks local git state and prompts the user to commit/push if needed.
37
58
  * Returns { justPushed: true } if changes were pushed, { justPushed: false } otherwise.
38
59
  * Provider-agnostic — works for Vercel, Netlify, or any future provider.
39
60
  */
40
- async function checkAndSyncGit(cwd) {
61
+ async function checkAndSyncGit(cwd, opts = {}) {
41
62
  const state = getGitSyncState(cwd);
42
63
  if (state.status === "clean" || state.status === "detached") {
43
64
  return { justPushed: false };
@@ -74,6 +95,21 @@ async function checkAndSyncGit(cwd) {
74
95
  if (state.changedFiles.length > maxFiles) {
75
96
  lines.push(` ${pc.dim(`... and ${state.changedFiles.length - maxFiles} more`)}`);
76
97
  }
98
+ if (isAgent) {
99
+ agentActionRequired({
100
+ type: "git_uncommitted",
101
+ message: "You have uncommitted changes. Your deployment won't include them.",
102
+ choices: [
103
+ { id: "commit_push", label: "Commit and push these changes" },
104
+ { id: "continue", label: "Continue without committing" },
105
+ ],
106
+ instructions: {
107
+ commit_push: "Stage the changes, commit with a descriptive message, and push. Then re-run with the next_command.",
108
+ continue: "Re-run with the next_command without committing.",
109
+ },
110
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
111
+ });
112
+ }
77
113
  lines.push("", pc.yellow("Your deployment won't include these changes."));
78
114
  p.log.warn("You have uncommitted changes:\n" + lines.join("\n"));
79
115
  const action = await p.select({
@@ -128,6 +164,21 @@ async function checkAndSyncGit(cwd) {
128
164
  if (state.unpushedCommits.length > 5) {
129
165
  commitLines.push(` ${pc.dim(`... and ${state.unpushedCommits.length - 5} more`)}`);
130
166
  }
167
+ if (isAgent) {
168
+ agentActionRequired({
169
+ type: "git_unpushed",
170
+ message: "You have unpushed commits. Your deployment won't include them.",
171
+ choices: [
172
+ { id: "push", label: "Push these commits" },
173
+ { id: "continue", label: "Continue without pushing" },
174
+ ],
175
+ instructions: {
176
+ push: "Run `git push` to push the commits. Then re-run with the next_command.",
177
+ continue: "Re-run with the next_command without pushing.",
178
+ },
179
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
180
+ });
181
+ }
131
182
  commitLines.push("", pc.yellow("Your deployment won't include these commits."));
132
183
  p.log.warn("You have unpushed commits:\n" + commitLines.join("\n"));
133
184
  const action = await p.select({
@@ -170,6 +221,21 @@ async function checkAndSyncGit(cwd) {
170
221
  p.log.info(`Branch ${pc.bold(branch)} has no new commits — using existing deployments.`);
171
222
  return { justPushed: false };
172
223
  }
224
+ if (isAgent) {
225
+ agentActionRequired({
226
+ type: "git_no_remote",
227
+ message: `Branch "${branch}" hasn't been pushed yet — no deployment exists.`,
228
+ choices: [
229
+ { id: "push", label: "Push to create a deployment" },
230
+ { id: "continue", label: "Continue without pushing" },
231
+ ],
232
+ instructions: {
233
+ push: "Run `git push -u origin ${branch}` to push the branch. Then re-run with the next_command.",
234
+ continue: "Re-run with the next_command without pushing.",
235
+ },
236
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
237
+ });
238
+ }
173
239
  p.log.warn(`Branch ${pc.bold(branch)} hasn't been pushed yet — no deployment exists.`);
174
240
  const confirm = await p.confirm({
175
241
  message: "Push to create a deployment?",
@@ -200,12 +266,174 @@ async function checkAndSyncGit(cwd) {
200
266
  }
201
267
  return { justPushed: false };
202
268
  }
269
+ /**
270
+ * Agent mode share flow.
271
+ * Mirrors every human prompt with action_required JSON.
272
+ * Polling runs normally (no spinners).
273
+ * Git is NOT touched — if there's dirty state, action_required is returned
274
+ * and the agent handles git itself, then re-runs with --skip-git-check.
275
+ */
276
+ async function agentShareFlow(cwd, apiKey, workspaceId, opts) {
277
+ // ── Git sync — report state, don't touch git ──
278
+ if (!opts.skipGitCheck) {
279
+ // checkAndSyncGit will call agentActionRequired and exit if git is dirty
280
+ await checkAndSyncGit(cwd, opts);
281
+ // If we reach here, git is clean
282
+ }
283
+ const gitInfo = getGitInfo(cwd);
284
+ // ── Resolve staging URL ──
285
+ let stagingUrl;
286
+ if (opts.deployment) {
287
+ // --deployment flag provided
288
+ stagingUrl = opts.deployment;
289
+ if (!stagingUrl.startsWith("http"))
290
+ stagingUrl = `https://${stagingUrl}`;
291
+ }
292
+ else {
293
+ // Determine provider
294
+ let providerId = opts.provider;
295
+ if (!providerId) {
296
+ // Auto-detect from local config files
297
+ const gitRoot = getGitRoot(cwd);
298
+ if (gitRoot && readLocalVercelProject(gitRoot)) {
299
+ providerId = "vercel";
300
+ }
301
+ else if (gitRoot && readLocalNetlifySite(gitRoot)) {
302
+ providerId = "netlify";
303
+ }
304
+ else {
305
+ agentActionRequired({
306
+ type: "choose_provider",
307
+ message: "Could not auto-detect deployment provider.",
308
+ choices: [
309
+ { id: "vercel", label: "Vercel" },
310
+ { id: "netlify", label: "Netlify" },
311
+ ],
312
+ nextCommand: "inflight share --skip-git-check --provider <ID>",
313
+ });
314
+ }
315
+ }
316
+ const provider = providers.find((prov) => prov.id === providerId);
317
+ if (!provider) {
318
+ agentError({
319
+ type: "invalid_provider",
320
+ message: `Unknown provider "${providerId}". Use "vercel" or "netlify".`,
321
+ });
322
+ }
323
+ stagingUrl = (await provider.resolve(cwd, gitInfo, {})) ?? undefined;
324
+ if (stagingUrl && !stagingUrl.startsWith("http")) {
325
+ stagingUrl = `https://${stagingUrl}`;
326
+ }
327
+ if (!stagingUrl) {
328
+ agentError({
329
+ type: "no_deployment",
330
+ message: "Could not find a deployment URL. Provide one with --url.",
331
+ suggestion: "inflight share --url <staging-url>",
332
+ });
333
+ }
334
+ }
335
+ // At this point stagingUrl is guaranteed set — provider.resolve returned a value or agentError exited
336
+ const resolvedUrl = stagingUrl;
337
+ // ── Resolve project ──
338
+ let selectedProjectId;
339
+ let overrideVersionId;
340
+ if (opts.project) {
341
+ if (opts.project !== "new") {
342
+ selectedProjectId = opts.project;
343
+ }
344
+ }
345
+ else {
346
+ const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
347
+ projects: [],
348
+ }));
349
+ if (projects.length > 0) {
350
+ const currentBranch = gitInfo.branch;
351
+ agentActionRequired({
352
+ type: "choose_project",
353
+ message: "Select a project or create new.",
354
+ choices: [
355
+ { id: "new", label: "Start a new project" },
356
+ ...projects.map((proj) => ({
357
+ id: proj.projectId,
358
+ label: proj.latestVersion.title,
359
+ hint: [
360
+ proj.latestVersion.branch === currentBranch ? "current branch" : proj.latestVersion.branch,
361
+ proj.latestVersion.branch === currentBranch ? "(recommended)" : undefined,
362
+ ]
363
+ .filter(Boolean)
364
+ .join(" ") || undefined,
365
+ })),
366
+ ],
367
+ nextCommand: `inflight share --skip-git-check --deployment ${resolvedUrl} --project <ID>`,
368
+ });
369
+ }
370
+ }
371
+ // ── Resolve override vs new version ──
372
+ if (selectedProjectId && !opts.override) {
373
+ const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
374
+ projects: [],
375
+ }));
376
+ const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
377
+ if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
378
+ agentActionRequired({
379
+ type: "choose_override",
380
+ message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
381
+ choices: [
382
+ {
383
+ id: "override",
384
+ label: "Update its staging URL",
385
+ hint: `replace with ${new URL(resolvedUrl).hostname}`,
386
+ },
387
+ {
388
+ id: "new_version",
389
+ label: "Add a new version",
390
+ hint: "keep both in version history",
391
+ },
392
+ ],
393
+ nextCommand: `inflight share --skip-git-check --deployment ${resolvedUrl} --project ${selectedProjectId} --override`,
394
+ });
395
+ }
396
+ }
397
+ if (opts.override && selectedProjectId) {
398
+ const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
399
+ projects: [],
400
+ }));
401
+ const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
402
+ if (selectedProject) {
403
+ overrideVersionId = selectedProject.latestVersion.id;
404
+ }
405
+ }
406
+ // ── Create version ──
407
+ const result = await apiCreateVersion({
408
+ apiKey,
409
+ workspaceId,
410
+ stagingUrl: resolvedUrl,
411
+ gitInfo,
412
+ ...(selectedProjectId && { projectId: selectedProjectId }),
413
+ ...(overrideVersionId && { overrideVersionId }),
414
+ }).catch((e) => {
415
+ agentError({ type: "create_failed", message: e.message });
416
+ });
417
+ await open(resolvedUrl);
418
+ agentSuccess({
419
+ stagingUrl: resolvedUrl,
420
+ ...result,
421
+ isOverride: !!overrideVersionId,
422
+ });
423
+ }
203
424
  export async function shareCommand(opts = {}) {
204
425
  const cwd = process.cwd();
205
426
  // TODO: Add a step to login if not authenticated
206
427
  // ── Step 1: Auth ──
207
428
  const auth = readGlobalAuth();
208
429
  if (!auth) {
430
+ if (isAgent) {
431
+ agentError({
432
+ type: "not_authenticated",
433
+ message: "Not logged in. Run inflight setup first.",
434
+ suggestion: "inflight setup",
435
+ });
436
+ }
209
437
  if (opts.json) {
210
438
  console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight setup first." }));
211
439
  }
@@ -217,6 +445,8 @@ export async function shareCommand(opts = {}) {
217
445
  let gitInfo = getGitInfo(cwd);
218
446
  // ── Step 2: Resolve workspace ──
219
447
  const me = await apiGetMe(auth.apiKey).catch((e) => {
448
+ if (isAgent)
449
+ agentError({ type: "api_error", message: e.message });
220
450
  if (opts.json) {
221
451
  console.log(JSON.stringify({ error: "api_error", message: e.message }));
222
452
  }
@@ -225,50 +455,10 @@ export async function shareCommand(opts = {}) {
225
455
  }
226
456
  process.exit(1);
227
457
  });
228
- const workspaces = me.workspaces;
229
- let workspaceId;
230
- const savedConfig = readWorkspaceConfig();
231
- const savedWorkspace = savedConfig ? workspaces.find((w) => w.id === savedConfig.workspaceId) : null;
232
- if (savedWorkspace) {
233
- workspaceId = savedWorkspace.id;
234
- }
235
- else if (workspaces.length === 0) {
236
- if (opts.json) {
237
- console.log(JSON.stringify({
238
- error: "no_workspaces",
239
- message: "No workspaces found. Create one at inflight.co first.",
240
- }));
241
- }
242
- else {
243
- p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
244
- }
245
- process.exit(1);
246
- }
247
- else if (workspaces.length === 1) {
248
- workspaceId = workspaces[0].id;
249
- if (!opts.json)
250
- p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
251
- }
252
- else {
253
- if (opts.json) {
254
- console.log(JSON.stringify({
255
- error: "no_workspace_set",
256
- message: "Multiple workspaces found. Run 'inflight workspace --set=ID' first.",
257
- workspaces: workspaces.map((w) => ({ id: w.id, name: w.name })),
258
- }));
259
- process.exit(1);
260
- }
261
- const selected = await p.select({
262
- message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
263
- options: workspaces.map((w) => ({ value: w.id, label: w.name })),
264
- });
265
- if (p.isCancel(selected)) {
266
- p.cancel("Cancelled.");
267
- process.exit(0);
268
- }
269
- workspaceId = selected;
270
- }
271
- writeWorkspaceConfig({ workspaceId });
458
+ const workspaceId = await resolveWorkspace(me.workspaces, {
459
+ explicitId: opts.workspace,
460
+ commandForNext: "inflight share",
461
+ });
272
462
  // ── Fast path: URL provided (agent / scripting) ──
273
463
  if (opts.url) {
274
464
  let stagingUrl = opts.url;
@@ -279,6 +469,8 @@ export async function shareCommand(opts = {}) {
279
469
  const message = stagingUrl.includes("localhost")
280
470
  ? "Inflight needs a hosted URL — localhost isn't accessible to your team. Deploy to Vercel, Netlify, or another hosting provider first."
281
471
  : "Must be a hosted URL with a domain (e.g., my-branch.vercel.app)";
472
+ if (isAgent)
473
+ agentError({ type: "invalid_url", message });
282
474
  if (opts.json) {
283
475
  console.log(JSON.stringify({ error: "invalid_url", message }));
284
476
  }
@@ -292,7 +484,10 @@ export async function shareCommand(opts = {}) {
292
484
  workspaceId,
293
485
  stagingUrl,
294
486
  gitInfo,
487
+ ...(opts.project && opts.project !== "new" && { projectId: opts.project }),
295
488
  }).catch((e) => {
489
+ if (isAgent)
490
+ agentError({ type: "create_failed", message: e.message });
296
491
  if (opts.json) {
297
492
  console.log(JSON.stringify({ error: "create_failed", message: e.message }));
298
493
  }
@@ -301,6 +496,10 @@ export async function shareCommand(opts = {}) {
301
496
  }
302
497
  process.exit(1);
303
498
  });
499
+ if (isAgent) {
500
+ await open(stagingUrl);
501
+ agentSuccess({ stagingUrl, ...result });
502
+ }
304
503
  if (opts.json) {
305
504
  console.log(JSON.stringify({ success: true, stagingUrl, ...result }));
306
505
  }
@@ -311,6 +510,11 @@ export async function shareCommand(opts = {}) {
311
510
  await open(stagingUrl);
312
511
  return;
313
512
  }
513
+ // ── Agent mode: structured flow with action_required for every choice ──
514
+ if (isAgent) {
515
+ await agentShareFlow(cwd, auth.apiKey, workspaceId, opts);
516
+ return;
517
+ }
314
518
  // Resolve staging URL
315
519
  const providerChoice = await p.select({
316
520
  message: "Where is your staging URL hosted?",