inflight-cli 2.1.5 → 2.1.7
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/share.js +89 -6
- package/dist/lib/api.d.ts +15 -0
- package/dist/lib/api.js +12 -0
- package/dist/lib/git.d.ts +5 -0
- package/dist/lib/git.js +9 -0
- package/dist/providers/vercel.js +31 -35
- package/package.json +1 -1
package/dist/commands/share.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
|
|
4
|
-
import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch } from "../lib/git.js";
|
|
4
|
+
import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead, } from "../lib/git.js";
|
|
5
5
|
import open from "open";
|
|
6
6
|
import { providers } from "../providers/index.js";
|
|
7
|
-
import { apiGetMe, apiCreateVersion } from "../lib/api.js";
|
|
7
|
+
import { apiGetMe, apiCreateVersion, apiGetRecentProjects } from "../lib/api.js";
|
|
8
|
+
function formatRelativeTime(timestampMs) {
|
|
9
|
+
const seconds = Math.floor((Date.now() - timestampMs) / 1000);
|
|
10
|
+
if (seconds < 60)
|
|
11
|
+
return "just now";
|
|
12
|
+
const minutes = Math.floor(seconds / 60);
|
|
13
|
+
if (minutes < 60)
|
|
14
|
+
return `${minutes}m ago`;
|
|
15
|
+
const hours = Math.floor(minutes / 60);
|
|
16
|
+
if (hours < 24)
|
|
17
|
+
return `${hours}h ago`;
|
|
18
|
+
const days = Math.floor(hours / 24);
|
|
19
|
+
if (days === 1)
|
|
20
|
+
return "yesterday";
|
|
21
|
+
if (days < 7)
|
|
22
|
+
return `${days}d ago`;
|
|
23
|
+
return new Date(timestampMs).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
24
|
+
}
|
|
8
25
|
/**
|
|
9
26
|
* Checks local git state and prompts the user to commit/push if needed.
|
|
10
27
|
* Returns { justPushed: true } if changes were pushed, { justPushed: false } otherwise.
|
|
@@ -126,7 +143,13 @@ async function checkAndSyncGit(cwd) {
|
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
if (state.status === "no_remote") {
|
|
129
|
-
// Working tree is clean (uncommitted was checked first in getGitSyncState)
|
|
146
|
+
// Working tree is clean (uncommitted was checked first in getGitSyncState).
|
|
147
|
+
// Check if this branch actually has commits ahead of the default branch —
|
|
148
|
+
// if not, pushing won't trigger a deployment.
|
|
149
|
+
if (!hasCommitsAhead(cwd)) {
|
|
150
|
+
p.log.info(`Branch ${pc.bold(branch)} has no new commits — using existing deployments.`);
|
|
151
|
+
return { justPushed: false };
|
|
152
|
+
}
|
|
130
153
|
p.log.warn(`Branch ${pc.bold(branch)} hasn't been pushed yet — no deployment exists.`);
|
|
131
154
|
const confirm = await p.confirm({
|
|
132
155
|
message: "Push to create a deployment?",
|
|
@@ -154,14 +177,15 @@ async function checkAndSyncGit(cwd) {
|
|
|
154
177
|
}
|
|
155
178
|
export async function shareCommand(opts = {}) {
|
|
156
179
|
const cwd = process.cwd();
|
|
180
|
+
// TODO: Add a step to login if not authenticated
|
|
157
181
|
// ── Step 1: Auth ──
|
|
158
182
|
const auth = readGlobalAuth();
|
|
159
183
|
if (!auth) {
|
|
160
184
|
if (opts.json) {
|
|
161
|
-
console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight
|
|
185
|
+
console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight setup first." }));
|
|
162
186
|
}
|
|
163
187
|
else {
|
|
164
|
-
p.log.error("Not logged in. Run " + pc.cyan("inflight
|
|
188
|
+
p.log.error("Not logged in. Run " + pc.cyan("inflight setup") + " first.");
|
|
165
189
|
}
|
|
166
190
|
process.exit(1);
|
|
167
191
|
}
|
|
@@ -296,16 +320,75 @@ export async function shareCommand(opts = {}) {
|
|
|
296
320
|
if (!stagingUrl.startsWith("http")) {
|
|
297
321
|
stagingUrl = `https://${stagingUrl}`;
|
|
298
322
|
}
|
|
323
|
+
// ── Step 4: Project selection ──
|
|
324
|
+
let selectedProjectId;
|
|
325
|
+
let overrideVersionId;
|
|
326
|
+
const { projects: recentProjects } = await apiGetRecentProjects(auth.apiKey, workspaceId).catch(() => ({
|
|
327
|
+
projects: [],
|
|
328
|
+
}));
|
|
329
|
+
if (recentProjects.length > 0) {
|
|
330
|
+
const projectChoice = await p.select({
|
|
331
|
+
message: "Add to an existing version or start fresh?",
|
|
332
|
+
options: [
|
|
333
|
+
...recentProjects.map((proj) => ({
|
|
334
|
+
value: proj.projectId,
|
|
335
|
+
label: `"${proj.latestVersion.title}"`,
|
|
336
|
+
hint: `created ${formatRelativeTime(proj.latestVersion.createdAt)}`,
|
|
337
|
+
})),
|
|
338
|
+
{ value: "new", label: "Start fresh" },
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
if (p.isCancel(projectChoice)) {
|
|
342
|
+
p.cancel("Cancelled.");
|
|
343
|
+
process.exit(0);
|
|
344
|
+
}
|
|
345
|
+
if (projectChoice !== "new") {
|
|
346
|
+
selectedProjectId = projectChoice;
|
|
347
|
+
// Check for override opportunity — only when latest version has no feedback
|
|
348
|
+
const selectedProject = recentProjects.find((proj) => proj.projectId === selectedProjectId);
|
|
349
|
+
if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
|
|
350
|
+
const overrideChoice = await p.select({
|
|
351
|
+
message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
|
|
352
|
+
options: [
|
|
353
|
+
{
|
|
354
|
+
value: "override",
|
|
355
|
+
label: "Update its staging URL",
|
|
356
|
+
hint: `replace with ${new URL(stagingUrl).hostname}`,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
value: "new_version",
|
|
360
|
+
label: "Add a new version",
|
|
361
|
+
hint: "keep both in version history",
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
if (p.isCancel(overrideChoice)) {
|
|
366
|
+
p.cancel("Cancelled.");
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
if (overrideChoice === "override") {
|
|
370
|
+
overrideVersionId = selectedProject.latestVersion.id;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
299
375
|
await apiCreateVersion({
|
|
300
376
|
apiKey: auth.apiKey,
|
|
301
377
|
workspaceId,
|
|
302
378
|
stagingUrl,
|
|
303
379
|
gitInfo,
|
|
380
|
+
...(selectedProjectId && { projectId: selectedProjectId }),
|
|
381
|
+
...(overrideVersionId && { overrideVersionId }),
|
|
304
382
|
}).catch((e) => {
|
|
305
383
|
p.log.error(e.message);
|
|
306
384
|
process.exit(1);
|
|
307
385
|
});
|
|
308
386
|
p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
|
|
309
|
-
|
|
387
|
+
if (overrideVersionId) {
|
|
388
|
+
p.outro(pc.green("✓ Staging URL updated") + " — opening in browser...");
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
|
|
392
|
+
}
|
|
310
393
|
await open(stagingUrl);
|
|
311
394
|
}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -9,16 +9,31 @@ export interface CreateVersionResult {
|
|
|
9
9
|
versionId: string;
|
|
10
10
|
inflightUrl: string;
|
|
11
11
|
}
|
|
12
|
+
export interface RecentProject {
|
|
13
|
+
projectId: string;
|
|
14
|
+
latestVersion: {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
stagingUrl: string | null;
|
|
18
|
+
commentCount: number;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
12
22
|
export declare function apiGetMe(apiKey: string): Promise<{
|
|
13
23
|
name: string | null;
|
|
14
24
|
email: string | null;
|
|
15
25
|
workspaces: Workspace[];
|
|
16
26
|
}>;
|
|
27
|
+
export declare function apiGetRecentProjects(apiKey: string, workspaceId: string): Promise<{
|
|
28
|
+
projects: RecentProject[];
|
|
29
|
+
}>;
|
|
17
30
|
export declare function apiCreateVersion(opts: {
|
|
18
31
|
apiKey: string;
|
|
19
32
|
workspaceId: string;
|
|
20
33
|
stagingUrl: string;
|
|
21
34
|
gitInfo: GitInfo;
|
|
35
|
+
projectId?: string;
|
|
36
|
+
overrideVersionId?: string;
|
|
22
37
|
}): Promise<CreateVersionResult>;
|
|
23
38
|
export interface WidgetLocationResult {
|
|
24
39
|
file: string | null;
|
package/dist/lib/api.js
CHANGED
|
@@ -7,6 +7,16 @@ export async function apiGetMe(apiKey) {
|
|
|
7
7
|
throw new Error("Invalid API key");
|
|
8
8
|
return res.json();
|
|
9
9
|
}
|
|
10
|
+
export async function apiGetRecentProjects(apiKey, workspaceId) {
|
|
11
|
+
const res = await fetch(`${API_URL}/api/cli/projects/recent?workspaceId=${encodeURIComponent(workspaceId)}`, {
|
|
12
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
16
|
+
throw new Error(body.error ?? `API error ${res.status}`);
|
|
17
|
+
}
|
|
18
|
+
return res.json();
|
|
19
|
+
}
|
|
10
20
|
export async function apiCreateVersion(opts) {
|
|
11
21
|
const res = await fetch(`${API_URL}/api/cli/version/create`, {
|
|
12
22
|
method: "POST",
|
|
@@ -18,6 +28,8 @@ export async function apiCreateVersion(opts) {
|
|
|
18
28
|
workspaceId: opts.workspaceId,
|
|
19
29
|
stagingUrl: opts.stagingUrl,
|
|
20
30
|
gitInfo: opts.gitInfo,
|
|
31
|
+
...(opts.projectId && { projectId: opts.projectId }),
|
|
32
|
+
...(opts.overrideVersionId && { overrideVersionId: opts.overrideVersionId }),
|
|
21
33
|
}),
|
|
22
34
|
});
|
|
23
35
|
if (!res.ok) {
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -74,4 +74,9 @@ export declare function commitAndPush(cwd: string, message: string, branch: stri
|
|
|
74
74
|
/**
|
|
75
75
|
* Pushes existing commits. Uses `-u` if no upstream exists.
|
|
76
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* Returns true if the current branch has commits ahead of the default branch (main/master).
|
|
79
|
+
* Used to avoid pushing branches with no new commits (which won't trigger a deployment).
|
|
80
|
+
*/
|
|
81
|
+
export declare function hasCommitsAhead(cwd: string): boolean;
|
|
77
82
|
export declare function pushBranch(cwd: string, branch: string): void;
|
package/dist/lib/git.js
CHANGED
|
@@ -315,6 +315,15 @@ export function commitAndPush(cwd, message, branch) {
|
|
|
315
315
|
/**
|
|
316
316
|
* Pushes existing commits. Uses `-u` if no upstream exists.
|
|
317
317
|
*/
|
|
318
|
+
/**
|
|
319
|
+
* Returns true if the current branch has commits ahead of the default branch (main/master).
|
|
320
|
+
* Used to avoid pushing branches with no new commits (which won't trigger a deployment).
|
|
321
|
+
*/
|
|
322
|
+
export function hasCommitsAhead(cwd) {
|
|
323
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
324
|
+
const count = run(`git rev-list ${defaultBranch}..HEAD --count`, cwd);
|
|
325
|
+
return count !== null && count !== "0";
|
|
326
|
+
}
|
|
318
327
|
export function pushBranch(cwd, branch) {
|
|
319
328
|
const upstream = run(`git rev-parse --abbrev-ref ${branch}@{upstream}`, cwd);
|
|
320
329
|
if (upstream) {
|
package/dist/providers/vercel.js
CHANGED
|
@@ -3,7 +3,7 @@ import pc from "picocolors";
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
5
5
|
import { writeVercelConfig } from "../lib/config.js";
|
|
6
|
-
import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectDetail, fetchAllProjectsWithLinks, matchProjectsByRepo, createVercelProject,
|
|
6
|
+
import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectDetail, fetchAllProjectsWithLinks, matchProjectsByRepo, createVercelProject, getRecentDeployments, } from "../lib/vercel.js";
|
|
7
7
|
// --- Auto-detection ---
|
|
8
8
|
/**
|
|
9
9
|
* Auto-detect the Vercel project for the current git repo.
|
|
@@ -29,7 +29,7 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
29
29
|
if (localProject) {
|
|
30
30
|
const detail = await getVercelProjectDetail(token, localProject.projectId, localProject.orgId);
|
|
31
31
|
if (detail) {
|
|
32
|
-
p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
|
|
32
|
+
// p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
|
|
33
33
|
return { teamId: localProject.orgId, projectId: detail.id, projectName: detail.name };
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -196,61 +196,57 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
196
196
|
const project = await autoDetectProject(cwd, gitInfo, token);
|
|
197
197
|
if (!project)
|
|
198
198
|
return null;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
const commitSha = gitInfo.commitShort?.slice(0, 7) ?? null;
|
|
200
|
+
// Try to find a deployment matching the current commit
|
|
201
|
+
let deployments = await getRecentDeployments(token, project.teamId, project.projectId, {
|
|
202
|
+
branch: gitInfo.branch ?? undefined,
|
|
203
|
+
});
|
|
204
|
+
let commitDeploy = deployments.find((d) => d.commitSha === commitSha);
|
|
205
|
+
// If just pushed, poll until the commit deployment appears
|
|
206
|
+
if (!commitDeploy && opts?.justPushed) {
|
|
202
207
|
const pollSpinner = p.spinner();
|
|
203
208
|
pollSpinner.start("Waiting for Vercel deployment...");
|
|
204
209
|
for (let i = 0; i < 30; i++) {
|
|
205
210
|
await new Promise((r) => setTimeout(r, 2000));
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
deployments = await getRecentDeployments(token, project.teamId, project.projectId, {
|
|
212
|
+
branch: gitInfo.branch ?? undefined,
|
|
213
|
+
});
|
|
214
|
+
commitDeploy = deployments.find((d) => d.commitSha === commitSha);
|
|
215
|
+
if (commitDeploy)
|
|
208
216
|
break;
|
|
209
217
|
}
|
|
210
|
-
if (
|
|
218
|
+
if (commitDeploy) {
|
|
211
219
|
pollSpinner.stop("Deployment found!");
|
|
212
220
|
}
|
|
213
221
|
else {
|
|
214
222
|
pollSpinner.stop("Deployment is still building...");
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{ value: "manual", label: "Paste a URL manually" },
|
|
226
|
-
],
|
|
227
|
-
});
|
|
228
|
-
if (p.isCancel(choice)) {
|
|
229
|
-
p.cancel("Cancelled.");
|
|
230
|
-
process.exit(0);
|
|
231
|
-
}
|
|
232
|
-
if (choice === "branch")
|
|
233
|
-
return branchAlias.url;
|
|
234
|
-
if (choice === "manual")
|
|
235
|
-
return null;
|
|
225
|
+
// If we found a commit-specific deployment, use it automatically
|
|
226
|
+
if (commitDeploy) {
|
|
227
|
+
const stateLabel = commitDeploy.state !== "READY" ? ` ${pc.yellow(`(${commitDeploy.state.toLowerCase()})`)}` : "";
|
|
228
|
+
const message = commitDeploy.commitMessage
|
|
229
|
+
? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
|
|
230
|
+
: "";
|
|
231
|
+
p.log.info(`Deployment for ${pc.bold(commitSha)}${message}:\n → ${pc.cyan(commitDeploy.url)}${stateLabel}`);
|
|
232
|
+
return commitDeploy.url;
|
|
236
233
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
if (recent.length === 0) {
|
|
234
|
+
// Fallback: no commit deployment found — let user pick from recent or paste manually
|
|
235
|
+
if (deployments.length === 0) {
|
|
240
236
|
p.log.warn("No deployments found. Paste a URL instead.");
|
|
241
237
|
return null;
|
|
242
238
|
}
|
|
243
|
-
const maxBranch = Math.max(...
|
|
239
|
+
const maxBranch = Math.max(...deployments.map((d) => (d.branch ?? "unknown").length));
|
|
244
240
|
const selected = await p.select({
|
|
245
|
-
message: "
|
|
241
|
+
message: "No deployment found for current commit. Select one:",
|
|
246
242
|
options: [
|
|
247
|
-
...
|
|
243
|
+
...deployments.map((d) => {
|
|
248
244
|
const branch = (d.branch ?? "unknown").padEnd(maxBranch);
|
|
249
245
|
const ago = timeAgo(d.createdAt).padEnd(8);
|
|
250
246
|
const state = d.state !== "READY" ? ` ${pc.yellow(`(${d.state.toLowerCase()})`)}` : "";
|
|
251
247
|
const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
|
|
252
|
-
const
|
|
253
|
-
return { value: d.url, label: `${branch} ${ago}${state} ${pc.dim(
|
|
248
|
+
const msg = truncate(firstLine, 55);
|
|
249
|
+
return { value: d.url, label: `${branch} ${ago}${state} ${pc.dim(msg)}` };
|
|
254
250
|
}),
|
|
255
251
|
{ value: "manual", label: "Paste a URL manually" },
|
|
256
252
|
],
|