inflight-cli 2.0.8 → 2.1.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/setup.js +56 -23
- package/dist/commands/share.js +6 -2
- package/dist/commands/vercel.js +4 -4
- 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/dist/commands/setup.js
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
|
-
import { readGlobalAuth, writeWorkspaceConfig } from "../lib/config.js";
|
|
4
|
+
import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
|
|
5
5
|
import { apiGetMe, apiDetectWidgetLocation } from "../lib/api.js";
|
|
6
6
|
import { loginCommand } from "./login.js";
|
|
7
7
|
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 ──
|
|
@@ -36,7 +50,14 @@ export async function setupCommand() {
|
|
|
36
50
|
});
|
|
37
51
|
const workspaces = me.workspaces;
|
|
38
52
|
let workspaceId;
|
|
39
|
-
if
|
|
53
|
+
// Check if a workspace is already configured and still valid
|
|
54
|
+
const existingConfig = readWorkspaceConfig();
|
|
55
|
+
const existingWorkspace = existingConfig ? workspaces.find((w) => w.id === existingConfig.workspaceId) : null;
|
|
56
|
+
if (existingWorkspace) {
|
|
57
|
+
workspaceId = existingWorkspace.id;
|
|
58
|
+
p.log.success(`Workspace: ${pc.bold(existingWorkspace.name)} ${pc.dim("(change anytime with inflight workspaces)")}`);
|
|
59
|
+
}
|
|
60
|
+
else if (workspaces.length === 0) {
|
|
40
61
|
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
41
62
|
process.exit(1);
|
|
42
63
|
}
|
|
@@ -46,7 +67,7 @@ export async function setupCommand() {
|
|
|
46
67
|
}
|
|
47
68
|
else {
|
|
48
69
|
const selected = await p.select({
|
|
49
|
-
message: "Select a workspace " + pc.dim("(change anytime with inflight
|
|
70
|
+
message: "Select a workspace " + pc.dim("(change anytime with inflight workspaces)"),
|
|
50
71
|
options: workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
51
72
|
});
|
|
52
73
|
if (p.isCancel(selected)) {
|
|
@@ -68,31 +89,43 @@ export async function setupCommand() {
|
|
|
68
89
|
else {
|
|
69
90
|
const spinner = p.spinner();
|
|
70
91
|
spinner.start("Detecting framework...");
|
|
71
|
-
const context = gatherProjectContext(cwd);
|
|
72
|
-
const location = await apiDetectWidgetLocation({
|
|
73
|
-
apiKey: auth.apiKey,
|
|
74
|
-
fileTree: context.fileTree,
|
|
75
|
-
fileContents: context.fileContents,
|
|
76
|
-
});
|
|
77
92
|
let inserted = false;
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.");
|
|
83
112
|
}
|
|
84
113
|
}
|
|
85
|
-
|
|
86
|
-
spinner.stop("Could not
|
|
114
|
+
catch {
|
|
115
|
+
spinner.stop("Could not analyze your project.");
|
|
87
116
|
}
|
|
88
117
|
if (!inserted) {
|
|
89
118
|
p.log.message(`Add this snippet to your root HTML layout, just before ${pc.cyan("</body>")}:\n\n` +
|
|
90
119
|
pc.dim(` <script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`));
|
|
91
|
-
await p.text({
|
|
120
|
+
const waited = await p.text({
|
|
92
121
|
message: "Press enter when you've added it",
|
|
93
122
|
defaultValue: "",
|
|
94
123
|
placeholder: "",
|
|
95
124
|
});
|
|
125
|
+
if (p.isCancel(waited)) {
|
|
126
|
+
p.cancel("Cancelled.");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
96
129
|
if (!hasInflightWidget(cwd)) {
|
|
97
130
|
const skip = await p.confirm({
|
|
98
131
|
message: "Widget script not detected. Continue anyway?",
|
|
@@ -131,14 +164,14 @@ export async function setupCommand() {
|
|
|
131
164
|
cwd: gitRoot,
|
|
132
165
|
stdio: "pipe",
|
|
133
166
|
});
|
|
134
|
-
p.log.success("Changes committed.");
|
|
135
|
-
const spinner = p.spinner();
|
|
136
|
-
spinner.start("Pushing...");
|
|
137
167
|
execSync("git push", { cwd: gitRoot, stdio: "pipe" });
|
|
138
|
-
|
|
168
|
+
p.log.success("Pushed. When sharing, pick a deployment that includes the script tag.");
|
|
139
169
|
}
|
|
140
|
-
catch {
|
|
141
|
-
|
|
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.");
|
|
142
175
|
}
|
|
143
176
|
}
|
|
144
177
|
else {
|
package/dist/commands/share.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function shareCommand(opts = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
else {
|
|
68
68
|
const selected = await p.select({
|
|
69
|
-
message: "Select a workspace " + pc.dim("(change anytime with inflight
|
|
69
|
+
message: "Select a workspace " + pc.dim("(change anytime with inflight workspaces)"),
|
|
70
70
|
options: me.workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
71
71
|
});
|
|
72
72
|
if (p.isCancel(selected)) {
|
|
@@ -77,6 +77,10 @@ export async function shareCommand(opts = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
writeWorkspaceConfig({ workspaceId });
|
|
79
79
|
}
|
|
80
|
+
if (!workspaceId) {
|
|
81
|
+
p.log.error("No workspace configured. Run " + pc.cyan("inflight workspaces") + " or " + pc.cyan("inflight setup") + ".");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
80
84
|
// Resolve staging URL
|
|
81
85
|
const providerChoice = await p.select({
|
|
82
86
|
message: "Where is your staging URL hosted?",
|
|
@@ -118,7 +122,7 @@ export async function shareCommand(opts = {}) {
|
|
|
118
122
|
if (!stagingUrl.startsWith("http")) {
|
|
119
123
|
stagingUrl = `https://${stagingUrl}`;
|
|
120
124
|
}
|
|
121
|
-
|
|
125
|
+
await apiCreateVersion({
|
|
122
126
|
apiKey: auth.apiKey,
|
|
123
127
|
workspaceId,
|
|
124
128
|
stagingUrl,
|
package/dist/commands/vercel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { readVercelConfig, writeVercelConfig } from "../lib/config.js";
|
|
4
|
-
import { ensureVercelCli, ensureVercelAuth, getVercelToken, getVercelTeams, getVercelProjects, getRecentDeployments,
|
|
4
|
+
import { ensureVercelCli, ensureVercelAuth, getVercelToken, getVercelTeams, getVercelProjects, getRecentDeployments, getBranchAlias, } from "../lib/vercel.js";
|
|
5
5
|
import { pickVercelProject } from "../providers/vercel.js";
|
|
6
6
|
// --- Action handlers ---
|
|
7
7
|
async function vercelSetup(opts) {
|
|
@@ -116,9 +116,9 @@ async function branchUrl(opts) {
|
|
|
116
116
|
teamId = teamId ?? config.teamId;
|
|
117
117
|
projectId = projectId ?? config.projectId;
|
|
118
118
|
}
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
console.log(JSON.stringify({ url, branch: opts.branch }));
|
|
119
|
+
const alias = await getBranchAlias(token, teamId, projectId, opts.branch);
|
|
120
|
+
if (alias) {
|
|
121
|
+
console.log(JSON.stringify({ url: alias.url, state: alias.state, branch: opts.branch }));
|
|
122
122
|
}
|
|
123
123
|
else {
|
|
124
124
|
console.log(JSON.stringify({ url: null, branch: opts.branch, message: "No deployment found for this branch." }));
|
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
|
],
|