inflight-cli 2.13.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.
@@ -44,7 +44,7 @@ function appendWidgetToken(url, widgetToken) {
44
44
  return parsed.toString();
45
45
  }
46
46
  /**
47
- * Builds a next_command string carrying forward relevant opts from the current run. test
47
+ * Builds a next_command string carrying forward relevant opts from the current run.
48
48
  */
49
49
  function buildNextCommand(base, opts) {
50
50
  const parts = [base];
@@ -52,8 +52,8 @@ function buildNextCommand(base, opts) {
52
52
  parts.push(`--workspace ${opts.workspace}`);
53
53
  if (opts.provider)
54
54
  parts.push(`--provider ${opts.provider}`);
55
- if (opts.deployment)
56
- parts.push(`--deployment ${opts.deployment}`);
55
+ if (opts.url)
56
+ parts.push(`--url ${opts.url}`);
57
57
  if (opts.project)
58
58
  parts.push(`--project ${opts.project}`);
59
59
  if (opts.versionMode)
@@ -67,376 +67,208 @@ function buildNextCommand(base, opts) {
67
67
  */
68
68
  async function checkAndSyncGit(cwd, opts = {}) {
69
69
  const state = getGitSyncState(cwd);
70
- if (state.status === "clean" || state.status === "detached") {
71
- return { justPushed: false };
72
- }
73
- // After ruling out clean/detached, branch is guaranteed non-null
74
- if (!state.branch)
75
- return { justPushed: false };
76
- const { branch } = state;
77
- if (state.status === "uncommitted") {
78
- // Show changed files
79
- const maxFiles = 8;
80
- const display = state.changedFiles.slice(0, maxFiles);
81
- const lines = display.map((line) => {
82
- const trimmed = line.trim();
83
- // Parse git porcelain status: "M file.ts", "?? file.ts", "A file.ts", etc.
84
- const match = trimmed.match(/^(\S+)\s+(.+)$/);
85
- if (!match)
86
- return ` ${pc.dim(trimmed)}`;
87
- const status = match[1];
88
- const file = match[2];
89
- const label = status === "??"
90
- ? "new"
91
- : status === "M"
92
- ? "modified"
93
- : status === "A"
94
- ? "added"
95
- : status === "D"
96
- ? "deleted"
97
- : status === "R"
98
- ? "renamed"
99
- : "changed";
100
- return ` ${pc.dim(file)} ${pc.dim(`(${label})`)}`;
101
- });
102
- if (state.changedFiles.length > maxFiles) {
103
- lines.push(` ${pc.dim(`... and ${state.changedFiles.length - maxFiles} more`)}`);
104
- }
105
- if (isAgent) {
106
- agentActionRequired({
107
- type: "git_uncommitted",
108
- message: "There are uncommitted changes that won't be in the deployment. Ask the user what to do.",
109
- choices: [
110
- { id: "commit_push", label: "Commit and push these changes" },
111
- { id: "continue", label: "Continue without committing" },
112
- ],
113
- instructions: {
114
- commit_push: "Stage the changes, commit with a descriptive message, and push. Then re-run with the next_command.",
115
- continue: "Re-run with the next_command without committing.",
116
- },
117
- nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
118
- });
119
- }
120
- lines.push("", pc.yellow("Your deployment won't include these changes."));
121
- p.log.warn("You have uncommitted changes:\n" + lines.join("\n"));
122
- const action = await p.select({
123
- message: "What would you like to do?",
124
- options: [
125
- { value: "commit_push", label: "Commit & push (recommended)" },
126
- { value: "continue", label: "Continue anyway — use existing deployment" },
127
- ],
128
- });
129
- if (p.isCancel(action)) {
130
- p.cancel("Cancelled.");
131
- process.exit(0);
132
- }
133
- if (action === "continue") {
70
+ switch (state.status) {
71
+ case "clean":
72
+ case "detached":
134
73
  return { justPushed: false };
135
- }
136
- // Commit & push flow
137
- const defaultMessage = generateCommitMessage(state.branch);
138
- const message = await p.text({
139
- message: "Commit message",
140
- initialValue: defaultMessage,
141
- validate: (v) => {
142
- if (!v || !v.trim())
143
- return "Commit message is required";
144
- },
145
- });
146
- if (p.isCancel(message)) {
147
- p.cancel("Cancelled.");
148
- process.exit(0);
149
- }
150
- const spinner = p.spinner();
151
- spinner.start("Committing and pushing...");
152
- try {
153
- commitAndPush(cwd, message.trim(), branch);
154
- spinner.stop("Changes pushed!");
155
- return { justPushed: true };
156
- }
157
- catch (e) {
158
- const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
159
- spinner.stop("Push failed.");
160
- if (stderr)
161
- p.log.message(pc.dim(stderr));
162
- else if (e instanceof Error)
163
- p.log.message(pc.dim(e.message));
164
- p.log.warn("Continuing with existing deployment.");
165
- return { justPushed: false };
166
- }
167
- }
168
- if (state.status === "unpushed") {
169
- // Show unpushed commits
170
- const commitLines = state.unpushedCommits.slice(0, 5).map((line) => ` ${pc.dim(line)}`);
171
- if (state.unpushedCommits.length > 5) {
172
- commitLines.push(` ${pc.dim(`... and ${state.unpushedCommits.length - 5} more`)}`);
173
- }
174
- if (isAgent) {
175
- agentActionRequired({
176
- type: "git_unpushed",
177
- message: "There are unpushed commits that won't be in the deployment. Ask the user what to do.",
178
- choices: [
179
- { id: "push", label: "Push these commits" },
180
- { id: "continue", label: "Continue without pushing" },
181
- ],
182
- instructions: {
183
- push: "Run `git push` to push the commits. Then re-run with the next_command.",
184
- continue: "Re-run with the next_command without pushing.",
185
- },
186
- nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
74
+ case "uncommitted": {
75
+ const { branch, changedFiles } = state;
76
+ const maxFiles = 8;
77
+ const display = changedFiles.slice(0, maxFiles);
78
+ const lines = display.map((line) => {
79
+ const trimmed = line.trim();
80
+ const match = trimmed.match(/^(\S+)\s+(.+)$/);
81
+ if (!match)
82
+ return ` ${pc.dim(trimmed)}`;
83
+ const status = match[1];
84
+ const file = match[2];
85
+ const label = status === "??"
86
+ ? "new"
87
+ : status === "M"
88
+ ? "modified"
89
+ : status === "A"
90
+ ? "added"
91
+ : status === "D"
92
+ ? "deleted"
93
+ : status === "R"
94
+ ? "renamed"
95
+ : "changed";
96
+ return ` ${pc.dim(file)} ${pc.dim(`(${label})`)}`;
187
97
  });
188
- }
189
- commitLines.push("", pc.yellow("Your deployment won't include these commits."));
190
- p.log.warn("You have unpushed commits:\n" + commitLines.join("\n"));
191
- const action = await p.select({
192
- message: "What would you like to do?",
193
- options: [
194
- { value: "push", label: "Push (recommended)" },
195
- { value: "continue", label: "Continue anyway — use existing deployment" },
196
- ],
197
- });
198
- if (p.isCancel(action)) {
199
- p.cancel("Cancelled.");
200
- process.exit(0);
201
- }
202
- if (action === "continue") {
203
- return { justPushed: false };
204
- }
205
- const spinner = p.spinner();
206
- spinner.start("Pushing...");
207
- try {
208
- pushBranch(cwd, branch);
209
- spinner.stop("Changes pushed!");
210
- return { justPushed: true };
211
- }
212
- catch (e) {
213
- const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
214
- spinner.stop("Push failed.");
215
- if (stderr)
216
- p.log.message(pc.dim(stderr));
217
- else if (e instanceof Error)
218
- p.log.message(pc.dim(e.message));
219
- p.log.warn("Continuing with existing deployment.");
220
- return { justPushed: false };
221
- }
222
- }
223
- if (state.status === "no_remote") {
224
- // Working tree is clean (uncommitted was checked first in getGitSyncState).
225
- // Check if this branch actually has commits ahead of the default branch —
226
- // if not, pushing won't trigger a deployment.
227
- if (!hasCommitsAhead(cwd)) {
228
- p.log.info(`Branch ${pc.bold(branch)} has no new commits — using existing deployments.`);
229
- return { justPushed: false };
230
- }
231
- if (isAgent) {
232
- agentActionRequired({
233
- type: "git_no_remote",
234
- message: `Branch "${branch}" hasn't been pushed yet — no deployment exists. Ask the user what to do.`,
235
- choices: [
236
- { id: "push", label: "Push to create a deployment" },
237
- { id: "continue", label: "Continue without pushing" },
98
+ if (changedFiles.length > maxFiles) {
99
+ lines.push(` ${pc.dim(`... and ${changedFiles.length - maxFiles} more`)}`);
100
+ }
101
+ if (isAgent) {
102
+ agentActionRequired({
103
+ type: "git_uncommitted",
104
+ message: "There are uncommitted changes that won't be in the deployment. Ask the user what to do.",
105
+ choices: [
106
+ { id: "commit_push", label: "Commit and push these changes" },
107
+ { id: "continue", label: "Continue without committing" },
108
+ ],
109
+ instructions: {
110
+ commit_push: "Stage the changes, commit with a descriptive message, and push. Then re-run with the next_command.",
111
+ continue: "Re-run with the next_command without committing.",
112
+ },
113
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
114
+ });
115
+ }
116
+ lines.push("", pc.yellow("Your deployment won't include these changes."));
117
+ p.log.warn("You have uncommitted changes:\n" + lines.join("\n"));
118
+ const action = await p.select({
119
+ message: "What would you like to do?",
120
+ options: [
121
+ { value: "commit_push", label: "Commit & push (recommended)" },
122
+ { value: "continue", label: "Continue anyway — use existing deployment" },
238
123
  ],
239
- instructions: {
240
- push: "Run `git push -u origin ${branch}` to push the branch. Then re-run with the next_command.",
241
- continue: "Re-run with the next_command without pushing.",
124
+ });
125
+ if (p.isCancel(action)) {
126
+ p.cancel("Cancelled.");
127
+ process.exit(0);
128
+ }
129
+ if (action === "continue") {
130
+ return { justPushed: false };
131
+ }
132
+ const defaultMessage = generateCommitMessage(branch);
133
+ const message = await p.text({
134
+ message: "Commit message",
135
+ initialValue: defaultMessage,
136
+ validate: (v) => {
137
+ if (!v || !v.trim())
138
+ return "Commit message is required";
242
139
  },
243
- nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
244
140
  });
245
- }
246
- p.log.warn(`Branch ${pc.bold(branch)} hasn't been pushed yet — no deployment exists.`);
247
- const confirm = await p.confirm({
248
- message: "Push to create a deployment?",
249
- });
250
- if (p.isCancel(confirm)) {
251
- p.cancel("Cancelled.");
252
- process.exit(0);
253
- }
254
- if (!confirm) {
255
- return { justPushed: false };
256
- }
257
- const spinner = p.spinner();
258
- spinner.start("Pushing...");
259
- try {
260
- pushBranch(cwd, branch);
261
- spinner.stop("Branch pushed!");
262
- return { justPushed: true };
263
- }
264
- catch (e) {
265
- const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
266
- spinner.stop("Push failed.");
267
- if (stderr)
268
- p.log.message(pc.dim(stderr));
269
- else if (e instanceof Error)
270
- p.log.message(pc.dim(e.message));
271
- return { justPushed: false };
272
- }
273
- }
274
- return { justPushed: false };
275
- }
276
- /**
277
- * Agent mode share flow.
278
- * Mirrors every human prompt with action_required JSON.
279
- * Polling runs normally (no spinners).
280
- * Git is NOT touched — if there's dirty state, action_required is returned
281
- * and the agent handles git itself, then re-runs with --skip-git-check.
282
- */
283
- async function agentShareFlow(cwd, apiKey, workspaceId, opts) {
284
- // ── Git sync — report state, don't touch git ──
285
- if (!opts.skipGitCheck) {
286
- // checkAndSyncGit will call agentActionRequired and exit if git is dirty
287
- await checkAndSyncGit(cwd, opts);
288
- // If we reach here, git is clean
289
- }
290
- const gitInfo = getGitInfo(cwd);
291
- // ── Resolve staging URL ──
292
- let stagingUrl;
293
- if (opts.deployment) {
294
- // --deployment flag provided
295
- stagingUrl = opts.deployment;
296
- if (!stagingUrl.startsWith("http"))
297
- stagingUrl = `https://${stagingUrl}`;
298
- }
299
- else {
300
- // Determine provider
301
- let providerId = opts.provider;
302
- if (!providerId) {
303
- // Auto-detect from local config files
304
- const gitRoot = getGitRoot(cwd);
305
- if (gitRoot && readLocalVercelProject(gitRoot)) {
306
- providerId = "vercel";
141
+ if (p.isCancel(message)) {
142
+ p.cancel("Cancelled.");
143
+ process.exit(0);
144
+ }
145
+ const spinner = p.spinner();
146
+ spinner.start("Committing and pushing...");
147
+ try {
148
+ commitAndPush(cwd, message, branch);
149
+ spinner.stop("Changes pushed!");
150
+ return { justPushed: true };
151
+ }
152
+ catch (e) {
153
+ const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
154
+ spinner.stop("Push failed.");
155
+ if (stderr)
156
+ p.log.message(pc.dim(stderr));
157
+ else if (e instanceof Error)
158
+ p.log.message(pc.dim(e.message));
159
+ p.log.warn("Continuing with existing deployment.");
160
+ return { justPushed: false };
307
161
  }
308
- else if (gitRoot && readLocalNetlifySite(gitRoot)) {
309
- providerId = "netlify";
162
+ }
163
+ case "unpushed": {
164
+ const { branch, unpushedCommits } = state;
165
+ const maxCommits = 5;
166
+ const commitLines = unpushedCommits.slice(0, maxCommits).map((line) => ` ${pc.dim(line)}`);
167
+ if (unpushedCommits.length > maxCommits) {
168
+ commitLines.push(` ${pc.dim(`... and ${unpushedCommits.length - maxCommits} more`)}`);
310
169
  }
311
- else {
170
+ if (isAgent) {
312
171
  agentActionRequired({
313
- type: "choose_provider",
314
- message: "Could not auto-detect deployment provider. Ask the user which one they use.",
172
+ type: "git_unpushed",
173
+ message: "There are unpushed commits that won't be in the deployment. Ask the user what to do.",
315
174
  choices: [
316
- { id: "vercel", label: "Vercel" },
317
- { id: "netlify", label: "Netlify" },
175
+ { id: "push", label: "Push these commits" },
176
+ { id: "continue", label: "Continue without pushing" },
318
177
  ],
319
- nextCommand: "inflight share --skip-git-check --provider <ID>",
178
+ instructions: {
179
+ push: "Run `git push` to push the commits. Then re-run with the next_command.",
180
+ continue: "Re-run with the next_command without pushing.",
181
+ },
182
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
320
183
  });
321
184
  }
322
- }
323
- const provider = providers.find((prov) => prov.id === providerId);
324
- if (!provider) {
325
- agentError({
326
- type: "invalid_provider",
327
- message: `Unknown provider "${providerId}". Use "vercel" or "netlify".`,
328
- });
329
- }
330
- stagingUrl = (await provider.resolve(cwd, gitInfo, {})) ?? undefined;
331
- if (stagingUrl && !stagingUrl.startsWith("http")) {
332
- stagingUrl = `https://${stagingUrl}`;
333
- }
334
- if (!stagingUrl) {
335
- agentError({
336
- type: "no_deployment",
337
- message: "Could not find a deployment URL. Provide one with --url.",
338
- suggestion: "inflight share --url <staging-url>",
339
- });
340
- }
341
- }
342
- // At this point stagingUrl is guaranteed set — provider.resolve returned a value or agentError exited
343
- const resolvedUrl = stagingUrl;
344
- // ── Resolve project ──
345
- let selectedProjectId;
346
- let overrideVersionId;
347
- if (opts.project) {
348
- if (opts.project !== "new") {
349
- selectedProjectId = opts.project;
350
- }
351
- }
352
- else {
353
- const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
354
- projects: [],
355
- }));
356
- if (projects.length > 0) {
357
- const currentBranch = gitInfo.branch;
358
- agentActionRequired({
359
- type: "choose_project",
360
- message: "Ask the user which project to share into. Present all choices below as a numbered list.",
361
- choices: [
362
- { id: "new", label: "Start a new project" },
363
- ...projects.map((proj) => ({
364
- id: proj.projectId,
365
- label: proj.latestVersion.title,
366
- hint: [
367
- proj.latestVersion.branch === currentBranch
368
- ? "current branch"
369
- : proj.latestVersion.branch,
370
- proj.latestVersion.branch === currentBranch ? "(recommended)" : undefined,
371
- ]
372
- .filter(Boolean)
373
- .join(" ") || undefined,
374
- })),
185
+ commitLines.push("", pc.yellow("Your deployment won't include these commits."));
186
+ p.log.warn("You have unpushed commits:\n" + commitLines.join("\n"));
187
+ const action = await p.select({
188
+ message: "What would you like to do?",
189
+ options: [
190
+ { value: "push", label: "Push (recommended)" },
191
+ { value: "continue", label: "Continue anyway — use existing deployment" },
375
192
  ],
376
- nextCommand: `inflight share --skip-git-check --deployment ${resolvedUrl} --project <ID>`,
377
193
  });
194
+ if (p.isCancel(action)) {
195
+ p.cancel("Cancelled.");
196
+ process.exit(0);
197
+ }
198
+ if (action === "continue") {
199
+ return { justPushed: false };
200
+ }
201
+ const spinner = p.spinner();
202
+ spinner.start("Pushing...");
203
+ try {
204
+ pushBranch(cwd, branch);
205
+ spinner.stop("Changes pushed!");
206
+ return { justPushed: true };
207
+ }
208
+ catch (e) {
209
+ const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
210
+ spinner.stop("Push failed.");
211
+ if (stderr)
212
+ p.log.message(pc.dim(stderr));
213
+ else if (e instanceof Error)
214
+ p.log.message(pc.dim(e.message));
215
+ p.log.warn("Continuing with existing deployment.");
216
+ return { justPushed: false };
217
+ }
378
218
  }
379
- }
380
- // ── Resolve override vs new version ──
381
- if (selectedProjectId && !opts.versionMode) {
382
- const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
383
- projects: [],
384
- }));
385
- const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
386
- if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
387
- agentActionRequired({
388
- type: "choose_override",
389
- message: `"${selectedProject.latestVersion.title}" has no feedback yet. Ask the user whether to update it or create a new version.`,
390
- choices: [
391
- {
392
- id: "override",
393
- label: "Update its staging URL",
394
- hint: `replace with ${new URL(resolvedUrl).hostname}`,
395
- },
396
- {
397
- id: "new_version",
398
- label: "Add a new version",
399
- hint: "keep both in version history",
219
+ case "no_remote": {
220
+ const { branch } = state;
221
+ if (!hasCommitsAhead(cwd)) {
222
+ if (isAgent) {
223
+ agentError({
224
+ type: "no_changes",
225
+ message: `No changes on branch "${branch}" yet. Make some changes and push before sharing.`,
226
+ });
227
+ }
228
+ p.log.warn("You haven't made any changes on this branch yet. There's nothing to deploy.");
229
+ process.exit(0);
230
+ }
231
+ if (isAgent) {
232
+ agentActionRequired({
233
+ type: "git_no_remote",
234
+ message: `Branch "${branch}" hasn't been pushed yet. Push it to create a deployment.`,
235
+ choices: [{ id: "push", label: "Push to create a deployment" }],
236
+ instructions: {
237
+ push: `Run \`git push -u origin ${branch}\`. Then re-run with the next_command.`,
400
238
  },
401
- ],
402
- instructions: {
403
- override: `Re-run with: inflight share --skip-git-check --deployment ${resolvedUrl} --project ${selectedProjectId} --version-mode override`,
404
- new_version: `Re-run with: inflight share --skip-git-check --deployment ${resolvedUrl} --project ${selectedProjectId} --version-mode new`,
405
- },
406
- nextCommand: `inflight share --skip-git-check --deployment ${resolvedUrl} --project ${selectedProjectId} --version-mode <override|new>`,
239
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
240
+ });
241
+ }
242
+ p.log.warn("Your changes haven't been pushed yet there's no deployment to share.");
243
+ const confirm = await p.confirm({
244
+ message: "Push now?",
407
245
  });
246
+ if (p.isCancel(confirm) || !confirm) {
247
+ p.cancel("Push your changes first, then run inflight share again.");
248
+ process.exit(0);
249
+ }
250
+ const spinner = p.spinner();
251
+ spinner.start("Pushing...");
252
+ try {
253
+ pushBranch(cwd, branch);
254
+ spinner.stop("Changes pushed!");
255
+ return { justPushed: true };
256
+ }
257
+ catch (e) {
258
+ const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
259
+ spinner.stop("Push failed.");
260
+ if (stderr)
261
+ p.log.message(pc.dim(stderr));
262
+ else if (e instanceof Error)
263
+ p.log.message(pc.dim(e.message));
264
+ p.log.info("Ask your AI agent to push your changes, then run inflight share again.");
265
+ process.exit(1);
266
+ }
408
267
  }
409
268
  }
410
- if (opts.versionMode === "override" && selectedProjectId) {
411
- const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
412
- projects: [],
413
- }));
414
- const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
415
- if (selectedProject) {
416
- overrideVersionId = selectedProject.latestVersion.id;
417
- }
418
- }
419
- // ── Create version ──
420
- const result = await apiCreateVersion({
421
- apiKey,
422
- workspaceId,
423
- stagingUrl: resolvedUrl,
424
- gitInfo,
425
- ...(selectedProjectId && { projectId: selectedProjectId }),
426
- ...(overrideVersionId && { overrideVersionId }),
427
- }).catch((e) => {
428
- agentError({ type: "create_failed", message: e.message });
429
- });
430
- await open(appendWidgetToken(resolvedUrl, result.widgetToken));
431
- agentSuccess({
432
- stagingUrl: resolvedUrl,
433
- ...result,
434
- isOverride: !!overrideVersionId,
435
- });
436
269
  }
437
270
  export async function shareCommand(opts = {}) {
438
271
  const cwd = process.cwd();
439
- // TODO: Add a step to login if not authenticated
440
272
  // ── Step 1: Auth ──
441
273
  const auth = readGlobalAuth();
442
274
  if (!auth) {
@@ -456,6 +288,14 @@ export async function shareCommand(opts = {}) {
456
288
  process.exit(1);
457
289
  }
458
290
  let gitInfo = getGitInfo(cwd);
291
+ const gitRoot = getGitRoot(cwd);
292
+ if (!gitInfo || !gitRoot) {
293
+ if (isAgent) {
294
+ agentError({ type: "not_git_repo", message: "Not a git repository." });
295
+ }
296
+ p.log.error("Not a git repository.");
297
+ process.exit(1);
298
+ }
459
299
  // ── Step 2: Resolve workspace ──
460
300
  const me = await apiGetMe(auth.apiKey).catch((e) => {
461
301
  if (isAgent)
@@ -472,85 +312,97 @@ export async function shareCommand(opts = {}) {
472
312
  explicitId: opts.workspace,
473
313
  commandForNext: "inflight share",
474
314
  });
475
- // ── Fast path: URL provided (agent / scripting) ──
315
+ // ── Step 3: Resolve staging URL ──
316
+ let stagingUrl = null;
317
+ let justPushed = false;
318
+ let provider;
476
319
  if (opts.url) {
477
- let stagingUrl = opts.url;
478
- if (!stagingUrl.startsWith("http")) {
479
- stagingUrl = `https://${stagingUrl}`;
480
- }
481
- if (!isValidHostedUrl(stagingUrl)) {
482
- const message = stagingUrl.includes("localhost")
483
- ? "Inflight needs a hosted URL — localhost isn't accessible to your team. Deploy to Vercel, Netlify, or another hosting provider first."
484
- : "Must be a hosted URL with a domain (e.g., my-branch.vercel.app)";
485
- if (isAgent)
486
- agentError({ type: "invalid_url", message });
487
- if (opts.json) {
488
- console.log(JSON.stringify({ error: "invalid_url", message }));
489
- }
490
- else {
491
- p.log.error(message);
320
+ // --url flag provided (agent re-invocation or scripting)
321
+ stagingUrl = opts.url;
322
+ }
323
+ else if (opts.provider) {
324
+ // --provider flag explicitly passed — validate it
325
+ provider = providers.find((prov) => prov.id === opts.provider);
326
+ if (!provider) {
327
+ if (isAgent) {
328
+ agentError({
329
+ type: "invalid_provider",
330
+ message: `Unknown provider "${opts.provider}". Use "vercel" or "netlify".`,
331
+ });
492
332
  }
333
+ p.log.error(`Unknown provider "${opts.provider}".`);
493
334
  process.exit(1);
494
335
  }
495
- const result = await apiCreateVersion({
496
- apiKey: auth.apiKey,
497
- workspaceId,
498
- stagingUrl,
499
- gitInfo,
500
- ...(opts.project && opts.project !== "new" && { projectId: opts.project }),
501
- }).catch((e) => {
502
- if (isAgent)
503
- agentError({ type: "create_failed", message: e.message });
504
- if (opts.json) {
505
- console.log(JSON.stringify({ error: "create_failed", message: e.message }));
336
+ }
337
+ else {
338
+ // No URL or provider — detect or prompt
339
+ let providerId;
340
+ // Auto-detect from local config files
341
+ const hasVercel = !!readLocalVercelProject(gitRoot);
342
+ const hasNetlify = !!readLocalNetlifySite(gitRoot);
343
+ if (hasVercel && !hasNetlify) {
344
+ providerId = "vercel";
345
+ }
346
+ else if (hasNetlify && !hasVercel) {
347
+ providerId = "netlify";
348
+ }
349
+ else if (isAgent) {
350
+ // Both exist (monorepo), neither exists, or can't determine — ask agent
351
+ agentActionRequired({
352
+ type: "choose_provider",
353
+ message: "Could not auto-detect deployment provider. Ask the user which one they use.",
354
+ choices: [
355
+ { id: "vercel", label: "Vercel" },
356
+ { id: "netlify", label: "Netlify" },
357
+ { id: "manual", label: "Paste a URL" },
358
+ ],
359
+ instructions: {
360
+ vercel: buildNextCommand("inflight share --skip-git-check --provider vercel", opts),
361
+ netlify: buildNextCommand("inflight share --skip-git-check --provider netlify", opts),
362
+ manual: "Ask the user for their staging URL, then re-run with: inflight share --url <staging-url>",
363
+ },
364
+ nextCommand: buildNextCommand("inflight share --skip-git-check --provider <ID>", opts),
365
+ });
366
+ }
367
+ else {
368
+ // Can't auto-detect — prompt user
369
+ const providerChoice = await p.select({
370
+ message: "Where is your staging URL hosted?",
371
+ options: [
372
+ ...providers.map((prov) => ({ value: prov.id, label: prov.label })),
373
+ { value: "manual", label: "Paste a URL" },
374
+ ],
375
+ });
376
+ if (p.isCancel(providerChoice)) {
377
+ p.cancel("Cancelled.");
378
+ process.exit(0);
506
379
  }
507
- else {
508
- p.log.error(e.message);
380
+ if (providerChoice !== "manual") {
381
+ providerId = providerChoice;
509
382
  }
510
- process.exit(1);
511
- });
512
- if (isAgent) {
513
- await open(appendWidgetToken(stagingUrl, result.widgetToken));
514
- agentSuccess({ stagingUrl, ...result });
515
383
  }
516
- if (opts.json) {
517
- console.log(JSON.stringify({ success: true, stagingUrl, ...result }));
518
- }
519
- else {
520
- p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
521
- p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
384
+ if (providerId) {
385
+ provider = providers.find((prov) => prov.id === providerId);
522
386
  }
523
- await open(appendWidgetToken(stagingUrl, result.widgetToken));
524
- return;
525
- }
526
- // ── Agent mode: structured flow with action_required for every choice ──
527
- if (isAgent) {
528
- await agentShareFlow(cwd, auth.apiKey, workspaceId, opts);
529
- return;
530
- }
531
- // Resolve staging URL
532
- const providerChoice = await p.select({
533
- message: "Where is your staging URL hosted?",
534
- options: [
535
- ...providers.map((prov) => ({ value: prov.id, label: prov.label })),
536
- { value: "manual", label: "Paste a URL" },
537
- ],
538
- });
539
- if (p.isCancel(providerChoice)) {
540
- p.cancel("Cancelled.");
541
- process.exit(0);
542
387
  }
543
- let stagingUrl;
544
- const provider = providers.find((prov) => prov.id === providerChoice);
388
+ // Resolve URL from provider
545
389
  if (provider) {
546
- const { justPushed } = await checkAndSyncGit(cwd);
547
- // Re-fetch git info after potential commit/push so provider AND apiCreateVersion see latest state
548
- if (justPushed) {
549
- gitInfo = getGitInfo(cwd);
390
+ if (!opts.skipGitCheck) {
391
+ ({ justPushed } = await checkAndSyncGit(cwd, opts));
392
+ if (justPushed) {
393
+ gitInfo = getGitInfo(cwd) ?? gitInfo;
394
+ }
550
395
  }
551
- stagingUrl = (await provider.resolve(cwd, gitInfo, { justPushed })) ?? undefined;
396
+ stagingUrl = await provider.resolve(gitRoot, gitInfo, { justPushed });
552
397
  }
553
398
  if (!stagingUrl) {
399
+ if (isAgent) {
400
+ agentError({
401
+ type: "no_deployment",
402
+ message: "Could not find a deployment URL. Provide one with --url.",
403
+ suggestion: "inflight share --url <staging-url>",
404
+ });
405
+ }
554
406
  const input = await p.text({
555
407
  message: "Staging URL",
556
408
  placeholder: "my-branch.vercel.app",
@@ -582,68 +434,127 @@ export async function shareCommand(opts = {}) {
582
434
  // ── Step 4: Project selection ──
583
435
  let selectedProjectId;
584
436
  let overrideVersionId;
585
- const { projects } = await apiGetRecentProjects(auth.apiKey, workspaceId, 20).catch(() => ({
586
- projects: [],
587
- }));
588
- if (projects.length > 0) {
589
- // Pre-select project whose latest version matches the current branch
590
- const currentBranch = gitInfo.branch;
591
- const branchMatch = currentBranch
592
- ? projects.find((proj) => proj.latestVersion.branch === currentBranch)
593
- : undefined;
594
- // Move branch-matched project to the top of the list
595
- const sortedProjects = branchMatch
596
- ? [branchMatch, ...projects.filter((proj) => proj.projectId !== branchMatch.projectId)]
597
- : projects;
598
- const projectChoice = await scrollableSelect({
599
- message: "Create a new project in Inflight, or update an existing one?",
600
- maxItems: 5,
601
- ...(branchMatch && { initialValue: branchMatch.projectId }),
602
- options: [
603
- { value: "new", label: "Start a new project" },
604
- ...sortedProjects.map((proj) => ({
605
- value: proj.projectId,
606
- label: `"${proj.latestVersion.title}"`,
607
- hint: proj.latestVersion.branch === currentBranch
608
- ? `${formatRelativeTime(proj.latestVersion.createdAt)} · current branch (${currentBranch})`
609
- : formatRelativeTime(proj.latestVersion.createdAt),
610
- })),
611
- ],
612
- });
613
- if (p.isCancel(projectChoice)) {
614
- p.cancel("Cancelled.");
615
- process.exit(0);
437
+ if (opts.project) {
438
+ // --project flag provided (agent re-invocation)
439
+ if (opts.project !== "new") {
440
+ selectedProjectId = opts.project;
441
+ }
442
+ }
443
+ else {
444
+ const { projects } = await apiGetRecentProjects(auth.apiKey, workspaceId, 20).catch(() => ({
445
+ projects: [],
446
+ }));
447
+ if (projects.length > 0) {
448
+ const branchMatches = gitInfo.branch
449
+ ? projects.filter((proj) => proj.latestVersion.branch === gitInfo.branch)
450
+ : [];
451
+ const rest = projects.filter((proj) => !branchMatches.includes(proj));
452
+ const sorted = [...branchMatches, ...rest];
453
+ if (isAgent) {
454
+ agentActionRequired({
455
+ type: "choose_project",
456
+ message: projects.length > 5
457
+ ? "Ask the user which project to share into. Show the first 5 options. If the user wants more, show the rest."
458
+ : "Ask the user which project to share into.",
459
+ choices: [
460
+ { id: "new", label: "Start a new project" },
461
+ ...sorted.map((proj) => ({
462
+ id: proj.projectId,
463
+ label: proj.latestVersion.title,
464
+ hint: branchMatches.includes(proj)
465
+ ? `${formatRelativeTime(proj.latestVersion.createdAt)} · current branch (${gitInfo.branch})`
466
+ : formatRelativeTime(proj.latestVersion.createdAt),
467
+ })),
468
+ ],
469
+ nextCommand: `inflight share --skip-git-check --url ${stagingUrl} --project <ID>`,
470
+ });
471
+ }
472
+ const firstBranchMatch = branchMatches[0];
473
+ const projectChoice = await scrollableSelect({
474
+ message: "Create a new project in Inflight, or update an existing one?",
475
+ maxItems: 5,
476
+ ...(firstBranchMatch && { initialValue: firstBranchMatch.projectId }),
477
+ options: [
478
+ { value: "new", label: "Start a new project" },
479
+ ...sorted.map((proj) => ({
480
+ value: proj.projectId,
481
+ label: `"${proj.latestVersion.title}"`,
482
+ hint: branchMatches.includes(proj)
483
+ ? `${formatRelativeTime(proj.latestVersion.createdAt)} · current branch (${gitInfo.branch})`
484
+ : formatRelativeTime(proj.latestVersion.createdAt),
485
+ })),
486
+ ],
487
+ });
488
+ if (p.isCancel(projectChoice)) {
489
+ p.cancel("Cancelled.");
490
+ process.exit(0);
491
+ }
492
+ if (projectChoice !== "new") {
493
+ selectedProjectId = projectChoice;
494
+ }
616
495
  }
617
- if (projectChoice !== "new") {
618
- selectedProjectId = projectChoice;
619
- // Check for override opportunity only when latest version has no feedback
620
- const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
621
- if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
622
- const overrideChoice = await p.select({
623
- message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
624
- options: [
496
+ }
497
+ // ── Step 5: Override check ──
498
+ if (selectedProjectId && opts.versionMode !== "new") {
499
+ const { projects } = await apiGetRecentProjects(auth.apiKey, workspaceId, 20).catch(() => ({
500
+ projects: [],
501
+ }));
502
+ const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
503
+ if (opts.versionMode === "override") {
504
+ if (selectedProject) {
505
+ overrideVersionId = selectedProject.latestVersion.id;
506
+ }
507
+ }
508
+ else if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
509
+ // No explicit mode and latest version has no feedback — ask
510
+ if (isAgent) {
511
+ agentActionRequired({
512
+ type: "choose_override",
513
+ message: `"${selectedProject.latestVersion.title}" has no feedback yet. Ask the user whether to update it or create a new version.`,
514
+ choices: [
625
515
  {
626
- value: "override",
516
+ id: "override",
627
517
  label: "Update its staging URL",
628
518
  hint: `replace with ${new URL(stagingUrl).hostname}`,
629
519
  },
630
520
  {
631
- value: "new_version",
521
+ id: "new_version",
632
522
  label: "Add a new version",
633
523
  hint: "keep both in version history",
634
524
  },
635
525
  ],
526
+ instructions: {
527
+ override: `Re-run with: inflight share --skip-git-check --url ${stagingUrl} --project ${selectedProjectId} --version-mode override`,
528
+ new_version: `Re-run with: inflight share --skip-git-check --url ${stagingUrl} --project ${selectedProjectId} --version-mode new`,
529
+ },
530
+ nextCommand: `inflight share --skip-git-check --url ${stagingUrl} --project ${selectedProjectId} --version-mode <override|new>`,
636
531
  });
637
- if (p.isCancel(overrideChoice)) {
638
- p.cancel("Cancelled.");
639
- process.exit(0);
640
- }
641
- if (overrideChoice === "override") {
642
- overrideVersionId = selectedProject.latestVersion.id;
643
- }
532
+ }
533
+ const overrideChoice = await p.select({
534
+ message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
535
+ options: [
536
+ {
537
+ value: "override",
538
+ label: "Update its staging URL",
539
+ hint: `replace with ${new URL(stagingUrl).hostname}`,
540
+ },
541
+ {
542
+ value: "new_version",
543
+ label: "Add a new version",
544
+ hint: "keep both in version history",
545
+ },
546
+ ],
547
+ });
548
+ if (p.isCancel(overrideChoice)) {
549
+ p.cancel("Cancelled.");
550
+ process.exit(0);
551
+ }
552
+ if (overrideChoice === "override") {
553
+ overrideVersionId = selectedProject.latestVersion.id;
644
554
  }
645
555
  }
646
556
  }
557
+ // ── Step 6: Create version ──
647
558
  const result = await apiCreateVersion({
648
559
  apiKey: auth.apiKey,
649
560
  workspaceId,
@@ -652,9 +563,19 @@ export async function shareCommand(opts = {}) {
652
563
  ...(selectedProjectId && { projectId: selectedProjectId }),
653
564
  ...(overrideVersionId && { overrideVersionId }),
654
565
  }).catch((e) => {
566
+ if (isAgent)
567
+ agentError({ type: "create_failed", message: e.message });
655
568
  p.log.error(e.message);
656
569
  process.exit(1);
657
570
  });
571
+ await open(appendWidgetToken(stagingUrl, result.widgetToken));
572
+ if (isAgent) {
573
+ agentSuccess({
574
+ stagingUrl,
575
+ ...result,
576
+ isOverride: !!overrideVersionId,
577
+ });
578
+ }
658
579
  p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
659
580
  if (overrideVersionId) {
660
581
  p.outro(pc.green("✓ Staging URL updated") + " — opening in browser...");
@@ -662,5 +583,4 @@ export async function shareCommand(opts = {}) {
662
583
  else {
663
584
  p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
664
585
  }
665
- await open(appendWidgetToken(stagingUrl, result.widgetToken));
666
586
  }