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