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.
- package/dist/commands/login.js +56 -33
- package/dist/commands/setup.d.ts +5 -1
- package/dist/commands/setup.js +75 -62
- package/dist/commands/share.d.ts +6 -0
- package/dist/commands/share.js +256 -56
- package/dist/index.js +13 -2
- package/dist/lib/agent.d.ts +25 -0
- package/dist/lib/agent.js +31 -0
- package/dist/lib/framework.js +7 -1
- package/dist/lib/netlify.d.ts +4 -1
- package/dist/lib/netlify.js +9 -4
- package/dist/lib/resolve-workspace.d.ts +15 -0
- package/dist/lib/resolve-workspace.js +77 -0
- package/dist/lib/vercel.d.ts +4 -1
- package/dist/lib/vercel.js +9 -4
- package/dist/providers/netlify.js +76 -52
- package/dist/providers/vercel.js +67 -45
- package/package.json +3 -3
package/dist/commands/login.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
// ──
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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(
|
|
138
|
+
if (p.isCancel(added)) {
|
|
128
139
|
p.cancel("Cancelled.");
|
|
129
140
|
process.exit(0);
|
|
130
141
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
? `
|
|
178
|
-
:
|
|
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 {
|
package/dist/commands/share.d.ts
CHANGED
|
@@ -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>;
|