inflight-cli 2.2.0 → 2.3.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/netlify.d.ts +2 -0
- package/dist/commands/netlify.js +79 -0
- package/dist/commands/reset.js +2 -1
- package/dist/commands/share.js +2 -2
- package/dist/commands/vercel.js +5 -5
- package/dist/index.js +2 -0
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.js +21 -0
- package/dist/lib/netlify.d.ts +67 -0
- package/dist/lib/netlify.js +191 -0
- package/dist/lib/vercel.d.ts +12 -18
- package/dist/lib/vercel.js +7 -17
- package/dist/providers/index.js +2 -0
- package/dist/providers/netlify.d.ts +5 -0
- package/dist/providers/netlify.js +218 -0
- package/dist/providers/vercel.d.ts +0 -6
- package/dist/providers/vercel.js +55 -50
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readNetlifyConfig } from "../lib/config.js";
|
|
2
|
+
import { getGitInfo, parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
3
|
+
import { getNetlifyToken, getNetlifyDeploys, getNetlifySites, readLocalNetlifySite, matchSitesByRepo, } from "../lib/netlify.js";
|
|
4
|
+
// --- Action handlers ---
|
|
5
|
+
async function listSites() {
|
|
6
|
+
const token = requireNetlifyToken();
|
|
7
|
+
const sites = await getNetlifySites(token);
|
|
8
|
+
console.log(JSON.stringify(sites.map((s) => ({
|
|
9
|
+
id: s.id,
|
|
10
|
+
name: s.name,
|
|
11
|
+
accountSlug: s.account_slug,
|
|
12
|
+
accountName: s.account_name,
|
|
13
|
+
}))));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolves siteId for subcommands.
|
|
17
|
+
* Priority: explicit flag → .netlify/state.json → git remote match → saved global config → error
|
|
18
|
+
*/
|
|
19
|
+
async function resolveSite(opts) {
|
|
20
|
+
if (opts.site)
|
|
21
|
+
return opts.site;
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
const gitRoot = getGitRoot(cwd);
|
|
24
|
+
if (gitRoot) {
|
|
25
|
+
const local = readLocalNetlifySite(gitRoot);
|
|
26
|
+
if (local)
|
|
27
|
+
return local.siteId;
|
|
28
|
+
}
|
|
29
|
+
const token = requireNetlifyToken();
|
|
30
|
+
const gitInfo = getGitInfo(cwd);
|
|
31
|
+
if (gitInfo.remoteUrl) {
|
|
32
|
+
const gitRepo = parseGitRepo(gitInfo.remoteUrl);
|
|
33
|
+
if (gitRepo) {
|
|
34
|
+
try {
|
|
35
|
+
const allSites = await getNetlifySites(token);
|
|
36
|
+
const matches = matchSitesByRepo(allSites, gitRepo.owner, gitRepo.name);
|
|
37
|
+
if (matches.length === 1)
|
|
38
|
+
return matches[0].id;
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const config = readNetlifyConfig();
|
|
44
|
+
if (config)
|
|
45
|
+
return config.siteId;
|
|
46
|
+
console.log(JSON.stringify({
|
|
47
|
+
error: "netlify_not_configured",
|
|
48
|
+
message: "Could not auto-detect Netlify site. Run 'inflight netlify sites' to list available sites, then pass --site to 'inflight netlify deploys'.",
|
|
49
|
+
}));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
async function listDeploys(opts) {
|
|
53
|
+
const token = requireNetlifyToken();
|
|
54
|
+
const siteId = await resolveSite(opts);
|
|
55
|
+
const deploys = await getNetlifyDeploys(token, siteId);
|
|
56
|
+
console.log(JSON.stringify({ deploys }));
|
|
57
|
+
}
|
|
58
|
+
/** Gets token silently, exits with JSON error if unavailable. */
|
|
59
|
+
function requireNetlifyToken() {
|
|
60
|
+
const token = getNetlifyToken();
|
|
61
|
+
if (!token) {
|
|
62
|
+
console.log(JSON.stringify({
|
|
63
|
+
error: "netlify_not_authenticated",
|
|
64
|
+
message: "Netlify auth missing. Run 'netlify login' first, or set NETLIFY_AUTH_TOKEN.",
|
|
65
|
+
}));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
return token;
|
|
69
|
+
}
|
|
70
|
+
// --- Command registration ---
|
|
71
|
+
export function registerNetlifyCommand(program) {
|
|
72
|
+
const netlify = program.command("netlify").description("Netlify integration commands");
|
|
73
|
+
netlify.command("sites").description("List all Netlify sites (JSON)").action(listSites);
|
|
74
|
+
netlify
|
|
75
|
+
.command("deploys")
|
|
76
|
+
.description("List recent deploys for a site (JSON)")
|
|
77
|
+
.option("--site <id>", "Netlify site ID (auto-detected if omitted)")
|
|
78
|
+
.action(listDeploys);
|
|
79
|
+
}
|
package/dist/commands/reset.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
-
import { clearGlobalAuth, clearWorkspaceConfig, clearVercelConfig } from "../lib/config.js";
|
|
3
|
+
import { clearGlobalAuth, clearWorkspaceConfig, clearVercelConfig, clearNetlifyConfig } from "../lib/config.js";
|
|
4
4
|
export async function resetCommand() {
|
|
5
5
|
const confirm = await p.confirm({
|
|
6
6
|
message: "This will clear all Inflight auth and workspace config. Continue?",
|
|
@@ -13,5 +13,6 @@ export async function resetCommand() {
|
|
|
13
13
|
clearGlobalAuth();
|
|
14
14
|
clearWorkspaceConfig();
|
|
15
15
|
clearVercelConfig();
|
|
16
|
+
clearNetlifyConfig();
|
|
16
17
|
p.log.success(pc.green("All Inflight config cleared."));
|
|
17
18
|
}
|
package/dist/commands/share.js
CHANGED
|
@@ -328,14 +328,14 @@ export async function shareCommand(opts = {}) {
|
|
|
328
328
|
}));
|
|
329
329
|
if (recentProjects.length > 0) {
|
|
330
330
|
const projectChoice = await p.select({
|
|
331
|
-
message: "Add to an existing
|
|
331
|
+
message: "Add to an existing project or start a new one?",
|
|
332
332
|
options: [
|
|
333
|
+
{ value: "new", label: "Start a new project" },
|
|
333
334
|
...recentProjects.map((proj) => ({
|
|
334
335
|
value: proj.projectId,
|
|
335
336
|
label: `"${proj.latestVersion.title}"`,
|
|
336
337
|
hint: `created ${formatRelativeTime(proj.latestVersion.createdAt)}`,
|
|
337
338
|
})),
|
|
338
|
-
{ value: "new", label: "Start fresh" },
|
|
339
339
|
],
|
|
340
340
|
});
|
|
341
341
|
if (p.isCancel(projectChoice)) {
|
package/dist/commands/vercel.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readVercelConfig } from "../lib/config.js";
|
|
2
2
|
import { getGitInfo, parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
3
|
-
import { getVercelToken,
|
|
3
|
+
import { getVercelToken, getVercelDeployments, getBranchAlias, readLocalVercelProject, getVercelProjects, matchVercelProjectsByRepo, } from "../lib/vercel.js";
|
|
4
4
|
// --- Action handlers ---
|
|
5
5
|
async function listProjects() {
|
|
6
6
|
const token = requireVercelToken();
|
|
7
|
-
const projects = await
|
|
7
|
+
const projects = await getVercelProjects(token);
|
|
8
8
|
console.log(JSON.stringify(projects.map((p) => ({ id: p.id, name: p.name, teamId: p.teamId, teamName: p.teamName }))));
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
@@ -28,8 +28,8 @@ async function resolveProject(opts) {
|
|
|
28
28
|
const gitRepo = parseGitRepo(gitInfo.remoteUrl);
|
|
29
29
|
if (gitRepo) {
|
|
30
30
|
try {
|
|
31
|
-
const allProjects = await
|
|
32
|
-
const matches =
|
|
31
|
+
const allProjects = await getVercelProjects(token);
|
|
32
|
+
const matches = matchVercelProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
|
|
33
33
|
if (matches.length === 1) {
|
|
34
34
|
return { teamId: matches[0].teamId, projectId: matches[0].id };
|
|
35
35
|
}
|
|
@@ -53,7 +53,7 @@ async function listDeployments(opts) {
|
|
|
53
53
|
const gitInfo = getGitInfo(cwd);
|
|
54
54
|
const currentBranch = gitInfo.branch;
|
|
55
55
|
const [deployments, branchAlias] = await Promise.all([
|
|
56
|
-
|
|
56
|
+
getVercelDeployments(token, teamId, projectId),
|
|
57
57
|
currentBranch ? getBranchAlias(token, teamId, projectId, currentBranch) : Promise.resolve(null),
|
|
58
58
|
]);
|
|
59
59
|
console.log(JSON.stringify({
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { logoutCommand } from "./commands/logout.js";
|
|
|
7
7
|
import { resetCommand } from "./commands/reset.js";
|
|
8
8
|
import { workspaceCommand } from "./commands/workspace.js";
|
|
9
9
|
import { registerVercelCommand } from "./commands/vercel.js";
|
|
10
|
+
import { registerNetlifyCommand } from "./commands/netlify.js";
|
|
10
11
|
import { setupCommand } from "./commands/setup.js";
|
|
11
12
|
import pkg from "../package.json" with { type: "json" };
|
|
12
13
|
const { version } = pkg;
|
|
@@ -32,6 +33,7 @@ program
|
|
|
32
33
|
.option("--set <id>", "Set the active workspace")
|
|
33
34
|
.action((opts) => workspaceCommand(opts));
|
|
34
35
|
registerVercelCommand(program);
|
|
36
|
+
registerNetlifyCommand(program);
|
|
35
37
|
program.command("logout").description("Disconnect your Inflight account").action(logoutCommand);
|
|
36
38
|
program.command("reset").description("Clear all Inflight auth and config").action(resetCommand);
|
|
37
39
|
program.parse();
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -19,3 +19,11 @@ export interface VercelConfig {
|
|
|
19
19
|
export declare function readVercelConfig(): VercelConfig | null;
|
|
20
20
|
export declare function writeVercelConfig(config: VercelConfig): void;
|
|
21
21
|
export declare function clearVercelConfig(): void;
|
|
22
|
+
export interface NetlifyConfig {
|
|
23
|
+
siteId: string;
|
|
24
|
+
siteName: string;
|
|
25
|
+
teamSlug: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function readNetlifyConfig(): NetlifyConfig | null;
|
|
28
|
+
export declare function writeNetlifyConfig(config: NetlifyConfig): void;
|
|
29
|
+
export declare function clearNetlifyConfig(): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -72,3 +72,24 @@ export function clearVercelConfig() {
|
|
|
72
72
|
unlinkSync(VERCEL_FILE);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
// --- Netlify config (global, persists across directories) ---
|
|
76
|
+
const NETLIFY_FILE = join(getGlobalConfigDir(), "netlify.json");
|
|
77
|
+
export function readNetlifyConfig() {
|
|
78
|
+
if (!existsSync(NETLIFY_FILE))
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(readFileSync(NETLIFY_FILE, "utf-8"));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function writeNetlifyConfig(config) {
|
|
88
|
+
mkdirSync(getGlobalConfigDir(), { recursive: true });
|
|
89
|
+
writeFileSync(NETLIFY_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
90
|
+
}
|
|
91
|
+
export function clearNetlifyConfig() {
|
|
92
|
+
if (existsSync(NETLIFY_FILE)) {
|
|
93
|
+
unlinkSync(NETLIFY_FILE);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export declare function ensureNetlifyCli(log?: (msg: string) => void): Promise<boolean>;
|
|
2
|
+
/**
|
|
3
|
+
* Gets the Netlify auth token non-interactively.
|
|
4
|
+
* Priority: NETLIFY_AUTH_TOKEN env var → CLI config file.
|
|
5
|
+
* Returns null if no valid token available.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getNetlifyToken(): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Ensures a valid Netlify auth token is available.
|
|
10
|
+
* Checks env/config first → opens browser login if needed.
|
|
11
|
+
* For interactive commands only.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureNetlifyAuth(): Promise<string | null>;
|
|
14
|
+
interface LocalNetlifySite {
|
|
15
|
+
siteId: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads `.netlify/state.json` from the given root directory.
|
|
19
|
+
* Created by `netlify link`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readLocalNetlifySite(root: string): LocalNetlifySite | null;
|
|
22
|
+
/**
|
|
23
|
+
* Writes `.netlify/state.json` at the given root directory and ensures `.netlify` is gitignored.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeLocalNetlifySite(root: string, siteId: string): void;
|
|
26
|
+
export interface NetlifySite {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
url: string;
|
|
30
|
+
ssl_url: string;
|
|
31
|
+
admin_url: string;
|
|
32
|
+
account_name: string;
|
|
33
|
+
account_slug: string;
|
|
34
|
+
build_settings?: {
|
|
35
|
+
provider?: string;
|
|
36
|
+
repo_path?: string;
|
|
37
|
+
repo_url?: string;
|
|
38
|
+
repo_branch?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface NetlifyDeploy {
|
|
42
|
+
id: string;
|
|
43
|
+
state: string;
|
|
44
|
+
branch: string | null;
|
|
45
|
+
commitRef: string | null;
|
|
46
|
+
commitMessage: string | null;
|
|
47
|
+
deploySslUrl: string;
|
|
48
|
+
context: string;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function getNetlifyUser(token: string): Promise<{
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
email: string;
|
|
55
|
+
} | null>;
|
|
56
|
+
export declare function getNetlifySites(token: string): Promise<NetlifySite[]>;
|
|
57
|
+
export declare function getNetlifySiteById(token: string, siteId: string): Promise<NetlifySite | null>;
|
|
58
|
+
export declare function getNetlifyDeploys(token: string, siteId: string, opts?: {
|
|
59
|
+
branch?: string;
|
|
60
|
+
limit?: number;
|
|
61
|
+
}): Promise<NetlifyDeploy[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Matches Netlify sites against a git remote's owner/repo.
|
|
64
|
+
* Compares build_settings.repo_path against "owner/repo".
|
|
65
|
+
*/
|
|
66
|
+
export declare function matchSitesByRepo(sites: NetlifySite[], gitOwner: string, gitRepo: string): NetlifySite[];
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { execSync, spawn, exec } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir, platform } from "os";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
// --- Netlify CLI management ---
|
|
8
|
+
function hasNetlifyCli() {
|
|
9
|
+
try {
|
|
10
|
+
execSync("netlify --version", { stdio: "pipe" });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function ensureNetlifyCli(log) {
|
|
18
|
+
if (hasNetlifyCli())
|
|
19
|
+
return true;
|
|
20
|
+
log?.("Installing Netlify CLI...");
|
|
21
|
+
try {
|
|
22
|
+
await execAsync("npm install -g netlify-cli");
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the Netlify CLI config directory for the current platform.
|
|
31
|
+
* Matches the paths used by netlify-cli via env-paths('netlify', { suffix: '' }).
|
|
32
|
+
*/
|
|
33
|
+
function getNetlifyConfigDir() {
|
|
34
|
+
if (platform() === "darwin") {
|
|
35
|
+
return join(homedir(), "Library", "Preferences", "netlify");
|
|
36
|
+
}
|
|
37
|
+
if (platform() === "win32") {
|
|
38
|
+
return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "netlify", "Config");
|
|
39
|
+
}
|
|
40
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "netlify");
|
|
41
|
+
}
|
|
42
|
+
function readNetlifyCliConfig() {
|
|
43
|
+
const configPath = join(getNetlifyConfigDir(), "config.json");
|
|
44
|
+
if (!existsSync(configPath))
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
48
|
+
return data.userId ? data : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Gets the Netlify auth token non-interactively.
|
|
56
|
+
* Priority: NETLIFY_AUTH_TOKEN env var → CLI config file.
|
|
57
|
+
* Returns null if no valid token available.
|
|
58
|
+
*/
|
|
59
|
+
export function getNetlifyToken() {
|
|
60
|
+
const envToken = process.env.NETLIFY_AUTH_TOKEN;
|
|
61
|
+
if (envToken && envToken !== "null")
|
|
62
|
+
return envToken;
|
|
63
|
+
const config = readNetlifyCliConfig();
|
|
64
|
+
if (!config)
|
|
65
|
+
return null;
|
|
66
|
+
const user = config.users[config.userId];
|
|
67
|
+
return user?.auth?.token ?? null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Ensures a valid Netlify auth token is available.
|
|
71
|
+
* Checks env/config first → opens browser login if needed.
|
|
72
|
+
* For interactive commands only.
|
|
73
|
+
*/
|
|
74
|
+
export async function ensureNetlifyAuth() {
|
|
75
|
+
const existing = getNetlifyToken();
|
|
76
|
+
if (existing)
|
|
77
|
+
return existing;
|
|
78
|
+
// Need browser login
|
|
79
|
+
const ok = await new Promise((resolve) => {
|
|
80
|
+
const child = spawn("netlify", ["login"], { stdio: "inherit" });
|
|
81
|
+
child.on("close", (code) => resolve(code === 0));
|
|
82
|
+
child.on("error", () => resolve(false));
|
|
83
|
+
});
|
|
84
|
+
if (!ok)
|
|
85
|
+
return null;
|
|
86
|
+
return getNetlifyToken();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reads `.netlify/state.json` from the given root directory.
|
|
90
|
+
* Created by `netlify link`.
|
|
91
|
+
*/
|
|
92
|
+
export function readLocalNetlifySite(root) {
|
|
93
|
+
const statePath = join(root, ".netlify", "state.json");
|
|
94
|
+
if (!existsSync(statePath))
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
98
|
+
if (typeof data.siteId === "string") {
|
|
99
|
+
return { siteId: data.siteId };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Writes `.netlify/state.json` at the given root directory and ensures `.netlify` is gitignored.
|
|
107
|
+
*/
|
|
108
|
+
export function writeLocalNetlifySite(root, siteId) {
|
|
109
|
+
const netlifyDir = join(root, ".netlify");
|
|
110
|
+
mkdirSync(netlifyDir, { recursive: true });
|
|
111
|
+
writeFileSync(join(netlifyDir, "state.json"), JSON.stringify({ siteId }, null, 2) + "\n");
|
|
112
|
+
// Ensure .netlify is in .gitignore
|
|
113
|
+
const gitignorePath = join(root, ".gitignore");
|
|
114
|
+
if (existsSync(gitignorePath)) {
|
|
115
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
116
|
+
if (!content.split("\n").some((line) => line.trim() === ".netlify")) {
|
|
117
|
+
appendFileSync(gitignorePath, "\n.netlify\n");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
writeFileSync(gitignorePath, ".netlify\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// --- API calls (all take token explicitly) ---
|
|
125
|
+
const NETLIFY_API = "https://api.netlify.com/api/v1";
|
|
126
|
+
export async function getNetlifyUser(token) {
|
|
127
|
+
const res = await fetch(`${NETLIFY_API}/user`, {
|
|
128
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
129
|
+
});
|
|
130
|
+
if (!res.ok)
|
|
131
|
+
return null;
|
|
132
|
+
const data = (await res.json());
|
|
133
|
+
return { id: data.id, name: data.full_name, email: data.email };
|
|
134
|
+
}
|
|
135
|
+
export async function getNetlifySites(token) {
|
|
136
|
+
const all = [];
|
|
137
|
+
let page = 1;
|
|
138
|
+
while (true) {
|
|
139
|
+
const params = new URLSearchParams({ filter: "all", per_page: "100", page: String(page) });
|
|
140
|
+
const res = await fetch(`${NETLIFY_API}/sites?${params}`, {
|
|
141
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok)
|
|
144
|
+
break;
|
|
145
|
+
const sites = (await res.json());
|
|
146
|
+
if (sites.length === 0)
|
|
147
|
+
break;
|
|
148
|
+
all.push(...sites);
|
|
149
|
+
if (sites.length < 100)
|
|
150
|
+
break;
|
|
151
|
+
page++;
|
|
152
|
+
}
|
|
153
|
+
return all;
|
|
154
|
+
}
|
|
155
|
+
export async function getNetlifySiteById(token, siteId) {
|
|
156
|
+
const res = await fetch(`${NETLIFY_API}/sites/${encodeURIComponent(siteId)}`, {
|
|
157
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok)
|
|
160
|
+
return null;
|
|
161
|
+
return (await res.json());
|
|
162
|
+
}
|
|
163
|
+
export async function getNetlifyDeploys(token, siteId, opts) {
|
|
164
|
+
const params = new URLSearchParams({ per_page: String(opts?.limit ?? 20) });
|
|
165
|
+
if (opts?.branch)
|
|
166
|
+
params.set("branch", opts.branch);
|
|
167
|
+
const res = await fetch(`${NETLIFY_API}/sites/${encodeURIComponent(siteId)}/deploys?${params}`, {
|
|
168
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok)
|
|
171
|
+
return [];
|
|
172
|
+
const deploys = (await res.json());
|
|
173
|
+
return deploys.map((d) => ({
|
|
174
|
+
id: d.id,
|
|
175
|
+
state: d.state,
|
|
176
|
+
branch: d.branch,
|
|
177
|
+
commitRef: d.commit_ref?.slice(0, 7) ?? null,
|
|
178
|
+
commitMessage: d.title,
|
|
179
|
+
deploySslUrl: d.deploy_ssl_url,
|
|
180
|
+
context: d.context,
|
|
181
|
+
createdAt: new Date(d.created_at).getTime(),
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Matches Netlify sites against a git remote's owner/repo.
|
|
186
|
+
* Compares build_settings.repo_path against "owner/repo".
|
|
187
|
+
*/
|
|
188
|
+
export function matchSitesByRepo(sites, gitOwner, gitRepo) {
|
|
189
|
+
const target = `${gitOwner}/${gitRepo}`.toLowerCase();
|
|
190
|
+
return sites.filter((s) => s.build_settings?.repo_path?.toLowerCase() === target);
|
|
191
|
+
}
|
package/dist/lib/vercel.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ export interface VercelTeam {
|
|
|
32
32
|
export interface VercelProject {
|
|
33
33
|
id: string;
|
|
34
34
|
name: string;
|
|
35
|
+
teamId: string;
|
|
36
|
+
teamName?: string;
|
|
37
|
+
link?: {
|
|
38
|
+
org?: string;
|
|
39
|
+
repo?: string;
|
|
40
|
+
repoId?: number;
|
|
41
|
+
type?: string;
|
|
42
|
+
};
|
|
35
43
|
}
|
|
36
44
|
export interface VercelDeployment {
|
|
37
45
|
url: string;
|
|
@@ -41,34 +49,20 @@ export interface VercelDeployment {
|
|
|
41
49
|
commitMessage: string | null;
|
|
42
50
|
createdAt: number;
|
|
43
51
|
}
|
|
44
|
-
export declare function getVercelTeams(token: string): Promise<VercelTeam[]>;
|
|
45
|
-
export declare function getVercelProjects(token: string, teamId: string): Promise<VercelProject[]>;
|
|
46
52
|
/**
|
|
47
53
|
* Fetches details for a single Vercel project by ID.
|
|
48
54
|
*/
|
|
49
|
-
export declare function
|
|
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
|
-
}
|
|
55
|
+
export declare function getVercelProjectById(token: string, projectId: string, teamId: string): Promise<VercelProject | null>;
|
|
62
56
|
/**
|
|
63
57
|
* Fetches all projects across all teams with their link info.
|
|
64
58
|
* Used for client-side repo matching.
|
|
65
59
|
*/
|
|
66
|
-
export declare function
|
|
60
|
+
export declare function getVercelProjects(token: string): Promise<VercelProject[]>;
|
|
67
61
|
/**
|
|
68
62
|
* Matches Vercel projects against a git remote's owner/repo.
|
|
69
63
|
* Exact match on link.org and link.repo.
|
|
70
64
|
*/
|
|
71
|
-
export declare function
|
|
65
|
+
export declare function matchVercelProjectsByRepo(projects: VercelProject[], gitOwner: string, gitRepo: string): VercelProject[];
|
|
72
66
|
/**
|
|
73
67
|
* Creates a new Vercel project linked to a git repository.
|
|
74
68
|
*/
|
|
@@ -85,7 +79,7 @@ export declare function getBranchAlias(token: string, teamId: string, projectId:
|
|
|
85
79
|
/**
|
|
86
80
|
* Fetches recent deployments for a project.
|
|
87
81
|
*/
|
|
88
|
-
export declare function
|
|
82
|
+
export declare function getVercelDeployments(token: string, teamId: string, projectId: string, opts?: {
|
|
89
83
|
limit?: number;
|
|
90
84
|
branch?: string;
|
|
91
85
|
}): Promise<VercelDeployment[]>;
|
package/dist/lib/vercel.js
CHANGED
|
@@ -142,7 +142,7 @@ export function writeLocalVercelProject(root, orgId, projectId) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
// --- API calls (all take token + IDs explicitly) ---
|
|
145
|
-
|
|
145
|
+
async function getVercelTeams(token) {
|
|
146
146
|
const res = await fetch("https://api.vercel.com/v2/teams", {
|
|
147
147
|
headers: { Authorization: `Bearer ${token}` },
|
|
148
148
|
});
|
|
@@ -151,20 +151,10 @@ export async function getVercelTeams(token) {
|
|
|
151
151
|
const data = (await res.json());
|
|
152
152
|
return data.teams;
|
|
153
153
|
}
|
|
154
|
-
export async function getVercelProjects(token, teamId) {
|
|
155
|
-
const params = new URLSearchParams({ teamId, limit: "100" });
|
|
156
|
-
const res = await fetch(`https://api.vercel.com/v10/projects?${params}`, {
|
|
157
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
158
|
-
});
|
|
159
|
-
if (!res.ok)
|
|
160
|
-
return [];
|
|
161
|
-
const data = (await res.json());
|
|
162
|
-
return data.projects;
|
|
163
|
-
}
|
|
164
154
|
/**
|
|
165
155
|
* Fetches details for a single Vercel project by ID.
|
|
166
156
|
*/
|
|
167
|
-
export async function
|
|
157
|
+
export async function getVercelProjectById(token, projectId, teamId) {
|
|
168
158
|
const params = new URLSearchParams({ teamId });
|
|
169
159
|
const res = await fetch(`https://api.vercel.com/v10/projects/${encodeURIComponent(projectId)}?${params}`, {
|
|
170
160
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -172,13 +162,13 @@ export async function getVercelProjectDetail(token, projectId, teamId) {
|
|
|
172
162
|
if (!res.ok)
|
|
173
163
|
return null;
|
|
174
164
|
const data = (await res.json());
|
|
175
|
-
return { id: data.id, name: data.name };
|
|
165
|
+
return { id: data.id, name: data.name, teamId, link: data.link };
|
|
176
166
|
}
|
|
177
167
|
/**
|
|
178
168
|
* Fetches all projects across all teams with their link info.
|
|
179
169
|
* Used for client-side repo matching.
|
|
180
170
|
*/
|
|
181
|
-
export async function
|
|
171
|
+
export async function getVercelProjects(token) {
|
|
182
172
|
const teams = await getVercelTeams(token);
|
|
183
173
|
const all = [];
|
|
184
174
|
const fetches = teams.map(async (team) => {
|
|
@@ -200,7 +190,7 @@ export async function fetchAllProjectsWithLinks(token) {
|
|
|
200
190
|
* Matches Vercel projects against a git remote's owner/repo.
|
|
201
191
|
* Exact match on link.org and link.repo.
|
|
202
192
|
*/
|
|
203
|
-
export function
|
|
193
|
+
export function matchVercelProjectsByRepo(projects, gitOwner, gitRepo) {
|
|
204
194
|
const linked = projects.filter((p) => p.link?.org && p.link?.repo);
|
|
205
195
|
return linked.filter((p) => p.link?.org === gitOwner && p.link?.repo === gitRepo);
|
|
206
196
|
}
|
|
@@ -225,7 +215,7 @@ export async function createVercelProject(token, teamId, name, repo, repoType) {
|
|
|
225
215
|
throw new Error(body?.error?.message ?? `Failed to create project (${res.status})`);
|
|
226
216
|
}
|
|
227
217
|
const data = (await res.json());
|
|
228
|
-
return { id: data.id, name: data.name };
|
|
218
|
+
return { id: data.id, name: data.name, teamId };
|
|
229
219
|
}
|
|
230
220
|
export async function getBranchAlias(token, teamId, projectId, branch) {
|
|
231
221
|
if (!branch)
|
|
@@ -261,7 +251,7 @@ export async function getBranchAlias(token, teamId, projectId, branch) {
|
|
|
261
251
|
/**
|
|
262
252
|
* Fetches recent deployments for a project.
|
|
263
253
|
*/
|
|
264
|
-
export async function
|
|
254
|
+
export async function getVercelDeployments(token, teamId, projectId, opts) {
|
|
265
255
|
const params = new URLSearchParams({
|
|
266
256
|
projectId,
|
|
267
257
|
teamId,
|
package/dist/providers/index.js
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GitInfo } from "../lib/git.js";
|
|
2
|
+
import type { ProviderResolveOptions } from "./index.js";
|
|
3
|
+
import type { NetlifyConfig } from "../lib/config.js";
|
|
4
|
+
export declare function pickNetlifySite(token: string): Promise<NetlifyConfig | null>;
|
|
5
|
+
export declare function resolveNetlifyUrl(cwd: string, gitInfo: GitInfo, opts?: ProviderResolveOptions): Promise<string | null>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
4
|
+
import { writeNetlifyConfig } from "../lib/config.js";
|
|
5
|
+
import { ensureNetlifyCli, ensureNetlifyAuth, readLocalNetlifySite, writeLocalNetlifySite, getNetlifySiteById, getNetlifySites, getNetlifyDeploys, matchSitesByRepo, } from "../lib/netlify.js";
|
|
6
|
+
// --- Auto-detection ---
|
|
7
|
+
/**
|
|
8
|
+
* Auto-detect the Netlify site for the current git repo.
|
|
9
|
+
*
|
|
10
|
+
* 1. `.netlify/state.json` at git root (instant, no API)
|
|
11
|
+
* 2. Fetch all sites, exact match on git remote
|
|
12
|
+
* 3. Multiple matches → pick from matches
|
|
13
|
+
* 4. Zero matches → pick from all sites
|
|
14
|
+
*/
|
|
15
|
+
async function autoDetectSite(cwd, gitInfo, token) {
|
|
16
|
+
const gitRoot = getGitRoot(cwd);
|
|
17
|
+
// Cache result to .netlify/state.json so subsequent runs skip API calls
|
|
18
|
+
const cacheResult = (site) => {
|
|
19
|
+
if (gitRoot) {
|
|
20
|
+
writeLocalNetlifySite(gitRoot, site.siteId);
|
|
21
|
+
}
|
|
22
|
+
return site;
|
|
23
|
+
};
|
|
24
|
+
// --- Fast path: .netlify/state.json ---
|
|
25
|
+
if (gitRoot) {
|
|
26
|
+
const localSite = readLocalNetlifySite(gitRoot);
|
|
27
|
+
if (localSite) {
|
|
28
|
+
const detail = await getNetlifySiteById(token, localSite.siteId);
|
|
29
|
+
if (detail) {
|
|
30
|
+
return {
|
|
31
|
+
siteId: detail.id,
|
|
32
|
+
siteName: detail.name,
|
|
33
|
+
teamSlug: detail.account_slug,
|
|
34
|
+
repoPath: detail.build_settings?.repo_path ?? null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// --- Fetch all sites ---
|
|
40
|
+
const spinner = p.spinner();
|
|
41
|
+
spinner.start("Detecting Netlify site...");
|
|
42
|
+
let allSites;
|
|
43
|
+
try {
|
|
44
|
+
allSites = await getNetlifySites(token);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
spinner.stop("Could not fetch Netlify sites.");
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (allSites.length === 0) {
|
|
51
|
+
spinner.stop("No Netlify sites found.");
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
// --- Try exact match on git remote ---
|
|
55
|
+
const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
|
|
56
|
+
if (gitRepo) {
|
|
57
|
+
const matches = matchSitesByRepo(allSites, gitRepo.owner, gitRepo.name);
|
|
58
|
+
if (matches.length === 1) {
|
|
59
|
+
spinner.stop(`Detected Netlify site: ${pc.bold(matches[0].name)}`);
|
|
60
|
+
return cacheResult({
|
|
61
|
+
siteId: matches[0].id,
|
|
62
|
+
siteName: matches[0].name,
|
|
63
|
+
teamSlug: matches[0].account_slug,
|
|
64
|
+
repoPath: matches[0].build_settings?.repo_path ?? null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (matches.length > 1) {
|
|
68
|
+
spinner.stop(`Found ${matches.length} Netlify sites for this repo.`);
|
|
69
|
+
const picked = await pickFromList(matches);
|
|
70
|
+
return picked ? cacheResult(picked) : null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// --- No match ---
|
|
74
|
+
spinner.stop("Could not auto-detect Netlify site.");
|
|
75
|
+
const picked = await pickFromList(allSites);
|
|
76
|
+
return picked ? cacheResult(picked) : null;
|
|
77
|
+
}
|
|
78
|
+
async function pickFromList(sites) {
|
|
79
|
+
const maxName = Math.max(...sites.map((s) => s.name.length));
|
|
80
|
+
const selected = await p.select({
|
|
81
|
+
message: "Select a Netlify site",
|
|
82
|
+
options: sites.map((s) => ({
|
|
83
|
+
value: s,
|
|
84
|
+
label: s.account_name
|
|
85
|
+
? `${s.name.padEnd(maxName)} ${pc.dim(`(${s.account_name})`)}`
|
|
86
|
+
: s.name,
|
|
87
|
+
})),
|
|
88
|
+
});
|
|
89
|
+
if (p.isCancel(selected)) {
|
|
90
|
+
p.cancel("Cancelled.");
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
const match = selected;
|
|
94
|
+
return { siteId: match.id, siteName: match.name, teamSlug: match.account_slug, repoPath: match.build_settings?.repo_path ?? null };
|
|
95
|
+
}
|
|
96
|
+
// --- Manual picker (used by `inflight netlify` command) ---
|
|
97
|
+
export async function pickNetlifySite(token) {
|
|
98
|
+
const allSites = await getNetlifySites(token);
|
|
99
|
+
if (allSites.length === 0) {
|
|
100
|
+
p.log.error("No Netlify sites found.");
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const selected = await p.select({
|
|
104
|
+
message: "Select a Netlify site",
|
|
105
|
+
options: allSites.map((s) => ({
|
|
106
|
+
value: s,
|
|
107
|
+
label: s.account_name ? `${s.name} ${pc.dim(`(${s.account_name})`)}` : s.name,
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
if (p.isCancel(selected)) {
|
|
111
|
+
p.cancel("Cancelled.");
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const match = selected;
|
|
115
|
+
const config = {
|
|
116
|
+
siteId: match.id,
|
|
117
|
+
siteName: match.name,
|
|
118
|
+
teamSlug: match.account_slug,
|
|
119
|
+
};
|
|
120
|
+
writeNetlifyConfig(config);
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
// --- Main resolve function ---
|
|
124
|
+
export async function resolveNetlifyUrl(cwd, gitInfo, opts) {
|
|
125
|
+
const cliOk = await ensureNetlifyCli((msg) => p.log.step(msg));
|
|
126
|
+
if (!cliOk) {
|
|
127
|
+
p.log.error("Failed to install Netlify CLI. Install manually: " + pc.cyan("npm install -g netlify-cli"));
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const token = await ensureNetlifyAuth();
|
|
131
|
+
if (!token) {
|
|
132
|
+
p.log.error("Netlify login failed.");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const site = await autoDetectSite(cwd, gitInfo, token);
|
|
136
|
+
if (!site)
|
|
137
|
+
return null;
|
|
138
|
+
const commitSha = gitInfo.commitShort?.slice(0, 7) ?? null;
|
|
139
|
+
// Check if the current git repo matches the Netlify site's linked repo
|
|
140
|
+
const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
|
|
141
|
+
const localRepoPath = gitRepo ? `${gitRepo.owner}/${gitRepo.name}`.toLowerCase() : null;
|
|
142
|
+
const repoMatches = site.repoPath && localRepoPath ? site.repoPath.toLowerCase() === localRepoPath : false;
|
|
143
|
+
// Only filter by branch if we're in the matching repo — otherwise show all deploys
|
|
144
|
+
let deploys = await getNetlifyDeploys(token, site.siteId, {
|
|
145
|
+
branch: repoMatches ? (gitInfo.branch ?? undefined) : undefined,
|
|
146
|
+
});
|
|
147
|
+
let commitDeploy = repoMatches ? deploys.find((d) => d.commitRef === commitSha) : undefined;
|
|
148
|
+
// If just pushed, poll until the commit deployment appears
|
|
149
|
+
if (!commitDeploy && repoMatches && opts?.justPushed) {
|
|
150
|
+
const pollSpinner = p.spinner();
|
|
151
|
+
pollSpinner.start("Waiting for Netlify deployment...");
|
|
152
|
+
for (let i = 0; i < 30; i++) {
|
|
153
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
154
|
+
deploys = await getNetlifyDeploys(token, site.siteId, {
|
|
155
|
+
branch: repoMatches ? (gitInfo.branch ?? undefined) : undefined,
|
|
156
|
+
});
|
|
157
|
+
commitDeploy = deploys.find((d) => d.commitRef === commitSha);
|
|
158
|
+
if (commitDeploy)
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (commitDeploy) {
|
|
162
|
+
pollSpinner.stop("Deployment found!");
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
pollSpinner.stop("Deployment is still building...");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// If we found a commit-specific deployment, use it automatically
|
|
169
|
+
if (commitDeploy) {
|
|
170
|
+
const stateLabel = commitDeploy.state !== "ready" ? ` ${pc.yellow(`(${commitDeploy.state})`)}` : "";
|
|
171
|
+
const message = commitDeploy.commitMessage
|
|
172
|
+
? pc.dim(` — ${truncate(commitDeploy.commitMessage.split("\n")[0], 50)}`)
|
|
173
|
+
: "";
|
|
174
|
+
p.log.info(`Deployment for ${pc.bold(commitSha)}${message}:\n → ${pc.cyan(commitDeploy.deploySslUrl)}${stateLabel}`);
|
|
175
|
+
return commitDeploy.deploySslUrl;
|
|
176
|
+
}
|
|
177
|
+
// Fallback: no commit deployment — let user pick from recent or paste manually
|
|
178
|
+
if (deploys.length === 0) {
|
|
179
|
+
p.log.warn("No deployments found. Paste a URL instead.");
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const maxBranch = Math.max(...deploys.map((d) => (d.branch ?? "unknown").length));
|
|
183
|
+
const selected = await p.select({
|
|
184
|
+
message: "No deployment found for current commit. Select one:",
|
|
185
|
+
options: [
|
|
186
|
+
...deploys.map((d) => {
|
|
187
|
+
const branch = (d.branch ?? "unknown").padEnd(maxBranch);
|
|
188
|
+
const ago = timeAgo(d.createdAt).padEnd(8);
|
|
189
|
+
const state = d.state !== "ready" ? ` ${pc.yellow(`(${d.state})`)}` : "";
|
|
190
|
+
const firstLine = (d.commitMessage ?? "No commit message").split("\n")[0];
|
|
191
|
+
const msg = truncate(firstLine, 55);
|
|
192
|
+
return { value: d.deploySslUrl, label: `${branch} ${ago}${state} ${pc.dim(msg)}` };
|
|
193
|
+
}),
|
|
194
|
+
{ value: "manual", label: "Paste a URL manually" },
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
if (p.isCancel(selected)) {
|
|
198
|
+
p.cancel("Cancelled.");
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
return selected === "manual" ? null : selected;
|
|
202
|
+
}
|
|
203
|
+
function truncate(str, max) {
|
|
204
|
+
return str.length > max ? str.slice(0, max - 1) + "…" : str;
|
|
205
|
+
}
|
|
206
|
+
function timeAgo(timestamp) {
|
|
207
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
208
|
+
if (seconds < 60)
|
|
209
|
+
return "now";
|
|
210
|
+
const minutes = Math.floor(seconds / 60);
|
|
211
|
+
if (minutes < 60)
|
|
212
|
+
return `${minutes}m ago`;
|
|
213
|
+
const hours = Math.floor(minutes / 60);
|
|
214
|
+
if (hours < 24)
|
|
215
|
+
return `${hours}h ago`;
|
|
216
|
+
const days = Math.floor(hours / 24);
|
|
217
|
+
return `${days}d ago`;
|
|
218
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import type { GitInfo } from "../lib/git.js";
|
|
2
2
|
import type { ProviderResolveOptions } from "./index.js";
|
|
3
|
-
import type { VercelConfig } from "../lib/config.js";
|
|
4
|
-
/**
|
|
5
|
-
* Interactive project picker. Saves selection to global config.
|
|
6
|
-
* Used by `inflight vercel` command for explicit manual override.
|
|
7
|
-
*/
|
|
8
|
-
export declare function pickVercelProject(token: string): Promise<VercelConfig | null>;
|
|
9
3
|
export declare function resolveVercelUrl(cwd: string, gitInfo: GitInfo, opts?: ProviderResolveOptions): Promise<string | null>;
|
package/dist/providers/vercel.js
CHANGED
|
@@ -2,8 +2,7 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { parseGitRepo, getGitRoot } from "../lib/git.js";
|
|
5
|
-
import {
|
|
6
|
-
import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectDetail, fetchAllProjectsWithLinks, matchProjectsByRepo, createVercelProject, getRecentDeployments, } from "../lib/vercel.js";
|
|
5
|
+
import { ensureVercelCli, ensureVercelAuth, readLocalVercelProject, writeLocalVercelProject, getVercelProjectById, getVercelProjects, matchVercelProjectsByRepo, createVercelProject, getVercelDeployments, } from "../lib/vercel.js";
|
|
7
6
|
// --- Auto-detection ---
|
|
8
7
|
/**
|
|
9
8
|
* Auto-detect the Vercel project for the current git repo.
|
|
@@ -27,10 +26,16 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
27
26
|
if (gitRoot) {
|
|
28
27
|
const localProject = readLocalVercelProject(gitRoot);
|
|
29
28
|
if (localProject) {
|
|
30
|
-
const detail = await
|
|
29
|
+
const detail = await getVercelProjectById(token, localProject.projectId, localProject.orgId);
|
|
31
30
|
if (detail) {
|
|
32
31
|
// p.log.info(`Detected Vercel project: ${pc.bold(detail.name)}`);
|
|
33
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
teamId: localProject.orgId,
|
|
34
|
+
projectId: detail.id,
|
|
35
|
+
projectName: detail.name,
|
|
36
|
+
repoOwner: detail.link?.org ?? null,
|
|
37
|
+
repoName: detail.link?.repo ?? null,
|
|
38
|
+
};
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
}
|
|
@@ -39,7 +44,7 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
39
44
|
spinner.start("Detecting Vercel project...");
|
|
40
45
|
let allProjects;
|
|
41
46
|
try {
|
|
42
|
-
allProjects = await
|
|
47
|
+
allProjects = await getVercelProjects(token);
|
|
43
48
|
}
|
|
44
49
|
catch {
|
|
45
50
|
spinner.stop("Could not fetch Vercel projects.");
|
|
@@ -52,13 +57,15 @@ async function autoDetectProject(cwd, gitInfo, token) {
|
|
|
52
57
|
// --- Try exact match on git remote ---
|
|
53
58
|
const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
|
|
54
59
|
if (gitRepo) {
|
|
55
|
-
const matches =
|
|
60
|
+
const matches = matchVercelProjectsByRepo(allProjects, gitRepo.owner, gitRepo.name);
|
|
56
61
|
if (matches.length === 1) {
|
|
57
62
|
spinner.stop(`Detected Vercel project: ${pc.bold(matches[0].name)}`);
|
|
58
63
|
return cacheResult({
|
|
59
64
|
teamId: matches[0].teamId,
|
|
60
65
|
projectId: matches[0].id,
|
|
61
66
|
projectName: matches[0].name,
|
|
67
|
+
repoOwner: matches[0].link?.org ?? null,
|
|
68
|
+
repoName: matches[0].link?.repo ?? null,
|
|
62
69
|
});
|
|
63
70
|
}
|
|
64
71
|
if (matches.length > 1) {
|
|
@@ -95,7 +102,13 @@ async function pickFromList(projects, createCtx) {
|
|
|
95
102
|
return createProjectFlow(projects, createCtx);
|
|
96
103
|
}
|
|
97
104
|
const match = selected;
|
|
98
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
teamId: match.teamId,
|
|
107
|
+
projectId: match.id,
|
|
108
|
+
projectName: match.name,
|
|
109
|
+
repoOwner: match.link?.org ?? null,
|
|
110
|
+
repoName: match.link?.repo ?? null,
|
|
111
|
+
};
|
|
99
112
|
}
|
|
100
113
|
async function createProjectFlow(projects, ctx) {
|
|
101
114
|
const { token, gitRepo } = ctx;
|
|
@@ -128,59 +141,45 @@ async function createProjectFlow(projects, ctx) {
|
|
|
128
141
|
}
|
|
129
142
|
catch {
|
|
130
143
|
p.log.info("Push to git to trigger your first deployment.");
|
|
131
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
teamId,
|
|
146
|
+
projectId: created.id,
|
|
147
|
+
projectName: created.name,
|
|
148
|
+
repoOwner: gitRepo.owner,
|
|
149
|
+
repoName: gitRepo.name,
|
|
150
|
+
};
|
|
132
151
|
}
|
|
133
152
|
// Poll until a deployment appears
|
|
134
153
|
const pollSpinner = p.spinner();
|
|
135
154
|
pollSpinner.start("Waiting for Vercel to start deploying...");
|
|
136
155
|
for (let i = 0; i < 30; i++) {
|
|
137
156
|
await new Promise((r) => setTimeout(r, 2000));
|
|
138
|
-
const deps = await
|
|
157
|
+
const deps = await getVercelDeployments(token, teamId, created.id);
|
|
139
158
|
if (deps.length > 0) {
|
|
140
159
|
pollSpinner.stop("Deployment started.");
|
|
141
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
teamId,
|
|
162
|
+
projectId: created.id,
|
|
163
|
+
projectName: created.name,
|
|
164
|
+
repoOwner: gitRepo.owner,
|
|
165
|
+
repoName: gitRepo.name,
|
|
166
|
+
};
|
|
142
167
|
}
|
|
143
168
|
}
|
|
144
169
|
pollSpinner.stop("Deployment may take a moment to appear.");
|
|
145
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
teamId,
|
|
172
|
+
projectId: created.id,
|
|
173
|
+
projectName: created.name,
|
|
174
|
+
repoOwner: gitRepo.owner,
|
|
175
|
+
repoName: gitRepo.name,
|
|
176
|
+
};
|
|
146
177
|
}
|
|
147
178
|
catch (e) {
|
|
148
179
|
spinner.stop(e.message);
|
|
149
180
|
return null;
|
|
150
181
|
}
|
|
151
182
|
}
|
|
152
|
-
// --- Manual picker (used by `inflight vercel` command) ---
|
|
153
|
-
/**
|
|
154
|
-
* Interactive project picker. Saves selection to global config.
|
|
155
|
-
* Used by `inflight vercel` command for explicit manual override.
|
|
156
|
-
*/
|
|
157
|
-
export async function pickVercelProject(token) {
|
|
158
|
-
const allProjects = await fetchAllProjectsWithLinks(token);
|
|
159
|
-
if (allProjects.length === 0) {
|
|
160
|
-
p.log.error("No Vercel projects found.");
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
const selected = await p.select({
|
|
164
|
-
message: "Select a Vercel project",
|
|
165
|
-
options: allProjects.map((proj) => ({
|
|
166
|
-
value: proj,
|
|
167
|
-
label: proj.teamName ? `${proj.name} ${pc.dim(`(${proj.teamName})`)}` : proj.name,
|
|
168
|
-
})),
|
|
169
|
-
});
|
|
170
|
-
if (p.isCancel(selected)) {
|
|
171
|
-
p.cancel("Cancelled.");
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
const match = selected;
|
|
175
|
-
const config = {
|
|
176
|
-
teamId: match.teamId,
|
|
177
|
-
teamName: match.teamName,
|
|
178
|
-
projectId: match.id,
|
|
179
|
-
projectName: match.name,
|
|
180
|
-
};
|
|
181
|
-
writeVercelConfig(config);
|
|
182
|
-
return config;
|
|
183
|
-
}
|
|
184
183
|
// --- Main resolve function ---
|
|
185
184
|
export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
186
185
|
const cliOk = await ensureVercelCli((msg) => p.log.step(msg));
|
|
@@ -197,19 +196,25 @@ export async function resolveVercelUrl(cwd, gitInfo, opts) {
|
|
|
197
196
|
if (!project)
|
|
198
197
|
return null;
|
|
199
198
|
const commitSha = gitInfo.commitShort?.slice(0, 7) ?? null;
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
// Check if the current git repo matches the Vercel project's linked repo
|
|
200
|
+
const gitRepo = gitInfo.remoteUrl ? parseGitRepo(gitInfo.remoteUrl) : null;
|
|
201
|
+
const repoMatches = project.repoOwner && project.repoName && gitRepo
|
|
202
|
+
? project.repoOwner.toLowerCase() === gitRepo.owner.toLowerCase() &&
|
|
203
|
+
project.repoName.toLowerCase() === gitRepo.name.toLowerCase()
|
|
204
|
+
: false;
|
|
205
|
+
// Only filter by branch if we're in the matching repo — otherwise show all deployments
|
|
206
|
+
let deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
|
|
207
|
+
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
203
208
|
});
|
|
204
|
-
let commitDeploy = deployments.find((d) => d.commitSha === commitSha);
|
|
209
|
+
let commitDeploy = repoMatches ? deployments.find((d) => d.commitSha === commitSha) : undefined;
|
|
205
210
|
// If just pushed, poll until the commit deployment appears
|
|
206
|
-
if (!commitDeploy && opts?.justPushed) {
|
|
211
|
+
if (!commitDeploy && repoMatches && opts?.justPushed) {
|
|
207
212
|
const pollSpinner = p.spinner();
|
|
208
213
|
pollSpinner.start("Waiting for Vercel deployment...");
|
|
209
214
|
for (let i = 0; i < 30; i++) {
|
|
210
215
|
await new Promise((r) => setTimeout(r, 2000));
|
|
211
|
-
deployments = await
|
|
212
|
-
branch: gitInfo.branch ?? undefined,
|
|
216
|
+
deployments = await getVercelDeployments(token, project.teamId, project.projectId, {
|
|
217
|
+
branch: repoMatches ? gitInfo.branch ?? undefined : undefined,
|
|
213
218
|
});
|
|
214
219
|
commitDeploy = deployments.find((d) => d.commitSha === commitSha);
|
|
215
220
|
if (commitDeploy)
|