inflight-cli 1.1.4 → 2.0.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 +10 -9
- package/dist/commands/logout.js +0 -1
- package/dist/commands/preview.js +2 -3
- package/dist/commands/reset.d.ts +1 -0
- package/dist/commands/reset.js +17 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +168 -0
- package/dist/commands/share.d.ts +6 -1
- package/dist/commands/share.js +44 -13
- package/dist/commands/vercel.d.ts +2 -0
- package/dist/commands/vercel.js +169 -0
- package/dist/commands/workspace.js +2 -4
- package/dist/commands/workspaces.d.ts +4 -0
- package/dist/commands/workspaces.js +81 -0
- package/dist/index.js +20 -3
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +17 -0
- package/dist/lib/config.d.ts +12 -2
- package/dist/lib/config.js +29 -19
- package/dist/lib/framework.d.ts +17 -0
- package/dist/lib/framework.js +154 -0
- package/dist/lib/skill.d.ts +5 -0
- package/dist/lib/skill.js +22 -0
- package/dist/lib/vercel.d.ts +21 -11
- package/dist/lib/vercel.js +107 -83
- package/dist/providers/vercel.d.ts +6 -0
- package/dist/providers/vercel.js +67 -55
- package/package.json +3 -2
package/dist/lib/vercel.js
CHANGED
|
@@ -1,68 +1,112 @@
|
|
|
1
|
-
import { execSync,
|
|
1
|
+
import { execSync, spawn, exec } from "child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
function
|
|
4
|
+
import { homedir, platform } from "os";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
// --- Vercel CLI management ---
|
|
8
|
+
/** Checks if the Vercel CLI is installed globally. */
|
|
9
|
+
function hasVercelCli() {
|
|
10
10
|
try {
|
|
11
11
|
execSync("vercel --version", { stdio: "pipe" });
|
|
12
|
-
return
|
|
12
|
+
return true;
|
|
13
13
|
}
|
|
14
14
|
catch {
|
|
15
|
-
return
|
|
15
|
+
return false;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return data.token ?? null;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
18
|
+
/** Ensures the Vercel CLI is available — installs globally if missing. */
|
|
19
|
+
export async function ensureVercelCli(log) {
|
|
20
|
+
if (hasVercelCli())
|
|
21
|
+
return true;
|
|
22
|
+
log?.("Installing Vercel CLI...");
|
|
23
|
+
try {
|
|
24
|
+
await execAsync("npm install -g vercel");
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
35
29
|
}
|
|
36
|
-
return null;
|
|
37
30
|
}
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
/** Returns the Vercel CLI config directory for the current platform. */
|
|
32
|
+
function getVercelConfigDir() {
|
|
33
|
+
if (platform() === "darwin") {
|
|
34
|
+
return join(homedir(), "Library", "Application Support", "com.vercel.cli");
|
|
35
|
+
}
|
|
36
|
+
if (platform() === "win32") {
|
|
37
|
+
return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "com.vercel.cli");
|
|
38
|
+
}
|
|
39
|
+
return join(homedir(), ".local", "share", "com.vercel.cli");
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/** Reads the Vercel CLI auth file. */
|
|
42
|
+
function readVercelAuth() {
|
|
43
|
+
const authPath = join(getVercelConfigDir(), "auth.json");
|
|
44
|
+
if (!existsSync(authPath))
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
48
|
+
return data.token ? data : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Gets a valid Vercel token. Refreshes silently via `vercel whoami` if expired.
|
|
56
|
+
* Does NOT prompt for login — returns null if no valid token available.
|
|
57
|
+
*/
|
|
58
|
+
export function getVercelToken() {
|
|
59
|
+
const auth = readVercelAuth();
|
|
60
|
+
if (!auth)
|
|
61
|
+
return null;
|
|
62
|
+
// Token still valid
|
|
63
|
+
if (auth.expiresAt > Date.now() / 1000) {
|
|
64
|
+
return auth.token;
|
|
65
|
+
}
|
|
66
|
+
// Token expired — try silent refresh
|
|
67
|
+
try {
|
|
68
|
+
execSync("vercel whoami", { stdio: "pipe" });
|
|
69
|
+
const refreshed = readVercelAuth();
|
|
70
|
+
if (refreshed && refreshed.expiresAt > Date.now() / 1000) {
|
|
71
|
+
return refreshed.token;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Refresh failed
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
49
78
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Ensures a valid Vercel auth token is available.
|
|
81
|
+
* Checks existing token → refreshes if expired → opens browser login if needed.
|
|
82
|
+
* For interactive commands only.
|
|
83
|
+
*/
|
|
84
|
+
export async function ensureVercelAuth() {
|
|
85
|
+
// Try existing/refreshed token first
|
|
86
|
+
const existing = getVercelToken();
|
|
87
|
+
if (existing)
|
|
88
|
+
return existing;
|
|
89
|
+
// Need browser login — capture output to suppress Vercel's default messages
|
|
90
|
+
const ok = await new Promise((resolve) => {
|
|
91
|
+
const child = spawn("vercel", ["login"], { stdio: "pipe" });
|
|
92
|
+
child.stdout?.on("data", (data) => {
|
|
93
|
+
const text = data.toString();
|
|
94
|
+
// Only show the verification URL
|
|
95
|
+
const urlMatch = text.match(/(https:\/\/vercel\.com\/oauth\/device\S*)/);
|
|
96
|
+
if (urlMatch) {
|
|
97
|
+
process.stdout.write(` Visit ${urlMatch[1]}\n`);
|
|
98
|
+
}
|
|
57
99
|
});
|
|
58
100
|
child.on("close", (code) => resolve(code === 0));
|
|
59
101
|
child.on("error", () => resolve(false));
|
|
60
102
|
});
|
|
103
|
+
if (!ok)
|
|
104
|
+
return null;
|
|
105
|
+
const auth = readVercelAuth();
|
|
106
|
+
return auth?.token ?? null;
|
|
61
107
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!token)
|
|
65
|
-
return [];
|
|
108
|
+
// --- API calls (all take token + IDs explicitly) ---
|
|
109
|
+
export async function getVercelTeams(token) {
|
|
66
110
|
const res = await fetch("https://api.vercel.com/v2/teams", {
|
|
67
111
|
headers: { Authorization: `Bearer ${token}` },
|
|
68
112
|
});
|
|
@@ -71,10 +115,7 @@ export async function getVercelTeams() {
|
|
|
71
115
|
const data = (await res.json());
|
|
72
116
|
return data.teams;
|
|
73
117
|
}
|
|
74
|
-
export async function getVercelProjects(teamId) {
|
|
75
|
-
const token = getVercelAuthToken();
|
|
76
|
-
if (!token)
|
|
77
|
-
return [];
|
|
118
|
+
export async function getVercelProjects(token, teamId) {
|
|
78
119
|
const params = new URLSearchParams({ teamId, limit: "100" });
|
|
79
120
|
const res = await fetch(`https://api.vercel.com/v10/projects?${params}`, {
|
|
80
121
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -84,32 +125,16 @@ export async function getVercelProjects(teamId) {
|
|
|
84
125
|
const data = (await res.json());
|
|
85
126
|
return data.projects;
|
|
86
127
|
}
|
|
87
|
-
/** Reads the project + team IDs from `.vercel/project.json`. */
|
|
88
|
-
function getVercelProjectConfig(cwd) {
|
|
89
|
-
const configPath = join(cwd, ".vercel", "project.json");
|
|
90
|
-
if (!existsSync(configPath))
|
|
91
|
-
return null;
|
|
92
|
-
try {
|
|
93
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
128
|
/**
|
|
100
129
|
* Fetches the branch alias URL (stable, auto-updates with each push).
|
|
101
130
|
* Returns null if no deployment exists for this branch.
|
|
102
131
|
*/
|
|
103
|
-
export async function getBranchAliasUrl(
|
|
132
|
+
export async function getBranchAliasUrl(token, teamId, projectId, branch) {
|
|
104
133
|
if (!branch)
|
|
105
134
|
return null;
|
|
106
|
-
const token = getVercelAuthToken();
|
|
107
|
-
const project = getVercelProjectConfig(cwd);
|
|
108
|
-
if (!token || !project)
|
|
109
|
-
return null;
|
|
110
135
|
const params = new URLSearchParams({
|
|
111
|
-
projectId
|
|
112
|
-
teamId
|
|
136
|
+
projectId,
|
|
137
|
+
teamId,
|
|
113
138
|
limit: "1",
|
|
114
139
|
state: "READY",
|
|
115
140
|
branch,
|
|
@@ -124,30 +149,28 @@ export async function getBranchAliasUrl(cwd, branch) {
|
|
|
124
149
|
if (!deploy)
|
|
125
150
|
return null;
|
|
126
151
|
// Fetch the automatic alias for this deployment
|
|
127
|
-
const aliasParams = new URLSearchParams({ teamId
|
|
152
|
+
const aliasParams = new URLSearchParams({ teamId });
|
|
128
153
|
const aliasRes = await fetch(`https://api.vercel.com/v13/deployments/${deploy.uid}?${aliasParams}`, {
|
|
129
154
|
headers: { Authorization: `Bearer ${token}` },
|
|
130
155
|
});
|
|
131
156
|
if (!aliasRes.ok)
|
|
132
157
|
return null;
|
|
133
158
|
const aliasData = (await aliasRes.json());
|
|
134
|
-
|
|
135
|
-
return alias ?? null;
|
|
159
|
+
return aliasData.automaticAliases?.[0] ?? null;
|
|
136
160
|
}
|
|
137
161
|
/**
|
|
138
|
-
* Fetches recent deployments for
|
|
162
|
+
* Fetches recent deployments for a project.
|
|
139
163
|
*/
|
|
140
|
-
export async function getRecentDeployments(
|
|
141
|
-
const token = getVercelAuthToken();
|
|
142
|
-
const project = getVercelProjectConfig(cwd);
|
|
143
|
-
if (!token || !project)
|
|
144
|
-
return [];
|
|
164
|
+
export async function getRecentDeployments(token, teamId, projectId, opts) {
|
|
145
165
|
const params = new URLSearchParams({
|
|
146
|
-
projectId
|
|
147
|
-
teamId
|
|
148
|
-
limit: String(limit),
|
|
166
|
+
projectId,
|
|
167
|
+
teamId,
|
|
168
|
+
limit: String(opts?.limit ?? 10),
|
|
149
169
|
state: "READY",
|
|
150
170
|
});
|
|
171
|
+
if (opts?.branch) {
|
|
172
|
+
params.set("branch", opts.branch);
|
|
173
|
+
}
|
|
151
174
|
const res = await fetch(`https://api.vercel.com/v6/deployments?${params}`, {
|
|
152
175
|
headers: { Authorization: `Bearer ${token}` },
|
|
153
176
|
});
|
|
@@ -157,7 +180,8 @@ export async function getRecentDeployments(cwd, limit = 10) {
|
|
|
157
180
|
return data.deployments.map((d) => ({
|
|
158
181
|
url: d.url,
|
|
159
182
|
branch: d.meta?.githubCommitRef ?? d.meta?.gitlabCommitRef ?? d.meta?.bitbucketCommitRef ?? null,
|
|
160
|
-
commitSha: (d.meta?.githubCommitSha ?? d.meta?.gitlabCommitSha ?? d.meta?.bitbucketCommitSha ?? null)?.slice(0, 7) ??
|
|
183
|
+
commitSha: (d.meta?.githubCommitSha ?? d.meta?.gitlabCommitSha ?? d.meta?.bitbucketCommitSha ?? null)?.slice(0, 7) ??
|
|
184
|
+
null,
|
|
161
185
|
commitMessage: d.meta?.githubCommitMessage ?? d.meta?.gitlabCommitMessage ?? d.meta?.bitbucketCommitMessage ?? null,
|
|
162
186
|
createdAt: d.created,
|
|
163
187
|
}));
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { GitInfo } from "../lib/git.js";
|
|
2
|
+
import type { VercelConfig } from "../lib/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interactive team/project picker. Saves selection to global config.
|
|
5
|
+
* Reused by both `inflight vercel` and the share flow.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pickVercelProject(token: string): Promise<VercelConfig | null>;
|
|
2
8
|
export declare function resolveVercelUrl(cwd: string, gitInfo: GitInfo): Promise<string | null>;
|
package/dist/providers/vercel.js
CHANGED
|
@@ -1,64 +1,76 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
3
|
+
import { readVercelConfig, writeVercelConfig } from "../lib/config.js";
|
|
4
|
+
import { ensureVercelCli, ensureVercelAuth, getVercelTeams, getVercelProjects, getBranchAliasUrl, getRecentDeployments, } from "../lib/vercel.js";
|
|
5
|
+
/**
|
|
6
|
+
* Interactive team/project picker. Saves selection to global config.
|
|
7
|
+
* Reused by both `inflight vercel` and the share flow.
|
|
8
|
+
*/
|
|
9
|
+
export async function pickVercelProject(token) {
|
|
10
|
+
const teams = await getVercelTeams(token);
|
|
11
|
+
if (teams.length === 0) {
|
|
12
|
+
p.log.error("No Vercel teams found.");
|
|
13
|
+
return null;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
teamId = teams[0].id;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
const selectedTeam = await p.select({
|
|
30
|
-
message: "Select a Vercel team",
|
|
31
|
-
options: teams.map((t) => ({ value: t.id, label: t.name })),
|
|
32
|
-
});
|
|
33
|
-
if (p.isCancel(selectedTeam)) {
|
|
34
|
-
p.cancel("Cancelled.");
|
|
35
|
-
process.exit(0);
|
|
36
|
-
}
|
|
37
|
-
teamId = selectedTeam;
|
|
38
|
-
}
|
|
39
|
-
const projects = await getVercelProjects(teamId);
|
|
40
|
-
if (projects.length === 0) {
|
|
41
|
-
p.log.error("No Vercel projects found.");
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
const selectedProject = await p.select({
|
|
45
|
-
message: "Select a Vercel project",
|
|
46
|
-
options: projects.map((proj) => ({ value: proj.id, label: proj.name })),
|
|
15
|
+
let teamId;
|
|
16
|
+
let teamName;
|
|
17
|
+
if (teams.length === 1) {
|
|
18
|
+
teamId = teams[0].id;
|
|
19
|
+
teamName = teams[0].name;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const selected = await p.select({
|
|
23
|
+
message: "Select a Vercel team",
|
|
24
|
+
options: teams.map((t) => ({ value: t.id, label: t.name })),
|
|
47
25
|
});
|
|
48
|
-
if (p.isCancel(
|
|
26
|
+
if (p.isCancel(selected)) {
|
|
49
27
|
p.cancel("Cancelled.");
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
spinner.start("Linking to Vercel project...");
|
|
53
|
-
const ok = await vercelLink(cwd, selectedProject);
|
|
54
|
-
spinner.stop(ok ? "Linked to Vercel project" : "");
|
|
55
|
-
if (!ok) {
|
|
56
|
-
p.log.error("Vercel link failed.");
|
|
57
|
-
process.exit(1);
|
|
28
|
+
return null;
|
|
58
29
|
}
|
|
30
|
+
teamId = selected;
|
|
31
|
+
teamName = teams.find((t) => t.id === teamId)?.name ?? teamId;
|
|
32
|
+
}
|
|
33
|
+
const projects = await getVercelProjects(token, teamId);
|
|
34
|
+
if (projects.length === 0) {
|
|
35
|
+
p.log.error("No Vercel projects found for this team.");
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const selectedProject = await p.select({
|
|
39
|
+
message: "Select a Vercel project",
|
|
40
|
+
options: projects.map((proj) => ({ value: proj.id, label: proj.name })),
|
|
41
|
+
});
|
|
42
|
+
if (p.isCancel(selectedProject)) {
|
|
43
|
+
p.cancel("Cancelled.");
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const projectId = selectedProject;
|
|
47
|
+
const projectName = projects.find((proj) => proj.id === projectId)?.name ?? projectId;
|
|
48
|
+
const config = { teamId, teamName, projectId, projectName };
|
|
49
|
+
writeVercelConfig(config);
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
export async function resolveVercelUrl(cwd, gitInfo) {
|
|
53
|
+
// Ensure Vercel CLI is installed (only logs if installing)
|
|
54
|
+
const cliOk = await ensureVercelCli((msg) => p.log.step(msg));
|
|
55
|
+
if (!cliOk) {
|
|
56
|
+
p.log.error("Failed to install Vercel CLI. Install manually: " + pc.cyan("npm install -g vercel"));
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Ensure auth
|
|
60
|
+
const token = await ensureVercelAuth();
|
|
61
|
+
if (!token) {
|
|
62
|
+
p.log.error("Vercel login failed.");
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// Get or pick Vercel project
|
|
66
|
+
let config = readVercelConfig();
|
|
67
|
+
if (!config) {
|
|
68
|
+
config = await pickVercelProject(token);
|
|
69
|
+
if (!config)
|
|
70
|
+
return null;
|
|
59
71
|
}
|
|
60
72
|
// Fetch branch alias
|
|
61
|
-
const branchAlias = await getBranchAliasUrl(
|
|
73
|
+
const branchAlias = await getBranchAliasUrl(token, config.teamId, config.projectId, gitInfo.branch);
|
|
62
74
|
if (branchAlias) {
|
|
63
75
|
p.log.info(`Branch preview (auto-updates with each push):\n → ${pc.cyan(branchAlias)}`);
|
|
64
76
|
const choice = await p.select({
|
|
@@ -78,8 +90,8 @@ export async function resolveVercelUrl(cwd, gitInfo) {
|
|
|
78
90
|
if (choice === "manual")
|
|
79
91
|
return null;
|
|
80
92
|
}
|
|
81
|
-
// Show recent deployments
|
|
82
|
-
const recent = await getRecentDeployments(
|
|
93
|
+
// Show recent deployments
|
|
94
|
+
const recent = await getRecentDeployments(token, config.teamId, config.projectId);
|
|
83
95
|
if (recent.length === 0) {
|
|
84
96
|
p.log.warn("No deployments found. Paste a URL instead.");
|
|
85
97
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inflight-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Get feedback directly on your staging URL",
|
|
5
5
|
"bin": {
|
|
6
6
|
"inflight": "dist/index.js"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"homepage": "https://www.inflight.co",
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc && chmod +x dist/index.js",
|
|
17
|
-
"dev": "tsx src/index.ts"
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
21
|
"@clack/prompts": "^0.8.2",
|