inflight-cli 2.0.9 → 2.1.1
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/README.md +1 -2
- package/dist/commands/setup.js +48 -22
- package/dist/commands/share.js +36 -27
- package/dist/commands/vercel.js +64 -143
- package/dist/index.js +7 -10
- package/dist/lib/git.d.ts +8 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/vercel.d.ts +50 -1
- package/dist/lib/vercel.js +107 -7
- package/dist/providers/vercel.d.ts +2 -2
- package/dist/providers/vercel.js +151 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,8 +16,7 @@ That's it. The CLI installs globally, logs you in, adds the widget to your proje
|
|
|
16
16
|
| --------------------- | -------------------------------- |
|
|
17
17
|
| `inflight setup` | Set up Inflight in your project |
|
|
18
18
|
| `inflight share` | Share a staging URL for feedback |
|
|
19
|
-
| `inflight
|
|
20
|
-
| `inflight workspaces` | Change your Inflight workspace |
|
|
19
|
+
| `inflight workspace` | Get or set your active workspace |
|
|
21
20
|
| `inflight login` | Log in to your Inflight account |
|
|
22
21
|
| `inflight logout` | Log out of your Inflight account |
|
|
23
22
|
|
package/dist/commands/setup.js
CHANGED
|
@@ -8,6 +8,20 @@ import { shareCommand } from "./share.js";
|
|
|
8
8
|
import { gatherProjectContext, hasInflightWidget, insertWidgetScript } from "../lib/framework.js";
|
|
9
9
|
import { isGitRepo } from "../lib/git.js";
|
|
10
10
|
import { installSkill } from "../lib/skill.js";
|
|
11
|
+
function execSyncErrorDetail(err) {
|
|
12
|
+
if (err !== null && typeof err === "object" && "stderr" in err) {
|
|
13
|
+
const b = err.stderr;
|
|
14
|
+
if (Buffer.isBuffer(b) && b.length > 0) {
|
|
15
|
+
const text = b.toString("utf-8").trim();
|
|
16
|
+
if (text)
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (err instanceof Error && err.message) {
|
|
21
|
+
return err.message.trim();
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
11
25
|
export async function setupCommand() {
|
|
12
26
|
const cwd = process.cwd();
|
|
13
27
|
// ── Step 1: Install agent skill ──
|
|
@@ -41,7 +55,7 @@ export async function setupCommand() {
|
|
|
41
55
|
const existingWorkspace = existingConfig ? workspaces.find((w) => w.id === existingConfig.workspaceId) : null;
|
|
42
56
|
if (existingWorkspace) {
|
|
43
57
|
workspaceId = existingWorkspace.id;
|
|
44
|
-
p.log.success(`Workspace: ${pc.bold(existingWorkspace.name)} ${pc.dim("(change anytime with inflight
|
|
58
|
+
p.log.success(`Workspace: ${pc.bold(existingWorkspace.name)} ${pc.dim("(change anytime with inflight workspace)")}`);
|
|
45
59
|
}
|
|
46
60
|
else if (workspaces.length === 0) {
|
|
47
61
|
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
@@ -53,7 +67,7 @@ export async function setupCommand() {
|
|
|
53
67
|
}
|
|
54
68
|
else {
|
|
55
69
|
const selected = await p.select({
|
|
56
|
-
message: "Select a workspace " + pc.dim("(change anytime with inflight
|
|
70
|
+
message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
|
|
57
71
|
options: workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
58
72
|
});
|
|
59
73
|
if (p.isCancel(selected)) {
|
|
@@ -75,31 +89,43 @@ export async function setupCommand() {
|
|
|
75
89
|
else {
|
|
76
90
|
const spinner = p.spinner();
|
|
77
91
|
spinner.start("Detecting framework...");
|
|
78
|
-
const context = gatherProjectContext(cwd);
|
|
79
|
-
const location = await apiDetectWidgetLocation({
|
|
80
|
-
apiKey: auth.apiKey,
|
|
81
|
-
fileTree: context.fileTree,
|
|
82
|
-
fileContents: context.fileContents,
|
|
83
|
-
});
|
|
84
92
|
let inserted = false;
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
try {
|
|
94
|
+
const context = gatherProjectContext(cwd);
|
|
95
|
+
const location = await apiDetectWidgetLocation({
|
|
96
|
+
apiKey: auth.apiKey,
|
|
97
|
+
fileTree: context.fileTree,
|
|
98
|
+
fileContents: context.fileContents,
|
|
99
|
+
});
|
|
100
|
+
if (location.file && location.insertAfter && location.confidence === "high") {
|
|
101
|
+
const result = insertWidgetScript(cwd, location.file, location.insertAfter, widgetId);
|
|
102
|
+
if (result) {
|
|
103
|
+
spinner.stop(`Detected ${pc.bold(location.framework ?? "framework")} — widget script tag added to ${pc.cyan(location.file)}`);
|
|
104
|
+
inserted = true;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
spinner.stop("Could not update the detected file. Add the snippet manually below.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
spinner.stop("Could not auto-detect where to add the widget.");
|
|
90
112
|
}
|
|
91
113
|
}
|
|
92
|
-
|
|
93
|
-
spinner.stop("Could not
|
|
114
|
+
catch {
|
|
115
|
+
spinner.stop("Could not analyze your project.");
|
|
94
116
|
}
|
|
95
117
|
if (!inserted) {
|
|
96
118
|
p.log.message(`Add this snippet to your root HTML layout, just before ${pc.cyan("</body>")}:\n\n` +
|
|
97
119
|
pc.dim(` <script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`));
|
|
98
|
-
await p.text({
|
|
120
|
+
const waited = await p.text({
|
|
99
121
|
message: "Press enter when you've added it",
|
|
100
122
|
defaultValue: "",
|
|
101
123
|
placeholder: "",
|
|
102
124
|
});
|
|
125
|
+
if (p.isCancel(waited)) {
|
|
126
|
+
p.cancel("Cancelled.");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
103
129
|
if (!hasInflightWidget(cwd)) {
|
|
104
130
|
const skip = await p.confirm({
|
|
105
131
|
message: "Widget script not detected. Continue anyway?",
|
|
@@ -138,14 +164,14 @@ export async function setupCommand() {
|
|
|
138
164
|
cwd: gitRoot,
|
|
139
165
|
stdio: "pipe",
|
|
140
166
|
});
|
|
141
|
-
p.log.success("Changes committed.");
|
|
142
|
-
const spinner = p.spinner();
|
|
143
|
-
spinner.start("Pushing...");
|
|
144
167
|
execSync("git push", { cwd: gitRoot, stdio: "pipe" });
|
|
145
|
-
|
|
168
|
+
p.log.success("Pushed. When sharing, pick a deployment that includes the script tag.");
|
|
146
169
|
}
|
|
147
|
-
catch {
|
|
148
|
-
|
|
170
|
+
catch (err) {
|
|
171
|
+
const detail = execSyncErrorDetail(err);
|
|
172
|
+
p.log.warn(detail
|
|
173
|
+
? `Commit or push failed:\n${pc.dim(detail)}\n\nPush manually before sharing.`
|
|
174
|
+
: "Commit or push failed. Push manually before sharing.");
|
|
149
175
|
}
|
|
150
176
|
}
|
|
151
177
|
else {
|
package/dist/commands/share.js
CHANGED
|
@@ -7,6 +7,7 @@ import { providers } from "../providers/index.js";
|
|
|
7
7
|
import { apiGetMe, apiCreateVersion } from "../lib/api.js";
|
|
8
8
|
export async function shareCommand(opts = {}) {
|
|
9
9
|
const cwd = process.cwd();
|
|
10
|
+
// ── Step 1: Auth ──
|
|
10
11
|
const auth = readGlobalAuth();
|
|
11
12
|
if (!auth) {
|
|
12
13
|
if (opts.json) {
|
|
@@ -48,34 +49,42 @@ export async function shareCommand(opts = {}) {
|
|
|
48
49
|
await open(stagingUrl);
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
|
-
// ──
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
// ── Step 2: Get workspaces ──
|
|
53
|
+
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
54
|
+
p.log.error(e.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
const workspaces = me.workspaces;
|
|
58
|
+
let workspaceId;
|
|
59
|
+
// Check if a workspace is already configured and still valid
|
|
60
|
+
const existingConfig = readWorkspaceConfig();
|
|
61
|
+
const existingWorkspace = existingConfig ? workspaces.find((w) => w.id === existingConfig.workspaceId) : null;
|
|
62
|
+
if (existingWorkspace) {
|
|
63
|
+
workspaceId = existingWorkspace.id;
|
|
64
|
+
}
|
|
65
|
+
else if (workspaces.length === 0) {
|
|
66
|
+
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
else if (me.workspaces.length === 1) {
|
|
70
|
+
workspaceId = me.workspaces[0].id;
|
|
71
|
+
p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const selected = await p.select({
|
|
75
|
+
message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
|
|
76
|
+
options: me.workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
58
77
|
});
|
|
59
|
-
if (
|
|
60
|
-
p.
|
|
61
|
-
process.exit(
|
|
62
|
-
}
|
|
63
|
-
else if (me.workspaces.length === 1) {
|
|
64
|
-
workspaceId = me.workspaces[0].id;
|
|
65
|
-
p.log.info(`Workspace: ${pc.bold(me.workspaces[0].name)}`);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
const selected = await p.select({
|
|
69
|
-
message: "Select a workspace " + pc.dim("(change anytime with inflight workspaces)"),
|
|
70
|
-
options: me.workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
71
|
-
});
|
|
72
|
-
if (p.isCancel(selected)) {
|
|
73
|
-
p.cancel("Cancelled.");
|
|
74
|
-
process.exit(0);
|
|
75
|
-
}
|
|
76
|
-
workspaceId = selected;
|
|
78
|
+
if (p.isCancel(selected)) {
|
|
79
|
+
p.cancel("Cancelled.");
|
|
80
|
+
process.exit(0);
|
|
77
81
|
}
|
|
78
|
-
|
|
82
|
+
workspaceId = selected;
|
|
83
|
+
}
|
|
84
|
+
writeWorkspaceConfig({ workspaceId });
|
|
85
|
+
if (!workspaceId) {
|
|
86
|
+
p.log.error("No workspace configured. Run " + pc.cyan("inflight workspace") + " or " + pc.cyan("inflight setup") + ".");
|
|
87
|
+
process.exit(1);
|
|
79
88
|
}
|
|
80
89
|
// Resolve staging URL
|
|
81
90
|
const providerChoice = await p.select({
|
|
@@ -118,7 +127,7 @@ export async function shareCommand(opts = {}) {
|
|
|
118
127
|
if (!stagingUrl.startsWith("http")) {
|
|
119
128
|
stagingUrl = `https://${stagingUrl}`;
|
|
120
129
|
}
|
|
121
|
-
|
|
130
|
+
await apiCreateVersion({
|
|
122
131
|
apiKey: auth.apiKey,
|
|
123
132
|
workspaceId,
|
|
124
133
|
stagingUrl,
|
package/dist/commands/vercel.js
CHANGED
|
@@ -1,128 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { ensureVercelCli, ensureVercelAuth, getVercelToken, getVercelTeams, getVercelProjects, getRecentDeployments, getBranchAliasUrl, } from "../lib/vercel.js";
|
|
5
|
-
import { pickVercelProject } from "../providers/vercel.js";
|
|
1
|
+
import { readVercelConfig } from "../lib/config.js";
|
|
2
|
+
import { getGitInfo, parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
3
|
+
import { getVercelToken, getRecentDeployments, getBranchAlias, readLocalVercelProject, fetchAllProjectsWithLinks, matchProjectsByRepo, } from "../lib/vercel.js";
|
|
6
4
|
// --- Action handlers ---
|
|
7
|
-
async function
|
|
8
|
-
// Fast path: non-interactive set (for agents)
|
|
9
|
-
if (opts.team && opts.project) {
|
|
10
|
-
const token = requireVercelToken();
|
|
11
|
-
const teams = await getVercelTeams(token);
|
|
12
|
-
const team = teams.find((t) => t.id === opts.team);
|
|
13
|
-
if (!team) {
|
|
14
|
-
const msg = `Team '${opts.team}' not found. Available: ${teams.map((t) => `${t.name} (${t.id})`).join(", ")}`;
|
|
15
|
-
if (opts.json) {
|
|
16
|
-
console.log(JSON.stringify({ error: "team_not_found", message: msg }));
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
p.log.error(msg);
|
|
20
|
-
}
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
const projects = await getVercelProjects(token, opts.team);
|
|
24
|
-
const project = projects.find((proj) => proj.id === opts.project);
|
|
25
|
-
if (!project) {
|
|
26
|
-
const msg = `Project '${opts.project}' not found on team '${team.name}'.`;
|
|
27
|
-
if (opts.json) {
|
|
28
|
-
console.log(JSON.stringify({ error: "project_not_found", message: msg }));
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
p.log.error(msg);
|
|
32
|
-
}
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
writeVercelConfig({ teamId: team.id, teamName: team.name, projectId: project.id, projectName: project.name });
|
|
36
|
-
if (opts.json) {
|
|
37
|
-
console.log(JSON.stringify({
|
|
38
|
-
saved: true,
|
|
39
|
-
teamId: team.id,
|
|
40
|
-
teamName: team.name,
|
|
41
|
-
projectId: project.id,
|
|
42
|
-
projectName: project.name,
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
p.log.success(`Saved! Using ${pc.bold(project.name)} on ${pc.bold(team.name)}.`);
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Interactive path
|
|
51
|
-
const cliOk = await ensureVercelCli((msg) => p.log.step(msg));
|
|
52
|
-
if (!cliOk) {
|
|
53
|
-
p.log.error("Failed to install Vercel CLI. Install manually: " + pc.cyan("npm install -g vercel"));
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
p.log.step("Checking Vercel authentication...");
|
|
57
|
-
const token = await ensureVercelAuth();
|
|
58
|
-
if (!token) {
|
|
59
|
-
p.log.error("Vercel login failed.");
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
const current = readVercelConfig();
|
|
63
|
-
if (current) {
|
|
64
|
-
p.log.info(`Current: ${pc.bold(current.projectName)} on ${pc.bold(current.teamName)}`);
|
|
65
|
-
}
|
|
66
|
-
const config = await pickVercelProject(token);
|
|
67
|
-
if (!config)
|
|
68
|
-
process.exit(1);
|
|
69
|
-
p.log.success(`Saved! ${pc.cyan("inflight share")} will now use ${pc.bold(config.projectName)} on ${pc.bold(config.teamName)}.`);
|
|
70
|
-
}
|
|
71
|
-
async function listTeams() {
|
|
72
|
-
const token = requireVercelToken();
|
|
73
|
-
const teams = await getVercelTeams(token);
|
|
74
|
-
console.log(JSON.stringify(teams));
|
|
75
|
-
}
|
|
76
|
-
async function listProjects(opts) {
|
|
5
|
+
async function listProjects() {
|
|
77
6
|
const token = requireVercelToken();
|
|
78
|
-
const projects = await
|
|
79
|
-
console.log(JSON.stringify(projects));
|
|
7
|
+
const projects = await fetchAllProjectsWithLinks(token);
|
|
8
|
+
console.log(JSON.stringify(projects.map((p) => ({ id: p.id, name: p.name, teamId: p.teamId, teamName: p.teamName }))));
|
|
80
9
|
}
|
|
81
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Resolves teamId + projectId for subcommands.
|
|
12
|
+
* Priority: explicit flags → .vercel/project.json → git remote match → saved global config → error
|
|
13
|
+
*/
|
|
14
|
+
async function resolveProject(opts) {
|
|
15
|
+
if (opts.team && opts.project) {
|
|
16
|
+
return { teamId: opts.team, projectId: opts.project };
|
|
17
|
+
}
|
|
82
18
|
const token = requireVercelToken();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
const gitRoot = getGitRoot(cwd);
|
|
21
|
+
if (gitRoot) {
|
|
22
|
+
const local = readLocalVercelProject(gitRoot);
|
|
23
|
+
if (local)
|
|
24
|
+
return { teamId: local.orgId, projectId: local.projectId };
|
|
25
|
+
}
|
|
26
|
+
const gitInfo = getGitInfo(cwd);
|
|
27
|
+
if (gitInfo.remoteUrl) {
|
|
28
|
+
const gitRepo = parseGitRepo(gitInfo.remoteUrl);
|
|
29
|
+
if (gitRepo) {
|
|
30
|
+
try {
|
|
31
|
+
const allProjects = await fetchAllProjectsWithLinks(token);
|
|
32
|
+
const matches = matchProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
|
|
33
|
+
if (matches.length === 1) {
|
|
34
|
+
return { teamId: matches[0].teamId, projectId: matches[0].id };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
93
38
|
}
|
|
94
|
-
teamId = teamId ?? config.teamId;
|
|
95
|
-
projectId = projectId ?? config.projectId;
|
|
96
39
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
40
|
+
const config = readVercelConfig();
|
|
41
|
+
if (config)
|
|
42
|
+
return { teamId: config.teamId, projectId: config.projectId };
|
|
43
|
+
console.log(JSON.stringify({
|
|
44
|
+
error: "vercel_not_configured",
|
|
45
|
+
message: "Could not auto-detect Vercel project. Run 'inflight vercel projects' to list available projects, then pass --team and --project to 'inflight vercel deployments'.",
|
|
46
|
+
}));
|
|
47
|
+
process.exit(1);
|
|
102
48
|
}
|
|
103
|
-
async function
|
|
49
|
+
async function listDeployments(opts) {
|
|
104
50
|
const token = requireVercelToken();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (url) {
|
|
121
|
-
console.log(JSON.stringify({ url, branch: opts.branch }));
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
console.log(JSON.stringify({ url: null, branch: opts.branch, message: "No deployment found for this branch." }));
|
|
125
|
-
}
|
|
51
|
+
const { teamId, projectId } = await resolveProject(opts);
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
const gitInfo = getGitInfo(cwd);
|
|
54
|
+
const currentBranch = gitInfo.branch;
|
|
55
|
+
const [deployments, branchAlias] = await Promise.all([
|
|
56
|
+
getRecentDeployments(token, teamId, projectId, {
|
|
57
|
+
limit: parseInt(opts.limit ?? "10"),
|
|
58
|
+
branch: opts.branch,
|
|
59
|
+
}),
|
|
60
|
+
currentBranch ? getBranchAlias(token, teamId, projectId, currentBranch) : Promise.resolve(null),
|
|
61
|
+
]);
|
|
62
|
+
console.log(JSON.stringify({
|
|
63
|
+
branchAlias: branchAlias ? { url: branchAlias.url, state: branchAlias.state, branch: currentBranch } : null,
|
|
64
|
+
deployments,
|
|
65
|
+
}));
|
|
126
66
|
}
|
|
127
67
|
/** Gets token silently, exits with JSON error if unavailable. */
|
|
128
68
|
function requireVercelToken() {
|
|
@@ -130,7 +70,7 @@ function requireVercelToken() {
|
|
|
130
70
|
if (!token) {
|
|
131
71
|
console.log(JSON.stringify({
|
|
132
72
|
error: "vercel_not_authenticated",
|
|
133
|
-
message: "Vercel auth expired or missing. Run '
|
|
73
|
+
message: "Vercel auth expired or missing. Run 'vercel login' first.",
|
|
134
74
|
}));
|
|
135
75
|
process.exit(1);
|
|
136
76
|
}
|
|
@@ -138,33 +78,14 @@ function requireVercelToken() {
|
|
|
138
78
|
}
|
|
139
79
|
// --- Command registration ---
|
|
140
80
|
export function registerVercelCommand(program) {
|
|
141
|
-
const vercel = program
|
|
142
|
-
|
|
143
|
-
.description("Set up or change your Vercel project")
|
|
144
|
-
.passThroughOptions()
|
|
145
|
-
.option("--team <id>", "Vercel team ID (non-interactive)")
|
|
146
|
-
.option("--project <id>", "Vercel project ID (non-interactive)")
|
|
147
|
-
.option("--json", "Output as JSON (non-interactive)")
|
|
148
|
-
.action(vercelSetup);
|
|
149
|
-
vercel.command("teams").description("List Vercel teams (JSON)").action(listTeams);
|
|
150
|
-
vercel
|
|
151
|
-
.command("projects")
|
|
152
|
-
.description("List projects for a Vercel team (JSON)")
|
|
153
|
-
.requiredOption("--team <id>", "Vercel team ID")
|
|
154
|
-
.action(listProjects);
|
|
155
|
-
vercel
|
|
156
|
-
.command("branch-url")
|
|
157
|
-
.description("Get the stable branch preview URL (JSON)")
|
|
158
|
-
.requiredOption("--branch <name>", "Git branch name")
|
|
159
|
-
.option("--team <id>", "Vercel team ID (reads from saved config if omitted)")
|
|
160
|
-
.option("--project <id>", "Vercel project ID (reads from saved config if omitted)")
|
|
161
|
-
.action(branchUrl);
|
|
81
|
+
const vercel = program.command("vercel").description("Vercel integration commands");
|
|
82
|
+
vercel.command("projects").description("List all Vercel projects (JSON)").action(listProjects);
|
|
162
83
|
vercel
|
|
163
84
|
.command("deployments")
|
|
164
|
-
.description("List recent deployments (JSON)")
|
|
165
|
-
.option("--team <id>", "Vercel team ID (
|
|
166
|
-
.option("--project <id>", "Vercel project ID (
|
|
167
|
-
.option("--branch <name>", "Filter by
|
|
85
|
+
.description("List recent deployments + branch alias (JSON)")
|
|
86
|
+
.option("--team <id>", "Vercel team ID (auto-detected if omitted)")
|
|
87
|
+
.option("--project <id>", "Vercel project ID (auto-detected if omitted)")
|
|
88
|
+
.option("--branch <name>", "Filter deployments by branch")
|
|
168
89
|
.option("--limit <n>", "Number of deployments", "10")
|
|
169
90
|
.action(listDeployments);
|
|
170
91
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,11 @@ import pkg from "../package.json" with { type: "json" };
|
|
|
12
12
|
const { version } = pkg;
|
|
13
13
|
updateNotifier({ pkg }).notify();
|
|
14
14
|
const program = new Command();
|
|
15
|
-
program
|
|
15
|
+
program
|
|
16
|
+
.name("inflight")
|
|
17
|
+
.description("Get feedback directly on your staging URL")
|
|
18
|
+
.version(version)
|
|
19
|
+
.enablePositionalOptions();
|
|
16
20
|
program.command("setup").description("Set up Inflight in your project").action(setupCommand);
|
|
17
21
|
program.command("login").description("Authenticate with your Inflight account").action(loginCommand);
|
|
18
22
|
program
|
|
@@ -22,16 +26,9 @@ program
|
|
|
22
26
|
.option("--workspace <id>", "Workspace ID (skips workspace selection)")
|
|
23
27
|
.option("--json", "Output result as JSON")
|
|
24
28
|
.action((opts) => shareCommand(opts));
|
|
25
|
-
// program
|
|
26
|
-
// .command("preview")
|
|
27
|
-
// .description("Preview a live component from your code")
|
|
28
|
-
// .option("-m, --message <message>", "Pre-fill the intent prompt")
|
|
29
|
-
// .option("--scope <mode>", "Skip scope prompt: branch, uncommitted, staged")
|
|
30
|
-
// .option("--no-open", "Don't open result in browser")
|
|
31
|
-
// .action((opts) => previewCommand(opts));
|
|
32
29
|
program
|
|
33
|
-
.command("
|
|
34
|
-
.description("
|
|
30
|
+
.command("workspace")
|
|
31
|
+
.description("Get or set your active workspace")
|
|
35
32
|
.option("--json", "Output as JSON")
|
|
36
33
|
.option("--set <id>", "Set the active workspace")
|
|
37
34
|
.action((opts) => workspacesCommand(opts));
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface GitDiffResult {
|
|
|
20
20
|
}
|
|
21
21
|
export declare function getDefaultBranch(cwd: string): string;
|
|
22
22
|
export declare function getRemoteUrl(cwd: string): string | null;
|
|
23
|
+
export interface GitRepo {
|
|
24
|
+
owner: string;
|
|
25
|
+
name: string;
|
|
26
|
+
provider: "github" | "gitlab" | "bitbucket" | "unknown";
|
|
27
|
+
}
|
|
28
|
+
export declare function parseGitRepo(remoteUrl: string): GitRepo | null;
|
|
23
29
|
/**
|
|
24
30
|
* Get a structured diff result for the share API, supporting multiple scope modes.
|
|
25
31
|
*/
|
|
@@ -34,3 +40,5 @@ export declare function parseDiffStat(diffStat: string): Array<{
|
|
|
34
40
|
}>;
|
|
35
41
|
export declare function getGitInfo(cwd: string): GitInfo;
|
|
36
42
|
export declare function isGitRepo(cwd: string): boolean;
|
|
43
|
+
/** Returns the root directory of the git repo, or null if not in one. */
|
|
44
|
+
export declare function getGitRoot(cwd: string): string | null;
|
package/dist/lib/git.js
CHANGED
|
@@ -85,6 +85,38 @@ export function getDefaultBranch(cwd) {
|
|
|
85
85
|
export function getRemoteUrl(cwd) {
|
|
86
86
|
return run("git remote get-url origin", cwd);
|
|
87
87
|
}
|
|
88
|
+
export function parseGitRepo(remoteUrl) {
|
|
89
|
+
let host = null;
|
|
90
|
+
let path = null;
|
|
91
|
+
const sshMatch = remoteUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
92
|
+
if (sshMatch) {
|
|
93
|
+
host = sshMatch[1];
|
|
94
|
+
path = sshMatch[2];
|
|
95
|
+
}
|
|
96
|
+
if (!path) {
|
|
97
|
+
try {
|
|
98
|
+
const url = new URL(remoteUrl);
|
|
99
|
+
host = url.hostname;
|
|
100
|
+
path = url.pathname.replace(/^\//, "").replace(/\.git$/, "");
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}
|
|
104
|
+
if (!host || !path)
|
|
105
|
+
return null;
|
|
106
|
+
const parts = path.split("/");
|
|
107
|
+
if (parts.length < 2)
|
|
108
|
+
return null;
|
|
109
|
+
const owner = parts[0];
|
|
110
|
+
const name = parts[1];
|
|
111
|
+
let provider = "unknown";
|
|
112
|
+
if (host.includes("github"))
|
|
113
|
+
provider = "github";
|
|
114
|
+
else if (host.includes("gitlab"))
|
|
115
|
+
provider = "gitlab";
|
|
116
|
+
else if (host.includes("bitbucket"))
|
|
117
|
+
provider = "bitbucket";
|
|
118
|
+
return { owner, name, provider };
|
|
119
|
+
}
|
|
88
120
|
/**
|
|
89
121
|
* Get a structured diff result for the share API, supporting multiple scope modes.
|
|
90
122
|
*/
|
|
@@ -215,3 +247,7 @@ export function getGitInfo(cwd) {
|
|
|
215
247
|
export function isGitRepo(cwd) {
|
|
216
248
|
return run("git rev-parse --git-dir", cwd) !== null;
|
|
217
249
|
}
|
|
250
|
+
/** Returns the root directory of the git repo, or null if not in one. */
|
|
251
|
+
export function getGitRoot(cwd) {
|
|
252
|
+
return run("git rev-parse --show-toplevel", cwd);
|
|
253
|
+
}
|
package/dist/lib/vercel.d.ts
CHANGED
|
@@ -11,6 +11,19 @@ export declare function getVercelToken(): string | null;
|
|
|
11
11
|
* For interactive commands only.
|
|
12
12
|
*/
|
|
13
13
|
export declare function ensureVercelAuth(): Promise<string | null>;
|
|
14
|
+
interface LocalVercelProject {
|
|
15
|
+
orgId: string;
|
|
16
|
+
projectId: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reads `.vercel/project.json` from the given root directory.
|
|
20
|
+
* Created by `vercel link` or `vercel deploy`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readLocalVercelProject(root: string): LocalVercelProject | null;
|
|
23
|
+
/**
|
|
24
|
+
* Writes `.vercel/project.json` at the given root directory and ensures `.vercel` is gitignored.
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeLocalVercelProject(root: string, orgId: string, projectId: string): void;
|
|
14
27
|
export interface VercelTeam {
|
|
15
28
|
id: string;
|
|
16
29
|
name: string;
|
|
@@ -22,6 +35,7 @@ export interface VercelProject {
|
|
|
22
35
|
}
|
|
23
36
|
export interface VercelDeployment {
|
|
24
37
|
url: string;
|
|
38
|
+
state: string;
|
|
25
39
|
branch: string | null;
|
|
26
40
|
commitSha: string | null;
|
|
27
41
|
commitMessage: string | null;
|
|
@@ -29,11 +43,45 @@ export interface VercelDeployment {
|
|
|
29
43
|
}
|
|
30
44
|
export declare function getVercelTeams(token: string): Promise<VercelTeam[]>;
|
|
31
45
|
export declare function getVercelProjects(token: string, teamId: string): Promise<VercelProject[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Fetches details for a single Vercel project by ID.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getVercelProjectDetail(token: string, projectId: string, teamId: string): Promise<VercelProject | null>;
|
|
50
|
+
export interface VercelProjectWithLink {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
teamId: string;
|
|
54
|
+
teamName: string;
|
|
55
|
+
link?: {
|
|
56
|
+
org?: string;
|
|
57
|
+
repo?: string;
|
|
58
|
+
repoId?: number;
|
|
59
|
+
type?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Fetches all projects across all teams with their link info.
|
|
64
|
+
* Used for client-side repo matching.
|
|
65
|
+
*/
|
|
66
|
+
export declare function fetchAllProjectsWithLinks(token: string): Promise<VercelProjectWithLink[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Matches Vercel projects against a git remote's owner/repo.
|
|
69
|
+
* Exact match on link.org and link.repo.
|
|
70
|
+
*/
|
|
71
|
+
export declare function matchProjectsByRepo(projects: VercelProjectWithLink[], gitOwner: string, gitRepo: string): VercelProjectWithLink[];
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new Vercel project linked to a git repository.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createVercelProject(token: string, teamId: string, name: string, repo: string, repoType: "github" | "gitlab" | "bitbucket"): Promise<VercelProject>;
|
|
32
76
|
/**
|
|
33
77
|
* Fetches the branch alias URL (stable, auto-updates with each push).
|
|
34
78
|
* Returns null if no deployment exists for this branch.
|
|
35
79
|
*/
|
|
36
|
-
export
|
|
80
|
+
export interface BranchAlias {
|
|
81
|
+
url: string;
|
|
82
|
+
state: string;
|
|
83
|
+
}
|
|
84
|
+
export declare function getBranchAlias(token: string, teamId: string, projectId: string, branch: string | null): Promise<BranchAlias | null>;
|
|
37
85
|
/**
|
|
38
86
|
* Fetches recent deployments for a project.
|
|
39
87
|
*/
|
|
@@ -41,3 +89,4 @@ export declare function getRecentDeployments(token: string, teamId: string, proj
|
|
|
41
89
|
limit?: number;
|
|
42
90
|
branch?: string;
|
|
43
91
|
}): Promise<VercelDeployment[]>;
|
|
92
|
+
export {};
|
package/dist/lib/vercel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync, spawn, exec } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir, platform } from "os";
|
|
5
5
|
import { promisify } from "util";
|
|
@@ -105,6 +105,42 @@ export async function ensureVercelAuth() {
|
|
|
105
105
|
const auth = readVercelAuth();
|
|
106
106
|
return auth?.token ?? null;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Reads `.vercel/project.json` from the given root directory.
|
|
110
|
+
* Created by `vercel link` or `vercel deploy`.
|
|
111
|
+
*/
|
|
112
|
+
export function readLocalVercelProject(root) {
|
|
113
|
+
const projectPath = join(root, ".vercel", "project.json");
|
|
114
|
+
if (!existsSync(projectPath))
|
|
115
|
+
return null;
|
|
116
|
+
try {
|
|
117
|
+
const data = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
118
|
+
if (typeof data.orgId === "string" && typeof data.projectId === "string") {
|
|
119
|
+
return { orgId: data.orgId, projectId: data.projectId };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Writes `.vercel/project.json` at the given root directory and ensures `.vercel` is gitignored.
|
|
127
|
+
*/
|
|
128
|
+
export function writeLocalVercelProject(root, orgId, projectId) {
|
|
129
|
+
const vercelDir = join(root, ".vercel");
|
|
130
|
+
mkdirSync(vercelDir, { recursive: true });
|
|
131
|
+
writeFileSync(join(vercelDir, "project.json"), JSON.stringify({ orgId, projectId }, null, 2) + "\n");
|
|
132
|
+
// Ensure .vercel is in .gitignore
|
|
133
|
+
const gitignorePath = join(root, ".gitignore");
|
|
134
|
+
if (existsSync(gitignorePath)) {
|
|
135
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
136
|
+
if (!content.split("\n").some((line) => line.trim() === ".vercel")) {
|
|
137
|
+
appendFileSync(gitignorePath, "\n.vercel\n");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
writeFileSync(gitignorePath, ".vercel\n");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
108
144
|
// --- API calls (all take token + IDs explicitly) ---
|
|
109
145
|
export async function getVercelTeams(token) {
|
|
110
146
|
const res = await fetch("https://api.vercel.com/v2/teams", {
|
|
@@ -126,17 +162,78 @@ export async function getVercelProjects(token, teamId) {
|
|
|
126
162
|
return data.projects;
|
|
127
163
|
}
|
|
128
164
|
/**
|
|
129
|
-
* Fetches
|
|
130
|
-
|
|
165
|
+
* Fetches details for a single Vercel project by ID.
|
|
166
|
+
*/
|
|
167
|
+
export async function getVercelProjectDetail(token, projectId, teamId) {
|
|
168
|
+
const params = new URLSearchParams({ teamId });
|
|
169
|
+
const res = await fetch(`https://api.vercel.com/v10/projects/${encodeURIComponent(projectId)}?${params}`, {
|
|
170
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok)
|
|
173
|
+
return null;
|
|
174
|
+
const data = (await res.json());
|
|
175
|
+
return { id: data.id, name: data.name };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Fetches all projects across all teams with their link info.
|
|
179
|
+
* Used for client-side repo matching.
|
|
180
|
+
*/
|
|
181
|
+
export async function fetchAllProjectsWithLinks(token) {
|
|
182
|
+
const teams = await getVercelTeams(token);
|
|
183
|
+
const all = [];
|
|
184
|
+
const fetches = teams.map(async (team) => {
|
|
185
|
+
const params = new URLSearchParams({ teamId: team.id, limit: "100" });
|
|
186
|
+
const res = await fetch(`https://api.vercel.com/v10/projects?${params}`, {
|
|
187
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
188
|
+
});
|
|
189
|
+
if (!res.ok)
|
|
190
|
+
return;
|
|
191
|
+
const data = (await res.json());
|
|
192
|
+
for (const p of data.projects) {
|
|
193
|
+
all.push({ ...p, teamId: team.id, teamName: team.name });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
await Promise.all(fetches);
|
|
197
|
+
return all;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Matches Vercel projects against a git remote's owner/repo.
|
|
201
|
+
* Exact match on link.org and link.repo.
|
|
131
202
|
*/
|
|
132
|
-
export
|
|
203
|
+
export function matchProjectsByRepo(projects, gitOwner, gitRepo) {
|
|
204
|
+
const linked = projects.filter((p) => p.link?.org && p.link?.repo);
|
|
205
|
+
return linked.filter((p) => p.link?.org === gitOwner && p.link?.repo === gitRepo);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Creates a new Vercel project linked to a git repository.
|
|
209
|
+
*/
|
|
210
|
+
export async function createVercelProject(token, teamId, name, repo, repoType) {
|
|
211
|
+
const params = new URLSearchParams({ teamId });
|
|
212
|
+
const res = await fetch(`https://api.vercel.com/v11/projects${params ? `?${params}` : ""}`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Bearer ${token}`,
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
},
|
|
218
|
+
body: JSON.stringify({
|
|
219
|
+
name,
|
|
220
|
+
gitRepository: { type: repoType, repo },
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
const body = (await res.json().catch(() => null));
|
|
225
|
+
throw new Error(body?.error?.message ?? `Failed to create project (${res.status})`);
|
|
226
|
+
}
|
|
227
|
+
const data = (await res.json());
|
|
228
|
+
return { id: data.id, name: data.name };
|
|
229
|
+
}
|
|
230
|
+
export async function getBranchAlias(token, teamId, projectId, branch) {
|
|
133
231
|
if (!branch)
|
|
134
232
|
return null;
|
|
135
233
|
const params = new URLSearchParams({
|
|
136
234
|
projectId,
|
|
137
235
|
teamId,
|
|
138
236
|
limit: "1",
|
|
139
|
-
state: "READY",
|
|
140
237
|
branch,
|
|
141
238
|
});
|
|
142
239
|
const res = await fetch(`https://api.vercel.com/v6/deployments?${params}`, {
|
|
@@ -156,7 +253,10 @@ export async function getBranchAliasUrl(token, teamId, projectId, branch) {
|
|
|
156
253
|
if (!aliasRes.ok)
|
|
157
254
|
return null;
|
|
158
255
|
const aliasData = (await aliasRes.json());
|
|
159
|
-
|
|
256
|
+
const url = aliasData.automaticAliases?.[0];
|
|
257
|
+
if (!url)
|
|
258
|
+
return null;
|
|
259
|
+
return { url, state: deploy.state };
|
|
160
260
|
}
|
|
161
261
|
/**
|
|
162
262
|
* Fetches recent deployments for a project.
|
|
@@ -166,7 +266,6 @@ export async function getRecentDeployments(token, teamId, projectId, opts) {
|
|
|
166
266
|
projectId,
|
|
167
267
|
teamId,
|
|
168
268
|
limit: String(opts?.limit ?? 10),
|
|
169
|
-
state: "READY",
|
|
170
269
|
});
|
|
171
270
|
if (opts?.branch) {
|
|
172
271
|
params.set("branch", opts.branch);
|
|
@@ -179,6 +278,7 @@ export async function getRecentDeployments(token, teamId, projectId, opts) {
|
|
|
179
278
|
const data = (await res.json());
|
|
180
279
|
return data.deployments.map((d) => ({
|
|
181
280
|
url: d.url,
|
|
281
|
+
state: d.state,
|
|
182
282
|
branch: d.meta?.githubCommitRef ?? d.meta?.gitlabCommitRef ?? d.meta?.bitbucketCommitRef ?? null,
|
|
183
283
|
commitSha: (d.meta?.githubCommitSha ?? d.meta?.gitlabCommitSha ?? d.meta?.bitbucketCommitSha ?? null)?.slice(0, 7) ??
|
|
184
284
|
null,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { GitInfo } from "../lib/git.js";
|
|
2
2
|
import type { VercelConfig } from "../lib/config.js";
|
|
3
3
|
/**
|
|
4
|
-
* Interactive
|
|
5
|
-
*
|
|
4
|
+
* Interactive project picker. Saves selection to global config.
|
|
5
|
+
* Used by `inflight vercel` command for explicit manual override.
|
|
6
6
|
*/
|
|
7
7
|
export declare function pickVercelProject(token: string): Promise<VercelConfig | null>;
|
|
8
8
|
export declare function resolveVercelUrl(cwd: string, gitInfo: GitInfo): Promise<string | null>;
|
package/dist/providers/vercel.js
CHANGED
|
@@ -1,83 +1,189 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
4
|
+
import { writeVercelConfig } from "../lib/config.js";
|
|
5
|
+
import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectDetail, fetchAllProjectsWithLinks, matchProjectsByRepo, createVercelProject, getBranchAlias, getRecentDeployments, } from "../lib/vercel.js";
|
|
6
|
+
// --- Auto-detection ---
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* Auto-detect the Vercel project for the current git repo.
|
|
9
|
+
* Returns null only if user cancels or no projects exist at all.
|
|
10
|
+
*
|
|
11
|
+
* 1. `.vercel/project.json` at git root (instant, no API)
|
|
12
|
+
* 2. Fetch all projects, exact match on git remote
|
|
13
|
+
* 3. Multiple matches → pick from matches
|
|
14
|
+
* 4. Zero matches → pick from all projects
|
|
8
15
|
*/
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
async function autoDetectProject(cwd, gitInfo, token) {
|
|
17
|
+
const gitRoot = getGitRoot(cwd);
|
|
18
|
+
// Cache result to .vercel/project.json so subsequent runs skip API calls
|
|
19
|
+
const cacheResult = (project) => {
|
|
20
|
+
if (gitRoot) {
|
|
21
|
+
writeLocalVercelProject(gitRoot, project.teamId, project.projectId);
|
|
22
|
+
}
|
|
23
|
+
return project;
|
|
24
|
+
};
|
|
25
|
+
// --- Fast path: .vercel/project.json ---
|
|
26
|
+
if (gitRoot) {
|
|
27
|
+
const localProject = readLocalVercelProject(gitRoot);
|
|
28
|
+
if (localProject) {
|
|
29
|
+
const detail = await getVercelProjectDetail(token, localProject.projectId, localProject.orgId);
|
|
30
|
+
if (detail) {
|
|
31
|
+
p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
|
|
32
|
+
return { teamId: localProject.orgId, projectId: detail.id, projectName: detail.name };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// --- Fetch all projects ---
|
|
37
|
+
const spinner = p.spinner();
|
|
38
|
+
spinner.start("Detecting Vercel project...");
|
|
39
|
+
let allProjects;
|
|
40
|
+
try {
|
|
41
|
+
allProjects = await fetchAllProjectsWithLinks(token);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
spinner.stop("Could not fetch Vercel projects.");
|
|
13
45
|
return null;
|
|
14
46
|
}
|
|
47
|
+
if (allProjects.length === 0) {
|
|
48
|
+
spinner.stop("No Vercel projects found.");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// --- Try exact match on git remote ---
|
|
52
|
+
const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
|
|
53
|
+
if (gitRepo) {
|
|
54
|
+
const matches = matchProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
|
|
55
|
+
if (matches.length === 1) {
|
|
56
|
+
spinner.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
|
|
57
|
+
return cacheResult({
|
|
58
|
+
teamId: matches[0].teamId,
|
|
59
|
+
projectId: matches[0].id,
|
|
60
|
+
projectName: matches[0].name,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (matches.length > 1) {
|
|
64
|
+
spinner.stop(`Found ${matches.length} Vercel projects for this repo.`);
|
|
65
|
+
const picked = await pickFromList(allProjects);
|
|
66
|
+
return picked ? cacheResult(picked) : null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// --- No match ---
|
|
70
|
+
spinner.stop("Could not auto-detect Vercel project.");
|
|
71
|
+
const canCreate = gitRepo && gitRepo.provider !== "unknown";
|
|
72
|
+
const picked = await pickFromList(allProjects, canCreate ? { token, gitRepo } : undefined);
|
|
73
|
+
return picked ? cacheResult(picked) : null;
|
|
74
|
+
}
|
|
75
|
+
async function pickFromList(projects, createCtx) {
|
|
76
|
+
const maxName = Math.max(...projects.map((proj) => proj.name.length));
|
|
77
|
+
const selected = await p.select({
|
|
78
|
+
message: "Select a Vercel project",
|
|
79
|
+
options: [
|
|
80
|
+
...projects.map((proj) => ({
|
|
81
|
+
value: proj,
|
|
82
|
+
label: proj.teamName ? `${proj.name.padEnd(maxName)} ${pc.dim(`(${proj.teamName})`)}` : proj.name,
|
|
83
|
+
})),
|
|
84
|
+
...(createCtx
|
|
85
|
+
? [{ value: "create", label: pc.dim("Create new Vercel project") }]
|
|
86
|
+
: []),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
if (p.isCancel(selected)) {
|
|
90
|
+
p.cancel("Cancelled.");
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
if (selected === "create" && createCtx) {
|
|
94
|
+
return createProjectFlow(projects, createCtx);
|
|
95
|
+
}
|
|
96
|
+
const match = selected;
|
|
97
|
+
return { teamId: match.teamId, projectId: match.id, projectName: match.name };
|
|
98
|
+
}
|
|
99
|
+
async function createProjectFlow(projects, ctx) {
|
|
100
|
+
const { token, gitRepo } = ctx;
|
|
101
|
+
// Pick team from unique teams
|
|
102
|
+
const teams = [...new Map(projects.map((proj) => [proj.teamId, proj.teamName])).entries()];
|
|
15
103
|
let teamId;
|
|
16
|
-
let teamName;
|
|
17
104
|
if (teams.length === 1) {
|
|
18
|
-
teamId = teams[0]
|
|
19
|
-
teamName = teams[0].name;
|
|
105
|
+
teamId = teams[0][0];
|
|
20
106
|
}
|
|
21
107
|
else {
|
|
22
|
-
const
|
|
23
|
-
message: "
|
|
24
|
-
options: teams.map((
|
|
108
|
+
const selectedTeam = await p.select({
|
|
109
|
+
message: "Which Vercel team?",
|
|
110
|
+
options: teams.map(([id, name]) => ({ value: id, label: name })),
|
|
25
111
|
});
|
|
26
|
-
if (p.isCancel(
|
|
112
|
+
if (p.isCancel(selectedTeam)) {
|
|
27
113
|
p.cancel("Cancelled.");
|
|
28
|
-
|
|
114
|
+
process.exit(0);
|
|
29
115
|
}
|
|
30
|
-
teamId =
|
|
31
|
-
|
|
116
|
+
teamId = selectedTeam;
|
|
117
|
+
}
|
|
118
|
+
const spinner = p.spinner();
|
|
119
|
+
spinner.start("Creating Vercel project...");
|
|
120
|
+
try {
|
|
121
|
+
const created = await createVercelProject(token, teamId, gitRepo.name, `${gitRepo.owner}/${gitRepo.name}`, gitRepo.provider);
|
|
122
|
+
spinner.stop(`Created ${pc.bold(created.name)}`);
|
|
123
|
+
return { teamId, projectId: created.id, projectName: created.name };
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
spinner.stop(e.message);
|
|
127
|
+
return null;
|
|
32
128
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
129
|
+
}
|
|
130
|
+
// --- Manual picker (used by `inflight vercel` command) ---
|
|
131
|
+
/**
|
|
132
|
+
* Interactive project picker. Saves selection to global config.
|
|
133
|
+
* Used by `inflight vercel` command for explicit manual override.
|
|
134
|
+
*/
|
|
135
|
+
export async function pickVercelProject(token) {
|
|
136
|
+
const allProjects = await fetchAllProjectsWithLinks(token);
|
|
137
|
+
if (allProjects.length === 0) {
|
|
138
|
+
p.log.error("No Vercel projects found.");
|
|
36
139
|
return null;
|
|
37
140
|
}
|
|
38
|
-
const
|
|
141
|
+
const selected = await p.select({
|
|
39
142
|
message: "Select a Vercel project",
|
|
40
|
-
options:
|
|
143
|
+
options: allProjects.map((proj) => ({
|
|
144
|
+
value: proj,
|
|
145
|
+
label: proj.teamName ? `${proj.name} ${pc.dim(`(${proj.teamName})`)}` : proj.name,
|
|
146
|
+
})),
|
|
41
147
|
});
|
|
42
|
-
if (p.isCancel(
|
|
148
|
+
if (p.isCancel(selected)) {
|
|
43
149
|
p.cancel("Cancelled.");
|
|
44
150
|
return null;
|
|
45
151
|
}
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
152
|
+
const match = selected;
|
|
153
|
+
const config = {
|
|
154
|
+
teamId: match.teamId,
|
|
155
|
+
teamName: match.teamName,
|
|
156
|
+
projectId: match.id,
|
|
157
|
+
projectName: match.name,
|
|
158
|
+
};
|
|
49
159
|
writeVercelConfig(config);
|
|
50
160
|
return config;
|
|
51
161
|
}
|
|
162
|
+
// --- Main resolve function ---
|
|
52
163
|
export async function resolveVercelUrl(cwd, gitInfo) {
|
|
53
|
-
// Ensure Vercel CLI is installed (only logs if installing)
|
|
54
164
|
const cliOk = await ensureVercelCli((msg) => p.log.step(msg));
|
|
55
165
|
if (!cliOk) {
|
|
56
166
|
p.log.error("Failed to install Vercel CLI. Install manually: " + pc.cyan("npm install -g vercel"));
|
|
57
167
|
return null;
|
|
58
168
|
}
|
|
59
|
-
// Ensure auth
|
|
60
169
|
const token = await ensureVercelAuth();
|
|
61
170
|
if (!token) {
|
|
62
171
|
p.log.error("Vercel login failed.");
|
|
63
172
|
return null;
|
|
64
173
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
config = await pickVercelProject(token);
|
|
69
|
-
if (!config)
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
174
|
+
const project = await autoDetectProject(cwd, gitInfo, token);
|
|
175
|
+
if (!project)
|
|
176
|
+
return null;
|
|
72
177
|
// Fetch branch alias
|
|
73
|
-
const branchAlias = await
|
|
178
|
+
const branchAlias = await getBranchAlias(token, project.teamId, project.projectId, gitInfo.branch);
|
|
74
179
|
if (branchAlias) {
|
|
75
|
-
|
|
180
|
+
const stateLabel = branchAlias.state !== "READY" ? ` ${pc.yellow(`(${branchAlias.state.toLowerCase()})`)}` : "";
|
|
181
|
+
p.log.info(`Branch preview (auto-updates with each push):\n → ${pc.cyan(branchAlias.url)}${stateLabel}`);
|
|
76
182
|
const choice = await p.select({
|
|
77
|
-
message: "Use this URL, or pick a
|
|
183
|
+
message: "Use this URL, or pick a specific deployment?",
|
|
78
184
|
options: [
|
|
79
185
|
{ value: "branch", label: "Use branch preview (recommended)" },
|
|
80
|
-
{ value: "recent", label: "Pick
|
|
186
|
+
{ value: "recent", label: "Pick a specific deployment" },
|
|
81
187
|
{ value: "manual", label: "Paste a URL manually" },
|
|
82
188
|
],
|
|
83
189
|
});
|
|
@@ -86,26 +192,27 @@ export async function resolveVercelUrl(cwd, gitInfo) {
|
|
|
86
192
|
process.exit(0);
|
|
87
193
|
}
|
|
88
194
|
if (choice === "branch")
|
|
89
|
-
return branchAlias;
|
|
195
|
+
return branchAlias.url;
|
|
90
196
|
if (choice === "manual")
|
|
91
197
|
return null;
|
|
92
198
|
}
|
|
93
199
|
// Show recent deployments
|
|
94
|
-
const recent = await getRecentDeployments(token,
|
|
200
|
+
const recent = await getRecentDeployments(token, project.teamId, project.projectId);
|
|
95
201
|
if (recent.length === 0) {
|
|
96
202
|
p.log.warn("No deployments found. Paste a URL instead.");
|
|
97
203
|
return null;
|
|
98
204
|
}
|
|
99
205
|
const maxBranch = Math.max(...recent.map((d) => (d.branch ?? "unknown").length));
|
|
100
206
|
const selected = await p.select({
|
|
101
|
-
message: "Select a deployment",
|
|
207
|
+
message: "Select a specific deployment",
|
|
102
208
|
options: [
|
|
103
209
|
...recent.map((d) => {
|
|
104
210
|
const branch = (d.branch ?? "unknown").padEnd(maxBranch);
|
|
105
211
|
const ago = timeAgo(d.createdAt).padEnd(8);
|
|
212
|
+
const state = d.state !== "READY" ? ` ${pc.yellow(`(${d.state.toLowerCase()})`)}` : "";
|
|
106
213
|
const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
|
|
107
214
|
const message = truncate(firstLine, 55);
|
|
108
|
-
return { value: d.url, label: `${branch} ${ago} ${pc.dim(message)}` };
|
|
215
|
+
return { value: d.url, label: `${branch} ${ago}${state} ${pc.dim(message)}` };
|
|
109
216
|
}),
|
|
110
217
|
{ value: "manual", label: "Paste a URL manually" },
|
|
111
218
|
],
|