inflight-cli 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/login.js +56 -33
- package/dist/commands/setup.d.ts +5 -1
- package/dist/commands/setup.js +75 -62
- package/dist/commands/share.d.ts +6 -0
- package/dist/commands/share.js +256 -56
- package/dist/index.js +13 -2
- package/dist/lib/agent.d.ts +25 -0
- package/dist/lib/agent.js +31 -0
- package/dist/lib/framework.js +7 -1
- package/dist/lib/netlify.d.ts +4 -1
- package/dist/lib/netlify.js +9 -4
- package/dist/lib/resolve-workspace.d.ts +15 -0
- package/dist/lib/resolve-workspace.js +77 -0
- package/dist/lib/vercel.d.ts +4 -1
- package/dist/lib/vercel.js +9 -4
- package/dist/providers/netlify.js +76 -52
- package/dist/providers/vercel.js +67 -45
- package/package.json +3 -3
package/dist/lib/vercel.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Ensures the Vercel CLI is available — installs globally if missing. */
|
|
2
|
-
export declare function ensureVercelCli(log?: (msg: string) => void): Promise<
|
|
2
|
+
export declare function ensureVercelCli(log?: (msg: string) => void): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
error?: string;
|
|
5
|
+
}>;
|
|
3
6
|
/**
|
|
4
7
|
* Gets a valid Vercel token. Refreshes silently via `vercel whoami` if expired.
|
|
5
8
|
* Does NOT prompt for login — returns null if no valid token available.
|
package/dist/lib/vercel.js
CHANGED
|
@@ -18,14 +18,19 @@ function hasVercelCli() {
|
|
|
18
18
|
/** Ensures the Vercel CLI is available — installs globally if missing. */
|
|
19
19
|
export async function ensureVercelCli(log) {
|
|
20
20
|
if (hasVercelCli())
|
|
21
|
-
return true;
|
|
21
|
+
return { ok: true };
|
|
22
22
|
log?.("Installing Vercel CLI...");
|
|
23
23
|
try {
|
|
24
24
|
await execAsync("npm install -g vercel");
|
|
25
|
-
return true;
|
|
25
|
+
return { ok: true };
|
|
26
26
|
}
|
|
27
|
-
catch {
|
|
28
|
-
|
|
27
|
+
catch (e) {
|
|
28
|
+
const stderr = e && typeof e === "object" && "stderr" in e && typeof e.stderr === "string"
|
|
29
|
+
? e.stderr
|
|
30
|
+
: e instanceof Error
|
|
31
|
+
? e.message
|
|
32
|
+
: "Unknown error";
|
|
33
|
+
return { ok: false, error: stderr };
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
/** Returns the Vercel CLI config directory for the current platform. */
|
|
@@ -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,18 +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
|
-
catch {
|
|
47
|
-
spinner
|
|
47
|
+
catch (e) {
|
|
48
|
+
spinner?.stop("Could not fetch Netlify sites.");
|
|
49
|
+
if (!isAgent && e instanceof Error)
|
|
50
|
+
p.log.message(pc.dim(e.message));
|
|
48
51
|
return null;
|
|
49
52
|
}
|
|
50
53
|
if (allSites.length === 0) {
|
|
51
|
-
spinner
|
|
54
|
+
spinner?.stop("No Netlify sites found.");
|
|
52
55
|
return null;
|
|
53
56
|
}
|
|
54
57
|
// --- Try exact match on git remote ---
|
|
@@ -56,7 +59,7 @@ async function autoDetectSite(cwd, gitInfo, token) {
|
|
|
56
59
|
if (gitRepo) {
|
|
57
60
|
const matches = matchSitesByRepo(allSites, gitRepo.owner, gitRepo.name);
|
|
58
61
|
if (matches.length === 1) {
|
|
59
|
-
spinner
|
|
62
|
+
spinner?.stop(`Detected Netlify site: ${pc.bold(matches[0].name)}`);
|
|
60
63
|
return cacheResult({
|
|
61
64
|
siteId: matches[0].id,
|
|
62
65
|
siteName: matches[0].name,
|
|
@@ -65,25 +68,25 @@ async function autoDetectSite(cwd, gitInfo, token) {
|
|
|
65
68
|
});
|
|
66
69
|
}
|
|
67
70
|
if (matches.length > 1) {
|
|
68
|
-
spinner
|
|
71
|
+
spinner?.stop(`Found ${matches.length} Netlify sites for this repo.`);
|
|
69
72
|
const picked = await pickFromList(matches);
|
|
70
73
|
return picked ? cacheResult(picked) : null;
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
// --- No match ---
|
|
74
|
-
spinner
|
|
77
|
+
spinner?.stop("Could not auto-detect Netlify site.");
|
|
75
78
|
const picked = await pickFromList(allSites);
|
|
76
79
|
return picked ? cacheResult(picked) : null;
|
|
77
80
|
}
|
|
78
81
|
async function pickFromList(sites) {
|
|
82
|
+
if (isAgent)
|
|
83
|
+
return null;
|
|
79
84
|
const maxName = Math.max(...sites.map((s) => s.name.length));
|
|
80
85
|
const selected = await p.select({
|
|
81
86
|
message: "Select a Netlify site",
|
|
82
87
|
options: sites.map((s) => ({
|
|
83
88
|
value: s,
|
|
84
|
-
label: s.account_name
|
|
85
|
-
? `${s.name.padEnd(maxName)} ${pc.dim(`(${s.account_name})`)}`
|
|
86
|
-
: s.name,
|
|
89
|
+
label: s.account_name ? `${s.name.padEnd(maxName)} ${pc.dim(`(${s.account_name})`)}` : s.name,
|
|
87
90
|
})),
|
|
88
91
|
});
|
|
89
92
|
if (p.isCancel(selected)) {
|
|
@@ -91,15 +94,23 @@ async function pickFromList(sites) {
|
|
|
91
94
|
process.exit(0);
|
|
92
95
|
}
|
|
93
96
|
const match = selected;
|
|
94
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
siteId: match.id,
|
|
99
|
+
siteName: match.name,
|
|
100
|
+
teamSlug: match.account_slug,
|
|
101
|
+
repoPath: match.build_settings?.repo_path ?? null,
|
|
102
|
+
};
|
|
95
103
|
}
|
|
96
104
|
// --- Manual picker (used by `inflight netlify` command) ---
|
|
97
105
|
export async function pickNetlifySite(token) {
|
|
98
106
|
const allSites = await getNetlifySites(token);
|
|
99
107
|
if (allSites.length === 0) {
|
|
100
|
-
|
|
108
|
+
if (!isAgent)
|
|
109
|
+
p.log.error("No Netlify sites found.");
|
|
101
110
|
return null;
|
|
102
111
|
}
|
|
112
|
+
if (isAgent)
|
|
113
|
+
return null;
|
|
103
114
|
const selected = await p.select({
|
|
104
115
|
message: "Select a Netlify site",
|
|
105
116
|
options: allSites.map((s) => ({
|
|
@@ -122,14 +133,22 @@ export async function pickNetlifySite(token) {
|
|
|
122
133
|
}
|
|
123
134
|
// --- Main resolve function ---
|
|
124
135
|
export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
const cli = await ensureNetlifyCli((msg) => { if (!isAgent)
|
|
137
|
+
p.log.step(msg); });
|
|
138
|
+
if (!cli.ok) {
|
|
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")}.`);
|
|
145
|
+
}
|
|
146
|
+
process.exit(0);
|
|
129
147
|
}
|
|
130
148
|
const token = await ensureNetlifyAuth();
|
|
131
149
|
if (!token) {
|
|
132
|
-
|
|
150
|
+
if (!isAgent)
|
|
151
|
+
p.log.error("Netlify login failed.");
|
|
133
152
|
return null;
|
|
134
153
|
}
|
|
135
154
|
const site = await autoDetectSite(cwd, gitInfo, token);
|
|
@@ -142,27 +161,27 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
142
161
|
const repoMatches = site.repoPath && localRepoPath ? site.repoPath.toLowerCase() === localRepoPath : false;
|
|
143
162
|
// Only filter by branch if we're in the matching repo — otherwise show all deploys
|
|
144
163
|
let deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
|
|
145
|
-
branch: repoMatches ?
|
|
164
|
+
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
146
165
|
});
|
|
147
166
|
let commitDeploy = repoMatches ? deploys.find((d) => d.commitRef === commitSha) : undefined;
|
|
148
167
|
// If just pushed, poll until the commit deployment appears
|
|
149
168
|
if (!commitDeploy && repoMatches && opts?.justPushed) {
|
|
150
|
-
const pollSpinner = p.spinner();
|
|
151
|
-
pollSpinner
|
|
169
|
+
const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
170
|
+
pollSpinner?.start("Waiting for deploy");
|
|
152
171
|
for (let i = 0; i < 30; i++) {
|
|
153
172
|
await new Promise((r) => setTimeout(r, 2000));
|
|
154
173
|
deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
|
|
155
|
-
branch: repoMatches ?
|
|
174
|
+
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
156
175
|
});
|
|
157
176
|
commitDeploy = deploys.find((d) => d.commitRef === commitSha);
|
|
158
177
|
if (commitDeploy)
|
|
159
178
|
break;
|
|
160
179
|
}
|
|
161
180
|
if (commitDeploy) {
|
|
162
|
-
pollSpinner
|
|
181
|
+
pollSpinner?.clear();
|
|
163
182
|
}
|
|
164
183
|
else {
|
|
165
|
-
pollSpinner
|
|
184
|
+
pollSpinner?.stop("No deployment detected yet — Netlify may still be processing.");
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
// If we found a commit-specific deployment, handle based on state
|
|
@@ -172,55 +191,50 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
172
191
|
? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
|
|
173
192
|
: "";
|
|
174
193
|
if (commitDeploy.state === "error") {
|
|
175
|
-
|
|
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.`);
|
|
176
196
|
commitDeploy = undefined;
|
|
177
197
|
// Fall through to the picker
|
|
178
198
|
}
|
|
179
199
|
else if (commitDeploy.state !== "ready") {
|
|
180
200
|
// Any non-ready, non-error state (building, enqueued, uploading, preparing, processing, etc.)
|
|
181
|
-
|
|
201
|
+
if (!isAgent)
|
|
202
|
+
p.log.info(`Netlify deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state)}`);
|
|
182
203
|
const buildingUrl = commitDeploy.deploySslUrl;
|
|
183
204
|
const startTime = Date.now();
|
|
184
205
|
const maxWaitMs = 120_000;
|
|
185
206
|
const pollIntervalMs = 5_000;
|
|
186
207
|
let resolved = false;
|
|
187
|
-
const spinner = p.spinner();
|
|
188
|
-
|
|
189
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
190
|
-
spinner.message(`Waiting for Netlify to finish building... (${elapsed}s)`);
|
|
191
|
-
};
|
|
192
|
-
spinner.start("Waiting for Netlify to finish building... (0s)");
|
|
193
|
-
const ticker = setInterval(updateMessage, 1000);
|
|
208
|
+
const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
209
|
+
spinner?.start("Waiting for build");
|
|
194
210
|
try {
|
|
195
211
|
while (Date.now() - startTime < maxWaitMs) {
|
|
196
212
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
197
|
-
updateMessage();
|
|
198
213
|
const freshDeps = await getNetlifyDeploys(token, site.siteId, site.siteName, {
|
|
199
|
-
branch: repoMatches ?
|
|
214
|
+
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
200
215
|
});
|
|
201
216
|
const fresh = freshDeps.find((d) => d.commitRef === commitSha);
|
|
202
217
|
if (!fresh)
|
|
203
218
|
break;
|
|
204
219
|
if (fresh.state === "ready") {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.deploySslUrl)}`);
|
|
220
|
+
spinner?.stop("Netlify deployment ready!");
|
|
221
|
+
if (!isAgent)
|
|
222
|
+
p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.deploySslUrl)}`);
|
|
209
223
|
return fresh.deploySslUrl;
|
|
210
224
|
}
|
|
211
225
|
if (fresh.state === "error") {
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
spinner?.stop("Still building...");
|
|
236
|
+
if (isAgent)
|
|
237
|
+
return buildingUrl;
|
|
224
238
|
const action = await p.select({
|
|
225
239
|
message: "Netlify deployment is still building.",
|
|
226
240
|
options: [
|
|
@@ -240,31 +254,39 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
240
254
|
// Fall through to picker
|
|
241
255
|
}
|
|
242
256
|
}
|
|
243
|
-
catch {
|
|
244
|
-
|
|
245
|
-
|
|
257
|
+
catch (e) {
|
|
258
|
+
spinner?.stop("Error checking deployment status.");
|
|
259
|
+
if (!isAgent && e instanceof Error)
|
|
260
|
+
p.log.message(pc.dim(e.message));
|
|
246
261
|
}
|
|
247
262
|
}
|
|
248
263
|
else {
|
|
249
264
|
// ready — use it
|
|
250
|
-
|
|
265
|
+
if (!isAgent)
|
|
266
|
+
p.log.info(`Netlify deployment for ${commitLabel}${message}:\n → ${pc.cyan(commitDeploy.deploySslUrl)}`);
|
|
251
267
|
return commitDeploy.deploySslUrl;
|
|
252
268
|
}
|
|
253
269
|
}
|
|
254
270
|
// Refresh deployments so the picker shows current states
|
|
255
271
|
deploys = await getNetlifyDeploys(token, site.siteId, site.siteName, {
|
|
256
|
-
branch: repoMatches ?
|
|
272
|
+
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
257
273
|
});
|
|
258
274
|
// Fallback: no commit deployment — let user pick from recent or paste manually
|
|
259
275
|
if (deploys.length === 0) {
|
|
260
|
-
|
|
276
|
+
if (!isAgent)
|
|
277
|
+
p.log.warn("No deployments found. Paste a URL instead.");
|
|
261
278
|
return null;
|
|
262
279
|
}
|
|
263
280
|
const hasWorkingDeploy = deploys.some((d) => d.state !== "error");
|
|
264
281
|
if (!hasWorkingDeploy) {
|
|
265
|
-
|
|
282
|
+
if (!isAgent)
|
|
283
|
+
p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
|
|
266
284
|
return null;
|
|
267
285
|
}
|
|
286
|
+
if (isAgent) {
|
|
287
|
+
const ready = deploys.find((d) => d.state === "ready");
|
|
288
|
+
return ready?.deploySslUrl ?? deploys[0]?.deploySslUrl ?? null;
|
|
289
|
+
}
|
|
268
290
|
const maxBranch = Math.max(...deploys.map((d) => (d.branch ?? "unknown").length));
|
|
269
291
|
const selected = await p.select({
|
|
270
292
|
message: "No deployment found for current commit. Select one:",
|
|
@@ -296,6 +318,8 @@ export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
|
296
318
|
// Warn if the user picked a failed deployment
|
|
297
319
|
const pickedDeploy = deploys.find((d) => d.deploySslUrl === selected);
|
|
298
320
|
if (pickedDeploy && pickedDeploy.state === "error") {
|
|
321
|
+
if (isAgent)
|
|
322
|
+
return null;
|
|
299
323
|
p.log.warn("This deployment failed — the URL may not load.");
|
|
300
324
|
const confirm = await p.confirm({ message: "Use it anyway?" });
|
|
301
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,18 +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
|
-
catch {
|
|
50
|
-
spinner
|
|
50
|
+
catch (e) {
|
|
51
|
+
spinner?.stop("Could not fetch Vercel projects.");
|
|
52
|
+
if (e instanceof Error)
|
|
53
|
+
if (!isAgent)
|
|
54
|
+
p.log.message(pc.dim(e.message));
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
53
57
|
if (allProjects.length === 0) {
|
|
54
|
-
spinner
|
|
58
|
+
spinner?.stop("No Vercel projects found.");
|
|
55
59
|
return null;
|
|
56
60
|
}
|
|
57
61
|
// --- Try exact match on git remote ---
|
|
@@ -59,7 +63,7 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
59
63
|
if (gitRepo) {
|
|
60
64
|
const matches = matchVercelProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
|
|
61
65
|
if (matches.length === 1) {
|
|
62
|
-
spinner
|
|
66
|
+
spinner?.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
|
|
63
67
|
return cacheResult({
|
|
64
68
|
teamId: matches[0].teamId,
|
|
65
69
|
projectId: matches[0].id,
|
|
@@ -69,18 +73,20 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
if (matches.length > 1) {
|
|
72
|
-
spinner
|
|
76
|
+
spinner?.stop(`Found ${matches.length} Vercel projects for this repo.`);
|
|
73
77
|
const picked = await pickFromList(allProjects);
|
|
74
78
|
return picked ? cacheResult(picked) : null;
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
// --- No match ---
|
|
78
|
-
spinner
|
|
82
|
+
spinner?.stop("Could not auto-detect Vercel project.");
|
|
79
83
|
const canCreate = gitRepo && gitRepo.provider !== "unknown";
|
|
80
84
|
const picked = await pickFromList(allProjects, canCreate ? { token, gitRepo } : undefined);
|
|
81
85
|
return picked ? cacheResult(picked) : null;
|
|
82
86
|
}
|
|
83
87
|
async function pickFromList(projects, createCtx) {
|
|
88
|
+
if (isAgent)
|
|
89
|
+
return null;
|
|
84
90
|
const maxName = Math.max(...projects.map((proj) => proj.name.length));
|
|
85
91
|
const selected = await p.select({
|
|
86
92
|
message: "Select a Vercel project",
|
|
@@ -111,6 +117,8 @@ async function pickFromList(projects, createCtx) {
|
|
|
111
117
|
};
|
|
112
118
|
}
|
|
113
119
|
async function createProjectFlow(projects, ctx) {
|
|
120
|
+
if (isAgent)
|
|
121
|
+
return null;
|
|
114
122
|
const { token, gitRepo } = ctx;
|
|
115
123
|
// Pick team from unique teams
|
|
116
124
|
const teams = [...new Map(projects.map((proj) => [proj.teamId, proj.teamName])).entries()];
|
|
@@ -182,14 +190,23 @@ async function createProjectFlow(projects, ctx) {
|
|
|
182
190
|
}
|
|
183
191
|
// --- Main resolve function ---
|
|
184
192
|
export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
const cli = await ensureVercelCli((msg) => { if (!isAgent)
|
|
194
|
+
p.log.step(msg); });
|
|
195
|
+
if (!cli.ok) {
|
|
196
|
+
if (!isAgent)
|
|
197
|
+
p.log.error("Could not install the Vercel CLI automatically.");
|
|
198
|
+
if (cli.error) {
|
|
199
|
+
if (!isAgent)
|
|
200
|
+
p.log.message(pc.dim(cli.error.trim()));
|
|
201
|
+
}
|
|
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")}.`);
|
|
204
|
+
process.exit(0);
|
|
189
205
|
}
|
|
190
206
|
const token = await ensureVercelAuth();
|
|
191
207
|
if (!token) {
|
|
192
|
-
|
|
208
|
+
if (!isAgent)
|
|
209
|
+
p.log.error("Vercel login failed.");
|
|
193
210
|
return null;
|
|
194
211
|
}
|
|
195
212
|
const project = await autoDetectProject(cwd, gitInfo, token);
|
|
@@ -209,8 +226,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
209
226
|
let commitDeploy = repoMatches ? deployments.find((d) => d.commitSha === commitSha) : undefined;
|
|
210
227
|
// If just pushed, poll until the commit deployment appears
|
|
211
228
|
if (!commitDeploy && repoMatches && opts?.justPushed) {
|
|
212
|
-
const pollSpinner = p.spinner();
|
|
213
|
-
pollSpinner
|
|
229
|
+
const pollSpinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
230
|
+
pollSpinner?.start("Waiting for deploy");
|
|
214
231
|
for (let i = 0; i < 30; i++) {
|
|
215
232
|
await new Promise((r) => setTimeout(r, 2000));
|
|
216
233
|
deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
|
|
@@ -221,10 +238,10 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
221
238
|
break;
|
|
222
239
|
}
|
|
223
240
|
if (commitDeploy) {
|
|
224
|
-
pollSpinner
|
|
241
|
+
pollSpinner?.clear();
|
|
225
242
|
}
|
|
226
243
|
else {
|
|
227
|
-
pollSpinner
|
|
244
|
+
pollSpinner?.stop("No deployment detected yet — Vercel may still be processing.");
|
|
228
245
|
}
|
|
229
246
|
}
|
|
230
247
|
// If we found a commit-specific deployment, handle based on state
|
|
@@ -234,7 +251,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
234
251
|
? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
|
|
235
252
|
: "";
|
|
236
253
|
if (commitDeploy.state === "ERROR" || commitDeploy.state === "CANCELED") {
|
|
237
|
-
|
|
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.`);
|
|
238
256
|
commitDeploy = undefined;
|
|
239
257
|
// Fall through to the picker
|
|
240
258
|
}
|
|
@@ -242,24 +260,19 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
242
260
|
commitDeploy.state === "QUEUED" ||
|
|
243
261
|
commitDeploy.state === "INITIALIZING") {
|
|
244
262
|
// Show what we're waiting on
|
|
245
|
-
|
|
263
|
+
if (!isAgent)
|
|
264
|
+
p.log.info(`Vercel deployment for ${commitLabel}${message} — ${pc.yellow(commitDeploy.state.toLowerCase())}`);
|
|
246
265
|
// Poll with a ticking timer for up to ~2 minutes
|
|
247
266
|
const buildingUrl = commitDeploy.url;
|
|
248
267
|
const startTime = Date.now();
|
|
249
268
|
const maxWaitMs = 120_000;
|
|
250
|
-
const pollIntervalMs =
|
|
269
|
+
const pollIntervalMs = 3_000;
|
|
251
270
|
let resolved = false;
|
|
252
|
-
const spinner = p.spinner();
|
|
253
|
-
|
|
254
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
255
|
-
spinner.message(`Waiting for Vercel to finish building... (${elapsed}s)`);
|
|
256
|
-
};
|
|
257
|
-
spinner.start("Waiting for Vercel to finish building... (0s) ");
|
|
258
|
-
const ticker = setInterval(updateMessage, 1000);
|
|
271
|
+
const spinner = !isAgent ? p.spinner({ indicator: "timer" }) : null;
|
|
272
|
+
spinner?.start("Waiting for build");
|
|
259
273
|
try {
|
|
260
274
|
while (Date.now() - startTime < maxWaitMs) {
|
|
261
275
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
262
|
-
updateMessage();
|
|
263
276
|
const freshDeps = await getVercelDeployments(token, project.teamId, project.projectId, {
|
|
264
277
|
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
265
278
|
});
|
|
@@ -267,25 +280,22 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
267
280
|
if (!fresh)
|
|
268
281
|
break;
|
|
269
282
|
if (fresh.state === "READY") {
|
|
270
|
-
|
|
271
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
272
|
-
spinner.stop(`Vercel deployment ready! (${elapsed}s)`);
|
|
273
|
-
p.log.info(`Vercel deployment for ${commitLabel}${message}:\n → ${pc.cyan(fresh.url)}`);
|
|
283
|
+
spinner?.stop("Vercel deployment ready!");
|
|
274
284
|
return fresh.url;
|
|
275
285
|
}
|
|
276
286
|
if (fresh.state === "ERROR" || fresh.state === "CANCELED") {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.`);
|
|
280
290
|
commitDeploy = undefined;
|
|
281
291
|
resolved = true;
|
|
282
292
|
break;
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
295
|
if (!resolved) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
spinner
|
|
296
|
+
if (isAgent)
|
|
297
|
+
return buildingUrl;
|
|
298
|
+
spinner?.stop("Still building...");
|
|
289
299
|
const action = await p.select({
|
|
290
300
|
message: "Vercel deployment is still building.",
|
|
291
301
|
options: [
|
|
@@ -298,21 +308,25 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
298
308
|
process.exit(0);
|
|
299
309
|
}
|
|
300
310
|
if (action === "continue") {
|
|
301
|
-
|
|
311
|
+
if (!isAgent)
|
|
312
|
+
p.log.info(`Vercel deployment for ${commitLabel}${message}:\n → ${pc.cyan(buildingUrl)} ${pc.yellow("(building)")}`);
|
|
302
313
|
return buildingUrl;
|
|
303
314
|
}
|
|
304
315
|
commitDeploy = undefined;
|
|
305
316
|
// Fall through to picker
|
|
306
317
|
}
|
|
307
318
|
}
|
|
308
|
-
catch {
|
|
309
|
-
|
|
310
|
-
|
|
319
|
+
catch (e) {
|
|
320
|
+
spinner?.stop("Error checking deployment status.");
|
|
321
|
+
if (e instanceof Error)
|
|
322
|
+
if (!isAgent)
|
|
323
|
+
p.log.message(pc.dim(e.message));
|
|
311
324
|
}
|
|
312
325
|
}
|
|
313
326
|
else {
|
|
314
327
|
// READY or any other terminal state — use it
|
|
315
|
-
|
|
328
|
+
if (!isAgent)
|
|
329
|
+
p.log.info(`Vercel deployment for ${commitLabel}${message}`);
|
|
316
330
|
return commitDeploy.url;
|
|
317
331
|
}
|
|
318
332
|
}
|
|
@@ -322,14 +336,20 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
322
336
|
});
|
|
323
337
|
// Fallback: no commit deployment found — let user pick from recent or paste manually
|
|
324
338
|
if (deployments.length === 0) {
|
|
325
|
-
|
|
339
|
+
if (!isAgent)
|
|
340
|
+
p.log.warn("No deployments found. Paste a URL instead.");
|
|
326
341
|
return null;
|
|
327
342
|
}
|
|
328
343
|
const hasWorkingDeployment = deployments.some((d) => d.state !== "ERROR" && d.state !== "CANCELED");
|
|
329
344
|
if (!hasWorkingDeployment) {
|
|
330
|
-
|
|
345
|
+
if (!isAgent)
|
|
346
|
+
p.log.warn("All recent deployments failed. Fix the build and push again, or paste a URL.");
|
|
331
347
|
return null;
|
|
332
348
|
}
|
|
349
|
+
if (isAgent) {
|
|
350
|
+
const ready = deployments.find((d) => d.state === "READY");
|
|
351
|
+
return ready?.url ?? deployments[0]?.url ?? null;
|
|
352
|
+
}
|
|
333
353
|
const maxBranchPick = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
|
|
334
354
|
const selected = await p.select({
|
|
335
355
|
message: "No deployment found for current commit. Select one:",
|
|
@@ -361,6 +381,8 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
361
381
|
// Warn if the user picked a failed deployment
|
|
362
382
|
const pickedDeploy = deployments.find((d) => d.url === selected);
|
|
363
383
|
if (pickedDeploy && (pickedDeploy.state === "ERROR" || pickedDeploy.state === "CANCELED")) {
|
|
384
|
+
if (isAgent)
|
|
385
|
+
return null;
|
|
364
386
|
p.log.warn("This deployment failed — the URL may not load.");
|
|
365
387
|
const confirm = await p.confirm({ message: "Use it anyway?" });
|
|
366
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",
|