inflight-cli 2.7.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 +44 -33
- package/dist/commands/share.d.ts +6 -0
- package/dist/commands/share.js +234 -47
- package/dist/index.js +13 -2
- package/dist/lib/agent.d.ts +25 -0
- package/dist/lib/agent.js +31 -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,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({
|
|
@@ -128,6 +160,17 @@ async function checkAndSyncGit(cwd) {
|
|
|
128
160
|
if (state.unpushedCommits.length > 5) {
|
|
129
161
|
commitLines.push(` ${pc.dim(`... and ${state.unpushedCommits.length - 5} more`)}`);
|
|
130
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
|
+
}
|
|
131
174
|
commitLines.push("", pc.yellow("Your deployment won't include these commits."));
|
|
132
175
|
p.log.warn("You have unpushed commits:\n" + commitLines.join("\n"));
|
|
133
176
|
const action = await p.select({
|
|
@@ -170,6 +213,17 @@ async function checkAndSyncGit(cwd) {
|
|
|
170
213
|
p.log.info(`Branch ${pc.bold(branch)} has no new commits — using existing deployments.`);
|
|
171
214
|
return { justPushed: false };
|
|
172
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
|
+
}
|
|
173
227
|
p.log.warn(`Branch ${pc.bold(branch)} hasn't been pushed yet — no deployment exists.`);
|
|
174
228
|
const confirm = await p.confirm({
|
|
175
229
|
message: "Push to create a deployment?",
|
|
@@ -200,12 +254,171 @@ async function checkAndSyncGit(cwd) {
|
|
|
200
254
|
}
|
|
201
255
|
return { justPushed: false };
|
|
202
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
|
+
}
|
|
203
409
|
export async function shareCommand(opts = {}) {
|
|
204
410
|
const cwd = process.cwd();
|
|
205
411
|
// TODO: Add a step to login if not authenticated
|
|
206
412
|
// ── Step 1: Auth ──
|
|
207
413
|
const auth = readGlobalAuth();
|
|
208
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
|
+
}
|
|
209
422
|
if (opts.json) {
|
|
210
423
|
console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight setup first." }));
|
|
211
424
|
}
|
|
@@ -217,6 +430,8 @@ export async function shareCommand(opts = {}) {
|
|
|
217
430
|
let gitInfo = getGitInfo(cwd);
|
|
218
431
|
// ── Step 2: Resolve workspace ──
|
|
219
432
|
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
433
|
+
if (isAgent)
|
|
434
|
+
agentError({ type: "api_error", message: e.message });
|
|
220
435
|
if (opts.json) {
|
|
221
436
|
console.log(JSON.stringify({ error: "api_error", message: e.message }));
|
|
222
437
|
}
|
|
@@ -225,50 +440,10 @@ export async function shareCommand(opts = {}) {
|
|
|
225
440
|
}
|
|
226
441
|
process.exit(1);
|
|
227
442
|
});
|
|
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 });
|
|
443
|
+
const workspaceId = await resolveWorkspace(me.workspaces, {
|
|
444
|
+
explicitId: opts.workspace,
|
|
445
|
+
commandForNext: "inflight share",
|
|
446
|
+
});
|
|
272
447
|
// ── Fast path: URL provided (agent / scripting) ──
|
|
273
448
|
if (opts.url) {
|
|
274
449
|
let stagingUrl = opts.url;
|
|
@@ -279,6 +454,8 @@ export async function shareCommand(opts = {}) {
|
|
|
279
454
|
const message = stagingUrl.includes("localhost")
|
|
280
455
|
? "Inflight needs a hosted URL — localhost isn't accessible to your team. Deploy to Vercel, Netlify, or another hosting provider first."
|
|
281
456
|
: "Must be a hosted URL with a domain (e.g., my-branch.vercel.app)";
|
|
457
|
+
if (isAgent)
|
|
458
|
+
agentError({ type: "invalid_url", message });
|
|
282
459
|
if (opts.json) {
|
|
283
460
|
console.log(JSON.stringify({ error: "invalid_url", message }));
|
|
284
461
|
}
|
|
@@ -292,7 +469,10 @@ export async function shareCommand(opts = {}) {
|
|
|
292
469
|
workspaceId,
|
|
293
470
|
stagingUrl,
|
|
294
471
|
gitInfo,
|
|
472
|
+
...(opts.project && opts.project !== "new" && { projectId: opts.project }),
|
|
295
473
|
}).catch((e) => {
|
|
474
|
+
if (isAgent)
|
|
475
|
+
agentError({ type: "create_failed", message: e.message });
|
|
296
476
|
if (opts.json) {
|
|
297
477
|
console.log(JSON.stringify({ error: "create_failed", message: e.message }));
|
|
298
478
|
}
|
|
@@ -301,6 +481,8 @@ export async function shareCommand(opts = {}) {
|
|
|
301
481
|
}
|
|
302
482
|
process.exit(1);
|
|
303
483
|
});
|
|
484
|
+
if (isAgent)
|
|
485
|
+
agentSuccess({ stagingUrl, ...result });
|
|
304
486
|
if (opts.json) {
|
|
305
487
|
console.log(JSON.stringify({ success: true, stagingUrl, ...result }));
|
|
306
488
|
}
|
|
@@ -311,6 +493,11 @@ export async function shareCommand(opts = {}) {
|
|
|
311
493
|
await open(stagingUrl);
|
|
312
494
|
return;
|
|
313
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
|
+
}
|
|
314
501
|
// Resolve staging URL
|
|
315
502
|
const providerChoice = await p.select({
|
|
316
503
|
message: "Where is your staging URL hosted?",
|
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
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
4
|
+
import { isAgent } from "../lib/agent.js";
|
|
4
5
|
import { writeNetlifyConfig } from "../lib/config.js";
|
|
5
6
|
import { ensureNetlifyCli, ensureNetlifyAuth, readLocalNetlifySite, writeLocalNetlifySite, getNetlifySiteById, getNetlifySites, getNetlifyDeploys, matchSitesByRepo, } from "../lib/netlify.js";
|
|
6
7
|
// --- Auto-detection ---
|
|
@@ -37,20 +38,20 @@ async function autoDetectSite(cwd, gitInfo, token) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
// --- Fetch all sites ---
|
|
40
|
-
const spinner = p.spinner();
|
|
41
|
-
spinner
|
|
41
|
+
const spinner = !isAgent ? p.spinner() : null;
|
|
42
|
+
spinner?.start("Detecting Netlify site...");
|
|
42
43
|
let allSites;
|
|
43
44
|
try {
|
|
44
45
|
allSites = await getNetlifySites(token);
|
|
45
46
|
}
|
|
46
47
|
catch (e) {
|
|
47
|
-
spinner
|
|
48
|
-
if (e instanceof Error)
|
|
48
|
+
spinner?.stop("Could not fetch Netlify sites.");
|
|
49
|
+
if (!isAgent && e instanceof Error)
|
|
49
50
|
p.log.message(pc.dim(e.message));
|
|
50
51
|
return null;
|
|
51
52
|
}
|
|
52
53
|
if (allSites.length === 0) {
|
|
53
|
-
spinner
|
|
54
|
+
spinner?.stop("No Netlify sites found.");
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
56
57
|
// --- Try exact match on git remote ---
|
|
@@ -58,7 +59,7 @@ async function autoDetectSite(cwd, gitInfo, token) {
|
|
|
58
59
|
if (gitRepo) {
|
|
59
60
|
const matches = matchSitesByRepo(allSites, gitRepo.owner, gitRepo.name);
|
|
60
61
|
if (matches.length === 1) {
|
|
61
|
-
spinner
|
|
62
|
+
spinner?.stop(`Detected Netlify site: ${pc.bold(matches[0].name)}`);
|
|
62
63
|
return cacheResult({
|
|
63
64
|
siteId: matches[0].id,
|
|
64
65
|
siteName: matches[0].name,
|
|
@@ -67,17 +68,19 @@ async function autoDetectSite(cwd, gitInfo, token) {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
if (matches.length > 1) {
|
|
70
|
-
spinner
|
|
71
|
+
spinner?.stop(`Found ${matches.length} Netlify sites for this repo.`);
|
|
71
72
|
const picked = await pickFromList(matches);
|
|
72
73
|
return picked ? cacheResult(picked) : null;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
// --- No match ---
|
|
76
|
-
spinner
|
|
77
|
+
spinner?.stop("Could not auto-detect Netlify site.");
|
|
77
78
|
const picked = await pickFromList(allSites);
|
|
78
79
|
return picked ? cacheResult(picked) : null;
|
|
79
80
|
}
|
|
80
81
|
async function pickFromList(sites) {
|
|
82
|
+
if (isAgent)
|
|
83
|
+
return null;
|
|
81
84
|
const maxName = Math.max(...sites.map((s) => s.name.length));
|
|
82
85
|
const selected = await p.select({
|
|
83
86
|
message: "Select a Netlify site",
|
|
@@ -102,9 +105,12 @@ async function pickFromList(sites) {
|
|
|
102
105
|
export async function pickNetlifySite(token) {
|
|
103
106
|
const allSites = await getNetlifySites(token);
|
|
104
107
|
if (allSites.length === 0) {
|
|
105
|
-
|
|
108
|
+
if (!isAgent)
|
|
109
|
+
p.log.error("No Netlify sites found.");
|
|
106
110
|
return null;
|
|
107
111
|
}
|
|
112
|
+
if (isAgent)
|
|
113
|
+
return null;
|
|
108
114
|
const selected = await p.select({
|
|
109
115
|
message: "Select a Netlify site",
|
|
110
116
|
options: allSites.map((s) => ({
|
|
@@ -127,18 +133,22 @@ export async function pickNetlifySite(token) {
|
|
|
127
133
|
}
|
|
128
134
|
// --- Main resolve function ---
|
|
129
135
|
export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
130
|
-
const cli = await ensureNetlifyCli((msg) =>
|
|
136
|
+
const cli = await ensureNetlifyCli((msg) => { if (!isAgent)
|
|
137
|
+
p.log.step(msg); });
|
|
131
138
|
if (!cli.ok) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
if (!isAgent) {
|
|
140
|
+
p.log.error("Could not install the Netlify CLI automatically.");
|
|
141
|
+
if (cli.error) {
|
|
142
|
+
p.log.message(pc.dim(cli.error.trim()));
|
|
143
|
+
}
|
|
144
|
+
p.log.info(`Paste the error above into your AI agent — it can fix this for you.\n\nThen re-run ${pc.cyan("inflight share")}.`);
|
|
135
145
|
}
|
|
136
|
-
p.log.info(`Paste the error above into your AI agent — it can fix this for you.\n\nThen re-run ${pc.cyan("inflight share")}.`);
|
|
137
146
|
process.exit(0);
|
|
138
147
|
}
|
|
139
148
|
const token = await ensureNetlifyAuth();
|
|
140
149
|
if (!token) {
|
|
141
|
-
|
|
150
|
+
if (!isAgent)
|
|
151
|
+
p.log.error("Netlify login failed.");
|
|
142
152
|
return null;
|
|
143
153
|
}
|
|
144
154
|
const site = await autoDetectSite(cwd, gitInfo, token);
|
|
@@ -156,8 +166,8 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
156
166
|
let commitDeploy = repoMatches ? deploys.find((d) => d.commitRef === commitSha) : undefined;
|
|
157
167
|
// If just pushed, poll until the commit deployment appears
|
|
158
168
|
if (!commitDeploy && repoMatches && opts?.justPushed) {
|
|
159
|
-
const pollSpinner = p.spinner({ indicator: "timer" });
|
|
160
|
-
pollSpinner
|
|
169
|
+
const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
170
|
+
pollSpinner?.start("Waiting for deploy");
|
|
161
171
|
for (let i = 0; i < 30; i++) {
|
|
162
172
|
await new Promise((r) => setTimeout(r, 2000));
|
|
163
173
|
deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
|
|
@@ -168,10 +178,10 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
168
178
|
break;
|
|
169
179
|
}
|
|
170
180
|
if (commitDeploy) {
|
|
171
|
-
pollSpinner
|
|
181
|
+
pollSpinner?.clear();
|
|
172
182
|
}
|
|
173
183
|
else {
|
|
174
|
-
pollSpinner
|
|
184
|
+
pollSpinner?.stop("No deployment detected yet — Netlify may still be processing.");
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
// If we found a commit-specific deployment, handle based on state
|
|
@@ -181,20 +191,22 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
181
191
|
? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
|
|
182
192
|
: "";
|
|
183
193
|
if (commitDeploy.state === "error") {
|
|
184
|
-
|
|
194
|
+
if (!isAgent)
|
|
195
|
+
p.log.error(`Netlify deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
|
|
185
196
|
commitDeploy = undefined;
|
|
186
197
|
// Fall through to the picker
|
|
187
198
|
}
|
|
188
199
|
else if (commitDeploy.state !== "ready") {
|
|
189
200
|
// Any non-ready, non-error state (building, enqueued, uploading, preparing, processing, etc.)
|
|
190
|
-
|
|
201
|
+
if (!isAgent)
|
|
202
|
+
p.log.info(`Netlify deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state)}`);
|
|
191
203
|
const buildingUrl = commitDeploy.deploySslUrl;
|
|
192
204
|
const startTime = Date.now();
|
|
193
205
|
const maxWaitMs = 120_000;
|
|
194
206
|
const pollIntervalMs = 5_000;
|
|
195
207
|
let resolved = false;
|
|
196
|
-
const spinner = p.spinner({ indicator: "timer" });
|
|
197
|
-
spinner
|
|
208
|
+
const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
209
|
+
spinner?.start("Waiting for build");
|
|
198
210
|
try {
|
|
199
211
|
while (Date.now() - startTime < maxWaitMs) {
|
|
200
212
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
@@ -205,20 +217,24 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
205
217
|
if (!fresh)
|
|
206
218
|
break;
|
|
207
219
|
if (fresh.state === "ready") {
|
|
208
|
-
spinner
|
|
209
|
-
|
|
220
|
+
spinner?.stop("Netlify deployment ready!");
|
|
221
|
+
if (!isAgent)
|
|
222
|
+
p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.deploySslUrl)}`);
|
|
210
223
|
return fresh.deploySslUrl;
|
|
211
224
|
}
|
|
212
225
|
if (fresh.state === "error") {
|
|
213
|
-
spinner
|
|
214
|
-
|
|
226
|
+
spinner?.stop(pc.red("Netlify deployment failed."));
|
|
227
|
+
if (!isAgent)
|
|
228
|
+
p.log.error(`Netlify deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
|
|
215
229
|
commitDeploy = undefined;
|
|
216
230
|
resolved = true;
|
|
217
231
|
break;
|
|
218
232
|
}
|
|
219
233
|
}
|
|
220
234
|
if (!resolved) {
|
|
221
|
-
spinner
|
|
235
|
+
spinner?.stop("Still building...");
|
|
236
|
+
if (isAgent)
|
|
237
|
+
return buildingUrl;
|
|
222
238
|
const action = await p.select({
|
|
223
239
|
message: "Netlify deployment is still building.",
|
|
224
240
|
options: [
|
|
@@ -239,14 +255,15 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
catch (e) {
|
|
242
|
-
spinner
|
|
243
|
-
if (e instanceof Error)
|
|
258
|
+
spinner?.stop("Error checking deployment status.");
|
|
259
|
+
if (!isAgent && e instanceof Error)
|
|
244
260
|
p.log.message(pc.dim(e.message));
|
|
245
261
|
}
|
|
246
262
|
}
|
|
247
263
|
else {
|
|
248
264
|
// ready — use it
|
|
249
|
-
|
|
265
|
+
if (!isAgent)
|
|
266
|
+
p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(commitDeploy.deploySslUrl)}`);
|
|
250
267
|
return commitDeploy.deploySslUrl;
|
|
251
268
|
}
|
|
252
269
|
}
|
|
@@ -256,14 +273,20 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
256
273
|
});
|
|
257
274
|
// Fallback: no commit deployment — let user pick from recent or paste manually
|
|
258
275
|
if (deploys.length === 0) {
|
|
259
|
-
|
|
276
|
+
if (!isAgent)
|
|
277
|
+
p.log.warn("No deployments found. Paste a URL instead.");
|
|
260
278
|
return null;
|
|
261
279
|
}
|
|
262
280
|
const hasWorkingDeploy = deploys.some((d) => d.state !== "error");
|
|
263
281
|
if (!hasWorkingDeploy) {
|
|
264
|
-
|
|
282
|
+
if (!isAgent)
|
|
283
|
+
p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
|
|
265
284
|
return null;
|
|
266
285
|
}
|
|
286
|
+
if (isAgent) {
|
|
287
|
+
const ready = deploys.find((d) => d.state === "ready");
|
|
288
|
+
return ready?.deploySslUrl ?? deploys[0]?.deploySslUrl ?? null;
|
|
289
|
+
}
|
|
267
290
|
const maxBranch = Math.max(...deploys.map((d) => (d.branch ?? "unknown").length));
|
|
268
291
|
const selected = await p.select({
|
|
269
292
|
message: "No deployment found for current commit. Select one:",
|
|
@@ -295,6 +318,8 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
295
318
|
// Warn if the user picked a failed deployment
|
|
296
319
|
const pickedDeploy = deploys.find((d) => d.deploySslUrl === selected);
|
|
297
320
|
if (pickedDeploy && pickedDeploy.state === "error") {
|
|
321
|
+
if (isAgent)
|
|
322
|
+
return null;
|
|
298
323
|
p.log.warn("This deployment failed — the URL may not load.");
|
|
299
324
|
const confirm = await p.confirm({ message: "Use it anyway?" });
|
|
300
325
|
if (p.isCancel(confirm) || !confirm)
|
package/dist/providers/vercel.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
5
|
+
import { isAgent } from "../lib/agent.js";
|
|
5
6
|
import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectById, getVercelProjects, matchVercelProjectsByRepo, createVercelProject, getVercelDeployments, } from "../lib/vercel.js";
|
|
6
7
|
// --- Auto-detection ---
|
|
7
8
|
/**
|
|
@@ -40,20 +41,21 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
// --- Fetch all projects ---
|
|
43
|
-
const spinner = p.spinner();
|
|
44
|
-
spinner
|
|
44
|
+
const spinner = !isAgent ? p.spinner() : null;
|
|
45
|
+
spinner?.start("Detecting Vercel project...");
|
|
45
46
|
let allProjects;
|
|
46
47
|
try {
|
|
47
48
|
allProjects = await getVercelProjects(token);
|
|
48
49
|
}
|
|
49
50
|
catch (e) {
|
|
50
|
-
spinner
|
|
51
|
+
spinner?.stop("Could not fetch Vercel projects.");
|
|
51
52
|
if (e instanceof Error)
|
|
52
|
-
|
|
53
|
+
if (!isAgent)
|
|
54
|
+
p.log.message(pc.dim(e.message));
|
|
53
55
|
return null;
|
|
54
56
|
}
|
|
55
57
|
if (allProjects.length === 0) {
|
|
56
|
-
spinner
|
|
58
|
+
spinner?.stop("No Vercel projects found.");
|
|
57
59
|
return null;
|
|
58
60
|
}
|
|
59
61
|
// --- Try exact match on git remote ---
|
|
@@ -61,7 +63,7 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
61
63
|
if (gitRepo) {
|
|
62
64
|
const matches = matchVercelProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
|
|
63
65
|
if (matches.length === 1) {
|
|
64
|
-
spinner
|
|
66
|
+
spinner?.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
|
|
65
67
|
return cacheResult({
|
|
66
68
|
teamId: matches[0].teamId,
|
|
67
69
|
projectId: matches[0].id,
|
|
@@ -71,18 +73,20 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
if (matches.length > 1) {
|
|
74
|
-
spinner
|
|
76
|
+
spinner?.stop(`Found ${matches.length} Vercel projects for this repo.`);
|
|
75
77
|
const picked = await pickFromList(allProjects);
|
|
76
78
|
return picked ? cacheResult(picked) : null;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
// --- No match ---
|
|
80
|
-
spinner
|
|
82
|
+
spinner?.stop("Could not auto-detect Vercel project.");
|
|
81
83
|
const canCreate = gitRepo && gitRepo.provider !== "unknown";
|
|
82
84
|
const picked = await pickFromList(allProjects, canCreate ? { token, gitRepo } : undefined);
|
|
83
85
|
return picked ? cacheResult(picked) : null;
|
|
84
86
|
}
|
|
85
87
|
async function pickFromList(projects, createCtx) {
|
|
88
|
+
if (isAgent)
|
|
89
|
+
return null;
|
|
86
90
|
const maxName = Math.max(...projects.map((proj) => proj.name.length));
|
|
87
91
|
const selected = await p.select({
|
|
88
92
|
message: "Select a Vercel project",
|
|
@@ -113,6 +117,8 @@ async function pickFromList(projects, createCtx) {
|
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
async function createProjectFlow(projects, ctx) {
|
|
120
|
+
if (isAgent)
|
|
121
|
+
return null;
|
|
116
122
|
const { token, gitRepo } = ctx;
|
|
117
123
|
// Pick team from unique teams
|
|
118
124
|
const teams = [...new Map(projects.map((proj) => [proj.teamId, proj.teamName])).entries()];
|
|
@@ -184,18 +190,23 @@ async function createProjectFlow(projects, ctx) {
|
|
|
184
190
|
}
|
|
185
191
|
// --- Main resolve function ---
|
|
186
192
|
export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
187
|
-
const cli = await ensureVercelCli((msg) =>
|
|
193
|
+
const cli = await ensureVercelCli((msg) => { if (!isAgent)
|
|
194
|
+
p.log.step(msg); });
|
|
188
195
|
if (!cli.ok) {
|
|
189
|
-
|
|
196
|
+
if (!isAgent)
|
|
197
|
+
p.log.error("Could not install the Vercel CLI automatically.");
|
|
190
198
|
if (cli.error) {
|
|
191
|
-
|
|
199
|
+
if (!isAgent)
|
|
200
|
+
p.log.message(pc.dim(cli.error.trim()));
|
|
192
201
|
}
|
|
193
|
-
|
|
202
|
+
if (!isAgent)
|
|
203
|
+
p.log.info(`Paste the error above into your AI agent — it can fix this for you.\n\nThen re-run ${pc.cyan("inflight share")}.`);
|
|
194
204
|
process.exit(0);
|
|
195
205
|
}
|
|
196
206
|
const token = await ensureVercelAuth();
|
|
197
207
|
if (!token) {
|
|
198
|
-
|
|
208
|
+
if (!isAgent)
|
|
209
|
+
p.log.error("Vercel login failed.");
|
|
199
210
|
return null;
|
|
200
211
|
}
|
|
201
212
|
const project = await autoDetectProject(cwd, gitInfo, token);
|
|
@@ -215,8 +226,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
215
226
|
let commitDeploy = repoMatches ? deployments.find((d) => d.commitSha === commitSha) : undefined;
|
|
216
227
|
// If just pushed, poll until the commit deployment appears
|
|
217
228
|
if (!commitDeploy && repoMatches && opts?.justPushed) {
|
|
218
|
-
const pollSpinner = p.spinner({ indicator: "timer" });
|
|
219
|
-
pollSpinner
|
|
229
|
+
const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
230
|
+
pollSpinner?.start("Waiting for deploy");
|
|
220
231
|
for (let i = 0; i < 30; i++) {
|
|
221
232
|
await new Promise((r) => setTimeout(r, 2000));
|
|
222
233
|
deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
|
|
@@ -227,10 +238,10 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
227
238
|
break;
|
|
228
239
|
}
|
|
229
240
|
if (commitDeploy) {
|
|
230
|
-
pollSpinner
|
|
241
|
+
pollSpinner?.clear();
|
|
231
242
|
}
|
|
232
243
|
else {
|
|
233
|
-
pollSpinner
|
|
244
|
+
pollSpinner?.stop("No deployment detected yet — Vercel may still be processing.");
|
|
234
245
|
}
|
|
235
246
|
}
|
|
236
247
|
// If we found a commit-specific deployment, handle based on state
|
|
@@ -240,7 +251,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
240
251
|
? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
|
|
241
252
|
: "";
|
|
242
253
|
if (commitDeploy.state === "ERROR" || commitDeploy.state === "CANCELED") {
|
|
243
|
-
|
|
254
|
+
if (!isAgent)
|
|
255
|
+
p.log.error(`Vercel deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
|
|
244
256
|
commitDeploy = undefined;
|
|
245
257
|
// Fall through to the picker
|
|
246
258
|
}
|
|
@@ -248,15 +260,16 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
248
260
|
commitDeploy.state === "QUEUED" ||
|
|
249
261
|
commitDeploy.state === "INITIALIZING") {
|
|
250
262
|
// Show what we're waiting on
|
|
251
|
-
|
|
263
|
+
if (!isAgent)
|
|
264
|
+
p.log.info(`Vercel deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state.toLowerCase())}`);
|
|
252
265
|
// Poll with a ticking timer for up to ~2 minutes
|
|
253
266
|
const buildingUrl = commitDeploy.url;
|
|
254
267
|
const startTime = Date.now();
|
|
255
268
|
const maxWaitMs = 120_000;
|
|
256
269
|
const pollIntervalMs = 3_000;
|
|
257
270
|
let resolved = false;
|
|
258
|
-
const spinner = p.spinner({ indicator: "timer" });
|
|
259
|
-
spinner
|
|
271
|
+
const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
272
|
+
spinner?.start("Waiting for build");
|
|
260
273
|
try {
|
|
261
274
|
while (Date.now() - startTime < maxWaitMs) {
|
|
262
275
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
@@ -267,19 +280,22 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
267
280
|
if (!fresh)
|
|
268
281
|
break;
|
|
269
282
|
if (fresh.state === "READY") {
|
|
270
|
-
spinner
|
|
283
|
+
spinner?.stop("Vercel deployment ready!");
|
|
271
284
|
return fresh.url;
|
|
272
285
|
}
|
|
273
286
|
if (fresh.state === "ERROR" || fresh.state === "CANCELED") {
|
|
274
|
-
spinner
|
|
275
|
-
|
|
287
|
+
spinner?.stop(pc.red("Vercel deployment failed."));
|
|
288
|
+
if (!isAgent)
|
|
289
|
+
p.log.error(`Vercel deployment for ${commitLabel}${message} ${pc.red("failed")}.\n Fix the build and push again, or select a different deployment below.`);
|
|
276
290
|
commitDeploy = undefined;
|
|
277
291
|
resolved = true;
|
|
278
292
|
break;
|
|
279
293
|
}
|
|
280
294
|
}
|
|
281
295
|
if (!resolved) {
|
|
282
|
-
|
|
296
|
+
if (isAgent)
|
|
297
|
+
return buildingUrl;
|
|
298
|
+
spinner?.stop("Still building...");
|
|
283
299
|
const action = await p.select({
|
|
284
300
|
message: "Vercel deployment is still building.",
|
|
285
301
|
options: [
|
|
@@ -292,7 +308,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
292
308
|
process.exit(0);
|
|
293
309
|
}
|
|
294
310
|
if (action === "continue") {
|
|
295
|
-
|
|
311
|
+
if (!isAgent)
|
|
312
|
+
p.log.info(`Vercel deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
|
|
296
313
|
return buildingUrl;
|
|
297
314
|
}
|
|
298
315
|
commitDeploy = undefined;
|
|
@@ -300,14 +317,16 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
catch (e) {
|
|
303
|
-
spinner
|
|
320
|
+
spinner?.stop("Error checking deployment status.");
|
|
304
321
|
if (e instanceof Error)
|
|
305
|
-
|
|
322
|
+
if (!isAgent)
|
|
323
|
+
p.log.message(pc.dim(e.message));
|
|
306
324
|
}
|
|
307
325
|
}
|
|
308
326
|
else {
|
|
309
327
|
// READY or any other terminal state — use it
|
|
310
|
-
|
|
328
|
+
if (!isAgent)
|
|
329
|
+
p.log.info(`Vercel deployment for ${commitLabel}${message}`);
|
|
311
330
|
return commitDeploy.url;
|
|
312
331
|
}
|
|
313
332
|
}
|
|
@@ -317,14 +336,20 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
317
336
|
});
|
|
318
337
|
// Fallback: no commit deployment found — let user pick from recent or paste manually
|
|
319
338
|
if (deployments.length === 0) {
|
|
320
|
-
|
|
339
|
+
if (!isAgent)
|
|
340
|
+
p.log.warn("No deployments found. Paste a URL instead.");
|
|
321
341
|
return null;
|
|
322
342
|
}
|
|
323
343
|
const hasWorkingDeployment = deployments.some((d) => d.state !== "ERROR" && d.state !== "CANCELED");
|
|
324
344
|
if (!hasWorkingDeployment) {
|
|
325
|
-
|
|
345
|
+
if (!isAgent)
|
|
346
|
+
p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
|
|
326
347
|
return null;
|
|
327
348
|
}
|
|
349
|
+
if (isAgent) {
|
|
350
|
+
const ready = deployments.find((d) => d.state === "READY");
|
|
351
|
+
return ready?.url ?? deployments[0]?.url ?? null;
|
|
352
|
+
}
|
|
328
353
|
const maxBranchPick = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
|
|
329
354
|
const selected = await p.select({
|
|
330
355
|
message: "No deployment found for current commit. Select one:",
|
|
@@ -356,6 +381,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
356
381
|
// Warn if the user picked a failed deployment
|
|
357
382
|
const pickedDeploy = deployments.find((d) => d.url === selected);
|
|
358
383
|
if (pickedDeploy && (pickedDeploy.state === "ERROR" || pickedDeploy.state === "CANCELED")) {
|
|
384
|
+
if (isAgent)
|
|
385
|
+
return null;
|
|
359
386
|
p.log.warn("This deployment failed — the URL may not load.");
|
|
360
387
|
const confirm = await p.confirm({ message: "Use it anyway?" });
|
|
361
388
|
if (p.isCancel(confirm) || !confirm)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inflight-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Get feedback directly on your staging URL",
|
|
5
5
|
"bin": {
|
|
6
6
|
"inflight": "dist/index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"prepublishOnly": "npm run build"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@clack/core": "1.
|
|
23
|
-
"@clack/prompts": "1.
|
|
22
|
+
"@clack/core": "^1.3.0",
|
|
23
|
+
"@clack/prompts": "^1.3.0",
|
|
24
24
|
"commander": "12.1.0",
|
|
25
25
|
"open": "11.0.0",
|
|
26
26
|
"picocolors": "1.1.1",
|