inflight-cli 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,17 @@ 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
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
107
+ });
108
+ }
77
109
  lines.push("", pc.yellow("Your deployment won't include these changes."));
78
110
  p.log.warn("You have uncommitted changes:\n" + lines.join("\n"));
79
111
  const action = await p.select({
@@ -112,7 +144,12 @@ async function checkAndSyncGit(cwd) {
112
144
  return { justPushed: true };
113
145
  }
114
146
  catch (e) {
115
- spinner.stop(`Push failed: ${e.message}`);
147
+ const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
148
+ spinner.stop("Push failed.");
149
+ if (stderr)
150
+ p.log.message(pc.dim(stderr));
151
+ else if (e instanceof Error)
152
+ p.log.message(pc.dim(e.message));
116
153
  p.log.warn("Continuing with existing deployment.");
117
154
  return { justPushed: false };
118
155
  }
@@ -123,6 +160,17 @@ async function checkAndSyncGit(cwd) {
123
160
  if (state.unpushedCommits.length > 5) {
124
161
  commitLines.push(` ${pc.dim(`... and ${state.unpushedCommits.length - 5} more`)}`);
125
162
  }
163
+ if (isAgent) {
164
+ agentActionRequired({
165
+ type: "git_unpushed",
166
+ message: "You have unpushed commits. Your deployment won't include them.",
167
+ choices: [
168
+ { id: "push", label: "Push these commits" },
169
+ { id: "continue", label: "Continue without pushing" },
170
+ ],
171
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
172
+ });
173
+ }
126
174
  commitLines.push("", pc.yellow("Your deployment won't include these commits."));
127
175
  p.log.warn("You have unpushed commits:\n" + commitLines.join("\n"));
128
176
  const action = await p.select({
@@ -147,7 +195,12 @@ async function checkAndSyncGit(cwd) {
147
195
  return { justPushed: true };
148
196
  }
149
197
  catch (e) {
150
- spinner.stop(`Push failed: ${e.message}`);
198
+ const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
199
+ spinner.stop("Push failed.");
200
+ if (stderr)
201
+ p.log.message(pc.dim(stderr));
202
+ else if (e instanceof Error)
203
+ p.log.message(pc.dim(e.message));
151
204
  p.log.warn("Continuing with existing deployment.");
152
205
  return { justPushed: false };
153
206
  }
@@ -160,6 +213,17 @@ async function checkAndSyncGit(cwd) {
160
213
  p.log.info(`Branch ${pc.bold(branch)} has no new commits — using existing deployments.`);
161
214
  return { justPushed: false };
162
215
  }
216
+ if (isAgent) {
217
+ agentActionRequired({
218
+ type: "git_no_remote",
219
+ message: `Branch "${branch}" hasn't been pushed yet — no deployment exists.`,
220
+ choices: [
221
+ { id: "push", label: "Push to create a deployment" },
222
+ { id: "continue", label: "Continue without pushing" },
223
+ ],
224
+ nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
225
+ });
226
+ }
163
227
  p.log.warn(`Branch ${pc.bold(branch)} hasn't been pushed yet — no deployment exists.`);
164
228
  const confirm = await p.confirm({
165
229
  message: "Push to create a deployment?",
@@ -179,18 +243,182 @@ async function checkAndSyncGit(cwd) {
179
243
  return { justPushed: true };
180
244
  }
181
245
  catch (e) {
182
- spinner.stop(`Push failed: ${e.message}`);
246
+ const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
247
+ spinner.stop("Push failed.");
248
+ if (stderr)
249
+ p.log.message(pc.dim(stderr));
250
+ else if (e instanceof Error)
251
+ p.log.message(pc.dim(e.message));
183
252
  return { justPushed: false };
184
253
  }
185
254
  }
186
255
  return { justPushed: false };
187
256
  }
257
+ /**
258
+ * Agent mode share flow.
259
+ * Mirrors every human prompt with action_required JSON.
260
+ * Polling runs normally (no spinners).
261
+ * Git is NOT touched — if there's dirty state, action_required is returned
262
+ * and the agent handles git itself, then re-runs with --skip-git-check.
263
+ */
264
+ async function agentShareFlow(cwd, apiKey, workspaceId, opts) {
265
+ // ── Git sync — report state, don't touch git ──
266
+ if (!opts.skipGitCheck) {
267
+ // checkAndSyncGit will call agentActionRequired and exit if git is dirty
268
+ await checkAndSyncGit(cwd, opts);
269
+ // If we reach here, git is clean
270
+ }
271
+ const gitInfo = getGitInfo(cwd);
272
+ // ── Resolve staging URL ──
273
+ let stagingUrl;
274
+ if (opts.deployment) {
275
+ // --deployment flag provided
276
+ stagingUrl = opts.deployment;
277
+ if (!stagingUrl.startsWith("http"))
278
+ stagingUrl = `https://${stagingUrl}`;
279
+ }
280
+ else {
281
+ // Determine provider
282
+ let providerId = opts.provider;
283
+ if (!providerId) {
284
+ // Auto-detect from local config files
285
+ const gitRoot = getGitRoot(cwd);
286
+ if (gitRoot && readLocalVercelProject(gitRoot)) {
287
+ providerId = "vercel";
288
+ }
289
+ else if (gitRoot && readLocalNetlifySite(gitRoot)) {
290
+ providerId = "netlify";
291
+ }
292
+ else {
293
+ agentActionRequired({
294
+ type: "choose_provider",
295
+ message: "Could not auto-detect deployment provider.",
296
+ choices: [
297
+ { id: "vercel", label: "Vercel" },
298
+ { id: "netlify", label: "Netlify" },
299
+ ],
300
+ nextCommand: "inflight share --skip-git-check --provider <ID>",
301
+ });
302
+ }
303
+ }
304
+ const provider = providers.find((prov) => prov.id === providerId);
305
+ if (!provider) {
306
+ agentError({
307
+ type: "invalid_provider",
308
+ message: `Unknown provider "${providerId}". Use "vercel" or "netlify".`,
309
+ });
310
+ }
311
+ stagingUrl = (await provider.resolve(cwd, gitInfo, {})) ?? undefined;
312
+ if (stagingUrl && !stagingUrl.startsWith("http")) {
313
+ stagingUrl = `https://${stagingUrl}`;
314
+ }
315
+ if (!stagingUrl) {
316
+ agentError({
317
+ type: "no_deployment",
318
+ message: "Could not find a deployment URL. Provide one with --url.",
319
+ suggestion: "inflight share --url <staging-url>",
320
+ });
321
+ }
322
+ }
323
+ // ── Resolve project ──
324
+ let selectedProjectId;
325
+ let overrideVersionId;
326
+ if (opts.project) {
327
+ if (opts.project !== "new") {
328
+ selectedProjectId = opts.project;
329
+ }
330
+ }
331
+ else {
332
+ const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
333
+ projects: [],
334
+ }));
335
+ if (projects.length > 0) {
336
+ const currentBranch = gitInfo.branch;
337
+ agentActionRequired({
338
+ type: "choose_project",
339
+ message: "Select a project or create new.",
340
+ choices: [
341
+ { id: "new", label: "Start a new project" },
342
+ ...projects.map((proj) => ({
343
+ id: proj.projectId,
344
+ label: proj.latestVersion.title,
345
+ hint: [
346
+ proj.latestVersion.branch === currentBranch ? "current branch" : proj.latestVersion.branch,
347
+ proj.latestVersion.branch === currentBranch ? "(recommended)" : undefined,
348
+ ]
349
+ .filter(Boolean)
350
+ .join(" ") || undefined,
351
+ })),
352
+ ],
353
+ nextCommand: `inflight share --skip-git-check --deployment ${stagingUrl} --project <ID>`,
354
+ });
355
+ }
356
+ }
357
+ // ── Resolve override vs new version ──
358
+ if (selectedProjectId && !opts.override) {
359
+ const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
360
+ projects: [],
361
+ }));
362
+ const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
363
+ if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
364
+ agentActionRequired({
365
+ type: "choose_override",
366
+ message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
367
+ choices: [
368
+ {
369
+ id: "override",
370
+ label: "Update its staging URL",
371
+ hint: `replace with ${new URL(stagingUrl).hostname}`,
372
+ },
373
+ {
374
+ id: "new_version",
375
+ label: "Add a new version",
376
+ hint: "keep both in version history",
377
+ },
378
+ ],
379
+ nextCommand: `inflight share --skip-git-check --deployment ${stagingUrl} --project ${selectedProjectId} --override`,
380
+ });
381
+ }
382
+ }
383
+ if (opts.override && selectedProjectId) {
384
+ const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
385
+ projects: [],
386
+ }));
387
+ const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
388
+ if (selectedProject) {
389
+ overrideVersionId = selectedProject.latestVersion.id;
390
+ }
391
+ }
392
+ // ── Create version ──
393
+ const result = await apiCreateVersion({
394
+ apiKey,
395
+ workspaceId,
396
+ stagingUrl: stagingUrl,
397
+ gitInfo,
398
+ ...(selectedProjectId && { projectId: selectedProjectId }),
399
+ ...(overrideVersionId && { overrideVersionId }),
400
+ }).catch((e) => {
401
+ agentError({ type: "create_failed", message: e.message });
402
+ });
403
+ agentSuccess({
404
+ stagingUrl,
405
+ ...result,
406
+ isOverride: !!overrideVersionId,
407
+ });
408
+ }
188
409
  export async function shareCommand(opts = {}) {
189
410
  const cwd = process.cwd();
190
411
  // TODO: Add a step to login if not authenticated
191
412
  // ── Step 1: Auth ──
192
413
  const auth = readGlobalAuth();
193
414
  if (!auth) {
415
+ if (isAgent) {
416
+ agentError({
417
+ type: "not_authenticated",
418
+ message: "Not logged in. Run inflight setup first.",
419
+ suggestion: "inflight setup",
420
+ });
421
+ }
194
422
  if (opts.json) {
195
423
  console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight setup first." }));
196
424
  }
@@ -202,6 +430,8 @@ export async function shareCommand(opts = {}) {
202
430
  let gitInfo = getGitInfo(cwd);
203
431
  // ── Step 2: Resolve workspace ──
204
432
  const me = await apiGetMe(auth.apiKey).catch((e) => {
433
+ if (isAgent)
434
+ agentError({ type: "api_error", message: e.message });
205
435
  if (opts.json) {
206
436
  console.log(JSON.stringify({ error: "api_error", message: e.message }));
207
437
  }
@@ -210,50 +440,10 @@ export async function shareCommand(opts = {}) {
210
440
  }
211
441
  process.exit(1);
212
442
  });
213
- const workspaces = me.workspaces;
214
- let workspaceId;
215
- const savedConfig = readWorkspaceConfig();
216
- const savedWorkspace = savedConfig ? workspaces.find((w) => w.id === savedConfig.workspaceId) : null;
217
- if (savedWorkspace) {
218
- workspaceId = savedWorkspace.id;
219
- }
220
- else if (workspaces.length === 0) {
221
- if (opts.json) {
222
- console.log(JSON.stringify({
223
- error: "no_workspaces",
224
- message: "No workspaces found. Create one at inflight.co first.",
225
- }));
226
- }
227
- else {
228
- p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
229
- }
230
- process.exit(1);
231
- }
232
- else if (workspaces.length === 1) {
233
- workspaceId = workspaces[0].id;
234
- if (!opts.json)
235
- p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
236
- }
237
- else {
238
- if (opts.json) {
239
- console.log(JSON.stringify({
240
- error: "no_workspace_set",
241
- message: "Multiple workspaces found. Run 'inflight workspace --set=ID' first.",
242
- workspaces: workspaces.map((w) => ({ id: w.id, name: w.name })),
243
- }));
244
- process.exit(1);
245
- }
246
- const selected = await p.select({
247
- message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
248
- options: workspaces.map((w) => ({ value: w.id, label: w.name })),
249
- });
250
- if (p.isCancel(selected)) {
251
- p.cancel("Cancelled.");
252
- process.exit(0);
253
- }
254
- workspaceId = selected;
255
- }
256
- writeWorkspaceConfig({ workspaceId });
443
+ const workspaceId = await resolveWorkspace(me.workspaces, {
444
+ explicitId: opts.workspace,
445
+ commandForNext: "inflight share",
446
+ });
257
447
  // ── Fast path: URL provided (agent / scripting) ──
258
448
  if (opts.url) {
259
449
  let stagingUrl = opts.url;
@@ -264,6 +454,8 @@ export async function shareCommand(opts = {}) {
264
454
  const message = stagingUrl.includes("localhost")
265
455
  ? "Inflight needs a hosted URL — localhost isn't accessible to your team. Deploy to Vercel, Netlify, or another hosting provider first."
266
456
  : "Must be a hosted URL with a domain (e.g., my-branch.vercel.app)";
457
+ if (isAgent)
458
+ agentError({ type: "invalid_url", message });
267
459
  if (opts.json) {
268
460
  console.log(JSON.stringify({ error: "invalid_url", message }));
269
461
  }
@@ -277,7 +469,10 @@ export async function shareCommand(opts = {}) {
277
469
  workspaceId,
278
470
  stagingUrl,
279
471
  gitInfo,
472
+ ...(opts.project && opts.project !== "new" && { projectId: opts.project }),
280
473
  }).catch((e) => {
474
+ if (isAgent)
475
+ agentError({ type: "create_failed", message: e.message });
281
476
  if (opts.json) {
282
477
  console.log(JSON.stringify({ error: "create_failed", message: e.message }));
283
478
  }
@@ -286,6 +481,8 @@ export async function shareCommand(opts = {}) {
286
481
  }
287
482
  process.exit(1);
288
483
  });
484
+ if (isAgent)
485
+ agentSuccess({ stagingUrl, ...result });
289
486
  if (opts.json) {
290
487
  console.log(JSON.stringify({ success: true, stagingUrl, ...result }));
291
488
  }
@@ -296,6 +493,11 @@ export async function shareCommand(opts = {}) {
296
493
  await open(stagingUrl);
297
494
  return;
298
495
  }
496
+ // ── Agent mode: structured flow with action_required for every choice ──
497
+ if (isAgent) {
498
+ await agentShareFlow(cwd, auth.apiKey, workspaceId, opts);
499
+ return;
500
+ }
299
501
  // Resolve staging URL
300
502
  const providerChoice = await p.select({
301
503
  message: "Where is your staging URL hosted?",
@@ -366,17 +568,15 @@ export async function shareCommand(opts = {}) {
366
568
  const projectChoice = await scrollableSelect({
367
569
  message: "Create a new project in Inflight, or update an existing one?",
368
570
  maxItems: 5,
571
+ ...(branchMatch && { initialValue: branchMatch.projectId }),
369
572
  options: [
370
573
  { value: "new", label: "Start a new project" },
371
574
  ...sortedProjects.map((proj) => ({
372
575
  value: proj.projectId,
373
576
  label: `"${proj.latestVersion.title}"`,
374
- hint: [
375
- formatRelativeTime(proj.latestVersion.createdAt),
376
- proj.latestVersion.branch === currentBranch ? `← current branch (${currentBranch})` : "",
377
- ]
378
- .filter(Boolean)
379
- .join(" · "),
577
+ hint: proj.latestVersion.branch === currentBranch
578
+ ? `${formatRelativeTime(proj.latestVersion.createdAt)} · current branch (${currentBranch})`
579
+ : formatRelativeTime(proj.latestVersion.createdAt),
380
580
  })),
381
581
  ],
382
582
  });
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,25 @@
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
+ }): never;
21
+ export declare function agentError(opts: {
22
+ type: string;
23
+ message: string;
24
+ suggestion?: string;
25
+ }): never;
@@ -0,0 +1,31 @@
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
+ }));
21
+ process.exit(0);
22
+ }
23
+ export function agentError(opts) {
24
+ console.log(JSON.stringify({
25
+ status: "error",
26
+ type: opts.type,
27
+ message: opts.message,
28
+ ...(opts.suggestion && { suggestion: opts.suggestion }),
29
+ }));
30
+ process.exit(1);
31
+ }
@@ -5,6 +5,7 @@ import { getGitRoot } from "./git.js";
5
5
  const CANDIDATE_PATTERNS = [
6
6
  "package.json",
7
7
  "index.html",
8
+ "src/index.html",
8
9
  "app/layout.tsx",
9
10
  "app/layout.jsx",
10
11
  "src/app/layout.tsx",
@@ -16,8 +17,13 @@ const CANDIDATE_PATTERNS = [
16
17
  "app/root.tsx",
17
18
  "app/root.jsx",
18
19
  "src/app.html",
20
+ "src/routes/+layout.svelte",
19
21
  "nuxt.config.ts",
20
22
  "nuxt.config.js",
23
+ "app.vue",
24
+ "layouts/default.vue",
25
+ "gatsby-ssr.tsx",
26
+ "gatsby-ssr.jsx",
21
27
  "src/layouts/Layout.astro",
22
28
  "src/layouts/BaseLayout.astro",
23
29
  ];
@@ -55,7 +61,7 @@ export function gatherProjectContext(cwd) {
55
61
  fileTree.push(relative(root, join(dir, entry)));
56
62
  }
57
63
  // Also list key subdirectories
58
- for (const sub of ["app", "src", "src/app", "src/layouts", "pages", "src/pages"]) {
64
+ for (const sub of ["app", "src", "src/app", "src/layouts", "src/routes", "pages", "src/pages", "layouts"]) {
59
65
  const subDir = join(dir, sub);
60
66
  if (!existsSync(subDir))
61
67
  continue;
@@ -1,4 +1,7 @@
1
- export declare function ensureNetlifyCli(log?: (msg: string) => void): Promise<boolean>;
1
+ export declare function ensureNetlifyCli(log?: (msg: string) => void): Promise<{
2
+ ok: boolean;
3
+ error?: string;
4
+ }>;
2
5
  /**
3
6
  * Gets the Netlify auth token non-interactively.
4
7
  * Priority: NETLIFY_AUTH_TOKEN env var → CLI config file.
@@ -16,14 +16,19 @@ function hasNetlifyCli() {
16
16
  }
17
17
  export async function ensureNetlifyCli(log) {
18
18
  if (hasNetlifyCli())
19
- return true;
19
+ return { ok: true };
20
20
  log?.("Installing Netlify CLI...");
21
21
  try {
22
22
  await execAsync("npm install -g netlify-cli");
23
- return true;
23
+ return { ok: true };
24
24
  }
25
- catch {
26
- return false;
25
+ catch (e) {
26
+ const stderr = e && typeof e === "object" && "stderr" in e && typeof e.stderr === "string"
27
+ ? e.stderr
28
+ : e instanceof Error
29
+ ? e.message
30
+ : "Unknown error";
31
+ return { ok: false, error: stderr };
27
32
  }
28
33
  }
29
34
  /**
@@ -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
+ }