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/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,26 @@
|
|
|
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
|
+
instructions?: Record<string, string>;
|
|
21
|
+
}): never;
|
|
22
|
+
export declare function agentError(opts: {
|
|
23
|
+
type: string;
|
|
24
|
+
message: string;
|
|
25
|
+
suggestion?: string;
|
|
26
|
+
}): never;
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
...(opts.instructions && { instructions: opts.instructions }),
|
|
21
|
+
}));
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
export function agentError(opts) {
|
|
25
|
+
console.log(JSON.stringify({
|
|
26
|
+
status: "error",
|
|
27
|
+
type: opts.type,
|
|
28
|
+
message: opts.message,
|
|
29
|
+
...(opts.suggestion && { suggestion: opts.suggestion }),
|
|
30
|
+
}));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
@@ -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)
|