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.
@@ -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,13 +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 { installSkill } from "../lib/skill.js";
10
+ import { isAgent, agentSuccess, agentError } from "../lib/agent.js";
11
+ import { resolveWorkspace } from "../lib/resolve-workspace.js";
11
12
  function execSyncErrorDetail(err) {
12
13
  if (err !== null && typeof err === "object" && "stderr" in err) {
13
14
  const b = err.stderr;
@@ -22,72 +23,81 @@ function execSyncErrorDetail(err) {
22
23
  }
23
24
  return "";
24
25
  }
25
- export async function setupCommand() {
26
+ export async function setupCommand(opts = {}) {
26
27
  const cwd = process.cwd();
27
- // ── Step 1: Install agent skill ──
28
- p.log.step("Installing agent skill...");
29
- await installSkill();
30
- // ── Step 2: Authenticate ──
28
+ // ── Pre-flight: warn if not a git repo ──
29
+ if (!isGitRepo(cwd) && !isAgent) {
30
+ p.log.warn("This directory is not a git repository.\n" +
31
+ " Inflight works best inside a git repo so it can track branches and commits.");
32
+ }
33
+ // ── Step 1: Authenticate ──
31
34
  let auth = readGlobalAuth();
35
+ const alreadyLoggedIn = !!auth;
32
36
  if (!auth) {
33
37
  await loginCommand();
34
38
  auth = readGlobalAuth();
35
39
  if (!auth) {
40
+ if (isAgent)
41
+ agentError({ type: "auth_failed", message: "Login failed." });
36
42
  p.log.error("Login failed.");
37
43
  process.exit(1);
38
44
  }
39
45
  }
40
- else {
41
- const me = await apiGetMe(auth.apiKey).catch(() => null);
42
- if (me?.email) {
43
- p.log.success(`Logged in as ${pc.bold(me.email)}`);
44
- }
45
- }
46
46
  // ── Step 3: Resolve workspace ──
47
47
  const me = await apiGetMe(auth.apiKey).catch((e) => {
48
+ if (isAgent)
49
+ agentError({ type: "api_error", message: e.message });
48
50
  p.log.error(e.message);
49
51
  process.exit(1);
50
52
  });
51
- const workspaces = me.workspaces;
52
- let workspaceId;
53
- // Check if a workspace is already configured and still valid
54
- const existingConfig = readWorkspaceConfig();
55
- const existingWorkspace = existingConfig ? workspaces.find((w) => w.id === existingConfig.workspaceId) : null;
56
- if (existingWorkspace) {
57
- workspaceId = existingWorkspace.id;
58
- p.log.success(`Workspace: ${pc.bold(existingWorkspace.name)} ${pc.dim("(change anytime with inflight workspace)")}`);
59
- }
60
- else if (workspaces.length === 0) {
61
- p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
62
- process.exit(1);
63
- }
64
- else if (workspaces.length === 1) {
65
- workspaceId = workspaces[0].id;
66
- p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
67
- }
68
- else {
69
- const selected = await p.select({
70
- message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
71
- options: workspaces.map((w) => ({ value: w.id, label: w.name })),
72
- });
73
- if (p.isCancel(selected)) {
74
- p.cancel("Cancelled.");
75
- process.exit(0);
76
- }
77
- workspaceId = selected;
53
+ if (!isAgent && alreadyLoggedIn && me.email) {
54
+ p.log.success(`Logged in as ${pc.bold(me.email)}`);
78
55
  }
56
+ const workspaceId = await resolveWorkspace(me.workspaces, {
57
+ explicitId: opts.workspace,
58
+ commandForNext: "inflight setup",
59
+ });
79
60
  writeWorkspaceConfig({ workspaceId });
80
- 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;
81
67
  if (!widgetId) {
68
+ if (isAgent)
69
+ agentError({ type: "no_widget_id", message: "Could not find widget ID for this workspace." });
82
70
  p.log.error("Could not find widget ID for this workspace.");
83
71
  process.exit(1);
84
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
+ }
85
95
  // ── Step 4: Add widget script tag ──
86
96
  const hasWidget = (fileContents) => Object.values(fileContents).some((c) => c.includes("inflight.co/widget.js"));
87
97
  const context = gatherProjectContext(cwd);
88
98
  const alreadyHasWidget = hasWidget(context.fileContents);
89
99
  if (alreadyHasWidget) {
90
- // Already present move on silently
100
+ p.log.success("Widget script tag already installed!");
91
101
  }
92
102
  else {
93
103
  const spinner = p.spinner();
@@ -113,33 +123,36 @@ export async function setupCommand() {
113
123
  spinner.stop("Could not auto-detect where to add the widget.");
114
124
  }
115
125
  }
116
- catch {
126
+ catch (e) {
117
127
  spinner.stop("Could not analyze your project.");
128
+ if (e instanceof Error)
129
+ p.log.message(pc.dim(e.message));
118
130
  }
119
131
  if (!inserted) {
120
- p.log.message(`Add this snippet to your root HTML layout, just before ${pc.cyan("</body>")}:\n\n` +
121
- pc.dim(` <script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`));
122
- const waited = await p.text({
123
- message: "Press enter when you've added it",
124
- defaultValue: "",
125
- placeholder: "",
132
+ const scriptTag = `<script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`;
133
+ const agentPrompt = `Add the Inflight widget to this project. Insert this script tag into the root layout file, just before </body> (or as the last child of <body> in JSX): ${scriptTag}`;
134
+ p.log.message(`Paste this into your AI agent (Cursor, Claude Code, etc.):\n\n` + pc.dim(` ${agentPrompt}`));
135
+ const added = await p.confirm({
136
+ message: "Have you added the script tag?",
126
137
  });
127
- if (p.isCancel(waited)) {
138
+ if (p.isCancel(added)) {
128
139
  p.cancel("Cancelled.");
129
140
  process.exit(0);
130
141
  }
131
- // Re-scan to check if user added the widget manually
132
- const rescan = gatherProjectContext(cwd);
133
- if (!hasWidget(rescan.fileContents)) {
134
- const skip = await p.confirm({
135
- message: "Widget script not detected. Continue anyway?",
136
- initialValue: false,
137
- });
138
- if (p.isCancel(skip) || !skip) {
139
- p.cancel("Add the widget script and run setup again.");
140
- process.exit(0);
142
+ if (added) {
143
+ // Re-scan to verify
144
+ const rescan = gatherProjectContext(cwd);
145
+ if (!hasWidget(rescan.fileContents)) {
146
+ p.log.warn("Widget script not found in your project files.\n" +
147
+ " Make sure the snippet is saved and includes " +
148
+ pc.cyan("inflight.co/widget.js") +
149
+ ".");
141
150
  }
142
151
  }
152
+ else {
153
+ p.log.info("No worries — add the snippet later and run " + pc.cyan("inflight setup") + " again.");
154
+ process.exit(0);
155
+ }
143
156
  }
144
157
  }
145
158
  // ── Step 5: Commit and push (only files containing the widget script) ──
@@ -174,8 +187,8 @@ export async function setupCommand() {
174
187
  catch (err) {
175
188
  const detail = execSyncErrorDetail(err);
176
189
  p.log.warn(detail
177
- ? `Commit or push failed:\n${pc.dim(detail)}\n\nPush manually before sharing.`
178
- : "Commit or push failed. Push manually before sharing.");
190
+ ? `Push failed:\n${pc.dim(detail)}\n\nPaste the error above into your AI agent to fix it, or run ${pc.cyan("git push")} manually.`
191
+ : `Push failed. Run ${pc.cyan("git push")} manually or ask your AI agent for help.`);
179
192
  }
180
193
  }
181
194
  else {
@@ -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>;