UNPKG

vercel

Version:

The command-line interface for Vercel

1,484 lines (1,468 loc) • 46 kB
import { createRequire as __createRequire } from 'node:module'; import { fileURLToPath as __fileURLToPath } from 'node:url'; import { dirname as __dirname_ } from 'node:path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirname_(__filename); import { getLocalPathConfig } from "./chunk-4VPRHRPA.js"; import { VERCEL_DIR_PROJECT, VERCEL_DIR_README, checkExistsAndConnect, compileVercelConfig, createProject, detectProjects, fetchProjectsForRepoUrl, findProjectsFromPath, findRepoRoot, findSourceVercelConfigFile, getLinkedProject, getProjectByNameOrId, getServicesConfigWriteBlocker, getTeams, getVercelDirectory, humanizePath, isDirectory, linkFolderToProject, linkRepoProject, parseGitConfig, pluckRemoteUrls, pull, readJSONFile, require_dist3 as require_dist, require_frameworks, require_lib, require_slugify, resolveGitRemote, selectAndParseRemoteUrl, selectOrg, writeServicesConfig } from "./chunk-X775BOSL.js"; import { table } from "./chunk-DKD6GTQT.js"; import { printError } from "./chunk-4GQQJY5Y.js"; import { CantParseJSONFile, ProjectNotFound, isAPIError } from "./chunk-UGXBNJMO.js"; import { output_manager_default } from "./chunk-ZQKJVHXY.js"; import { require_source } from "./chunk-S7KYDPEM.js"; import { __toESM } from "./chunk-TZ2YI2VH.js"; // src/util/validate-paths.ts var import_fs_extra = __toESM(require_lib(), 1); var import_chalk = __toESM(require_source(), 1); import { homedir } from "os"; async function validateRootDirectory(cwd, path2, errorSuffix = "") { const pathStat = await (0, import_fs_extra.lstat)(path2).catch(() => null); const suffix = errorSuffix ? ` ${errorSuffix}` : ""; if (!pathStat) { output_manager_default.error( `The provided path ${import_chalk.default.cyan( `\u201C${humanizePath(path2)}\u201D` )} does not exist.${suffix}` ); return false; } if (!pathStat.isDirectory()) { output_manager_default.error( `The provided path ${import_chalk.default.cyan( `\u201C${humanizePath(path2)}\u201D` )} is a file, but expected a directory.${suffix}` ); return false; } if (!path2.startsWith(cwd)) { output_manager_default.error( `The provided path ${import_chalk.default.cyan( `\u201C${humanizePath(path2)}\u201D` )} is outside of the project.${suffix}` ); return false; } return true; } async function validatePaths(client, paths) { if (paths.length > 1) { output_manager_default.error(`Can't deploy more than one path.`); return { valid: false, exitCode: 1 }; } const path2 = paths[0]; const pathStat = await (0, import_fs_extra.lstat)(path2).catch(() => null); if (!pathStat) { output_manager_default.error(`Could not find ${import_chalk.default.cyan(`\u201C${humanizePath(path2)}\u201D`)}`); return { valid: false, exitCode: 1 }; } if (!pathStat.isDirectory()) { output_manager_default.prettyError({ message: "Support for single file deployments has been removed.", link: "https://vercel.link/no-single-file-deployments" }); return { valid: false, exitCode: 1 }; } if (path2 === homedir()) { const shouldDeployHomeDirectory = await client.input.confirm( `You are deploying your home directory. Do you want to continue?`, false ); if (!shouldDeployHomeDirectory) { output_manager_default.print(`Canceled `); return { valid: false, exitCode: 0 }; } } return { valid: true, path: path2 }; } // src/util/config/read-config.ts async function readConfig(dir) { let pkgFilePath; try { const compileResult = await compileVercelConfig(dir); pkgFilePath = compileResult.configPath || getLocalPathConfig(dir); } catch (err) { if (err instanceof Error) { return err; } throw err; } const result = await readJSONFile(pkgFilePath); if (result instanceof CantParseJSONFile) { return result; } if (result) { return result; } return null; } // src/util/input/display-services.ts var import_frameworks = __toESM(require_frameworks(), 1); import { getServiceQueueTopics, isQueueTriggeredService, isScheduleTriggeredService } from "@vercel/build-utils"; var chalk2 = require_source(); var frameworksBySlug = new Map(import_frameworks.frameworkList.map((f) => [f.slug, f])); var frameworkColors = { // JavaScript/TypeScript frameworks nextjs: chalk2.white, vite: chalk2.magenta, nuxtjs: chalk2.green, remix: chalk2.cyan, astro: chalk2.magenta, gatsby: chalk2.magenta, svelte: chalk2.red, sveltekit: chalk2.red, solidstart: chalk2.blue, angular: chalk2.red, vue: chalk2.green, ember: chalk2.red, preact: chalk2.magenta, // Python frameworks fastapi: chalk2.green, flask: chalk2.cyan, // Node frameworks express: chalk2.yellow, nest: chalk2.red, hono: chalk2.yellowBright }; var runtimeColors = { node: chalk2.green, python: chalk2.blue, go: chalk2.cyan, ruby: chalk2.red, rust: chalk2.yellowBright }; function getFrameworkName(slug) { if (!slug) return void 0; return frameworksBySlug.get(slug)?.name; } function formatRoutePrefix(routePrefix) { if (routePrefix === "/") { return "/"; } const normalized = routePrefix.startsWith("/") ? routePrefix : `/${routePrefix}`; return `${normalized}/*`; } var jobTriggerLabels = { queue: "Job/Queue", schedule: "Job/Schedule", workflow: "Job/Workflow" }; function getServiceDescriptionInfo(service) { if (service.type === "worker" || service.type === "job" || service.type === "cron") { const typeLabel = service.type === "worker" ? "Worker" : jobTriggerLabels[service.trigger ?? ""] ?? "Job"; const typeColorFn = service.type === "worker" ? chalk2.magenta : chalk2.cyan; if (service.runtime) { const runtimeName = service.runtime.charAt(0).toUpperCase() + service.runtime.slice(1); const runtimeColorFn = runtimeColors[service.runtime] || chalk2.yellow; const label = `${typeLabel}${chalk2.white("/")}${runtimeColorFn(runtimeName)}`; return { label, colorFn: typeColorFn }; } return { label: typeLabel, colorFn: typeColorFn }; } const frameworkName = getFrameworkName(service.framework); if (frameworkName && service.framework) { const colorFn = frameworkColors[service.framework] || chalk2.cyan; return { label: frameworkName, colorFn }; } else if (service.runtime) { const normalizedRuntime = service.runtime.toLowerCase().replace(/@.*$/, ""); const colorFn = runtimeColors[normalizedRuntime] || chalk2.yellow; return { label: service.runtime, colorFn }; } else if (service.builder?.use) { return { label: service.builder.use, colorFn: chalk2.magenta }; } return { label: "unknown", colorFn: chalk2.dim }; } function getServiceTarget(service) { if (isScheduleTriggeredService(service)) { return `schedule: ${service.schedule ?? "none"}`; } if (isQueueTriggeredService(service)) { const topics = getServiceQueueTopics(service); return `topics: ${topics.join(", ")}`; } if (service.type === "job" && service.trigger === "workflow") { return "workflow"; } return service.routePrefix ? formatRoutePrefix(service.routePrefix) : "no route"; } function displayDetectedServices(services) { output_manager_default.print(`Detected services: `); const outputOrder = { web: 0, cron: 1, job: 1, worker: 2 }; const sorted = [...services].sort( (a, b) => (outputOrder[a.type] ?? 3) - (outputOrder[b.type] ?? 3) ); const rows = sorted.map((service) => { const descInfo = getServiceDescriptionInfo(service); const target = getServiceTarget(service); return [ `\u2022 ${service.name}`, descInfo.colorFn(`[${descInfo.label}]`), chalk2.dim("\u2192"), target ]; }); const tableOutput = table(rows, { align: ["l", "l", "l", "l"], hsep: 2 }); output_manager_default.print(`${tableOutput} `); } function displayServicesConfigNote(configFileName = "vercel.json") { output_manager_default.print( ` ${chalk2.dim(`Services are configured via ${configFileName}.`)} ` ); } function displayServiceErrors(errors) { for (const error of errors) { output_manager_default.warn(error.message); } } // src/util/link/setup-and-link.ts var import_chalk6 = __toESM(require_source(), 1); var import_fs_extra2 = __toESM(require_lib(), 1); var import_fs_detectors2 = __toESM(require_dist(), 1); import { join as join2, basename } from "path"; // src/util/input/input-project.ts var import_chalk2 = __toESM(require_source(), 1); var import_slugify = __toESM(require_slugify(), 1); async function inputProject(client, org, detectedProjectName, autoConfirm = false, skipAutoDetect = false) { const slugifiedName = (0, import_slugify.default)(detectedProjectName); let detectedProject = null; if (!skipAutoDetect) { output_manager_default.spinner("Searching for existing projects\u2026", 1e3); const [project, slugifiedProject] = await Promise.all([ getProjectByNameOrId(client, detectedProjectName, org.id), slugifiedName !== detectedProjectName ? getProjectByNameOrId(client, slugifiedName, org.id) : null ]); detectedProject = !(project instanceof ProjectNotFound) ? project : !(slugifiedProject instanceof ProjectNotFound) ? slugifiedProject : null; if (detectedProject && !detectedProject.id) { throw new Error(`Detected linked project does not have "id".`); } output_manager_default.stopSpinner(); } if (autoConfirm) { return detectedProject || detectedProjectName; } if (client.nonInteractive) { if (detectedProject) { return detectedProject; } const err = new Error("Confirmation required"); err.code = "HEADLESS"; throw err; } let shouldLinkProject; if (!detectedProject) { shouldLinkProject = await client.input.confirm( `Link to existing project?`, false ); } else { if (await client.input.confirm( `Found project ${import_chalk2.default.cyan( `"${org.slug}/${detectedProject.name}"` )}. Link to it?`, true )) { return detectedProject; } shouldLinkProject = await client.input.confirm( `Link to different existing project?`, true ); } if (shouldLinkProject) { const firstPage = await client.fetch(`/v9/projects?limit=100`, { accountId: org.id }); const projects = firstPage.projects; const hasMoreProjects = firstPage.pagination.next !== null; if (projects.length === 0) { output_manager_default.log( `No existing projects found under ${import_chalk2.default.bold(org.slug)}. Creating new project.` ); } else if (hasMoreProjects) { let toLink; await client.input.text({ message: "Existing project name?", validate: async (val) => { if (!val) { return "Project name cannot be empty"; } const project = await getProjectByNameOrId(client, val, org.id); if (project instanceof ProjectNotFound) { return "Project not found"; } toLink = project; return true; } }); return toLink; } else { const choices = projects.sort((a, b) => b.updatedAt - a.updatedAt).map((project) => ({ name: project.name, value: project })); const toLink = await client.input.select({ message: "Which existing project do you want to link?", choices }); return toLink; } } return await client.input.text({ message: `Name?`, default: !detectedProject ? slugifiedName : void 0, validate: async (val) => { if (!val) { return "Project name cannot be empty"; } const project = await getProjectByNameOrId(client, val, org.id); if (!(project instanceof ProjectNotFound)) { return "Project already exists"; } return true; } }); } // src/util/input/input-root-directory.ts var import_chalk3 = __toESM(require_source(), 1); import { normalizePath } from "@vercel/build-utils"; import path from "path"; async function inputRootDirectory(client, cwd, autoConfirm = false) { if (autoConfirm) { return null; } while (true) { const rootDirectory = await client.input.text({ message: `In which directory is your code located?`, transformer: (input) => { return `${import_chalk3.default.dim(`./`)}${input}`; } }); if (!rootDirectory) { return null; } const normal = path.normalize(rootDirectory); if (normal === "." || normal === "./") { return null; } const fullPath = path.join(cwd, normal); if (await validateRootDirectory( cwd, fullPath, "Please choose a different one." ) === false) { continue; } return normalizePath(normal); } } // src/util/input/edit-project-settings.ts var import_chalk4 = __toESM(require_source(), 1); var import_frameworks2 = __toESM(require_frameworks(), 1); // src/util/is-setting-value.ts function isSettingValue(setting) { return setting && typeof setting.value === "string"; } // src/util/input/edit-project-settings.ts var settingMap = { buildCommand: "Build Command", devCommand: "Development Command", commandForIgnoringBuildStep: "Ignore Command", installCommand: "Install Command", outputDirectory: "Output Directory", framework: "Framework" }; var settingKeys = Object.keys(settingMap).sort(); async function editProjectSettings(client, projectSettings, framework, autoConfirm, localConfigurationOverrides, configFileName = "vercel.json") { const settings = Object.assign( { buildCommand: null, devCommand: null, framework: null, commandForIgnoringBuildStep: null, installCommand: null, outputDirectory: null }, projectSettings ); const hasLocalConfigurationOverrides = localConfigurationOverrides && Object.values(localConfigurationOverrides ?? {}).some(Boolean); if (hasLocalConfigurationOverrides) { for (const setting of settingKeys) { const localConfigValue = localConfigurationOverrides[setting]; if (localConfigValue) settings[setting] = localConfigValue; } output_manager_default.print(` Local settings detected in ${configFileName}: `); for (const setting of settingKeys) { const override = localConfigurationOverrides[setting]; if (override) { output_manager_default.print( ` ${import_chalk4.default.dim( `${import_chalk4.default.bold(`${settingMap[setting]}:`)} ${override}` )} ` ); } } if (localConfigurationOverrides.framework) { const overrideFramework = import_frameworks2.frameworkList.find( (f) => f.slug === localConfigurationOverrides.framework ); if (overrideFramework) { framework = overrideFramework; output_manager_default.print( ` Merging default Project Settings for ${framework.name}. Previously listed overrides are prioritized. ` ); } } } if (!framework) { settings.framework = null; return settings; } if (!framework.slug) { output_manager_default.print(` No framework detected. Default Project Settings: `); } else { const buildCmd = framework.settings.buildCommand?.value ?? null; const outputSetting = framework.settings.outputDirectory; const outputDir = outputSetting ? isSettingValue(outputSetting) ? outputSetting.value : outputSetting.placeholder : null; const inline = [ buildCmd ? `${settingMap.buildCommand}: ${buildCmd}` : null, outputDir ? `${settingMap.outputDirectory}: ${outputDir}` : null ].filter(Boolean); const detail = inline.length ? import_chalk4.default.dim(` (${inline.join(", ")})`) : ""; output_manager_default.print(` ${import_chalk4.default.bold("Detected")} ${framework.name}${detail} `); } settings.framework = framework.slug; if (!framework.slug) { for (const setting of settingKeys) { if (setting === "framework" || setting === "commandForIgnoringBuildStep") { continue; } const defaultSetting = framework.settings[setting]; const override = localConfigurationOverrides?.[setting]; if (!override && defaultSetting) { output_manager_default.print( ` ${import_chalk4.default.dim( `${import_chalk4.default.bold(`${settingMap[setting]}:`)} ${isSettingValue(defaultSetting) ? defaultSetting.value : import_chalk4.default.italic(`${defaultSetting.placeholder}`)}` )} ` ); } } } if (autoConfirm || !await client.input.confirm("Customize settings?", false)) { return settings; } const choices = settingKeys.reduce( (acc, setting) => { const skip = setting === "framework" || setting === "commandForIgnoringBuildStep" || setting === "installCommand" || localConfigurationOverrides?.[setting]; if (skip) return acc; return [...acc, { name: settingMap[setting], value: setting }]; }, [] ); const settingFields = await client.input.checkbox({ message: "Which settings would you like to overwrite (select multiple)?", choices }); for (const setting of settingFields) { const field = settingMap[setting]; settings[setting] = await client.input.text({ message: `${import_chalk4.default.bold(field)}?` }); } return settings; } // src/util/link/setup-and-link.ts var import_frameworks3 = __toESM(require_frameworks(), 1); // src/util/input/vercel-auth.ts var import_chalk5 = __toESM(require_source(), 1); var DEFAULT_VERCEL_AUTH_SETTING = "standard"; var OPTIONS = { message: `What setting do you want to use for Vercel Authentication?`, default: DEFAULT_VERCEL_AUTH_SETTING, choices: [ { description: "Standard Protection (recommended)", name: "standard", value: "standard" }, { description: "No Protection (all deployments will be public)", name: "none", value: "none" } ] }; async function vercelAuth(client, { autoConfirm = false }) { if (autoConfirm || await client.input.confirm( `Want to use the default Deployment Protection settings? ${import_chalk5.default.dim(`(Vercel Authentication: Standard Protection)`)}`, true )) { return DEFAULT_VERCEL_AUTH_SETTING; } const vercelAuth2 = await client.input.select(OPTIONS); return vercelAuth2; } // src/util/link/services-setup.ts var import_fs_detectors = __toESM(require_dist(), 1); import { normalizePath as normalizePath2 } from "@vercel/build-utils"; import { join, relative } from "path"; var SERVICES_DOCS_URL = "https://vercel.com/docs/services"; var INFERRED_SERVICES_PROMPT = "Multiple services were detected. How would you like to set up this project?"; async function getServicesSetupState(workPath) { const detectServicesResult = await (0, import_fs_detectors.detectServices)({ fs: new import_fs_detectors.LocalFileSystemDetector(workPath) }); const hasConfiguredServices = detectServicesResult.resolved?.source === "configured"; const inferredServices = hasConfiguredServices ? null : detectServicesResult.inferred; const inferredServicesWriteBlocker = inferredServices ? await getServicesConfigWriteBlocker(workPath, inferredServices.config) : null; return { detectServicesResult, hasConfiguredServices, inferredServices, inferredServicesWriteBlocker }; } function displayConfiguredServicesSetup(detectServicesResult, configFileName = "vercel.json") { if (detectServicesResult.services.length > 0) { displayDetectedServices(detectServicesResult.services); } if (detectServicesResult.errors.length > 0) { displayServiceErrors(detectServicesResult.errors); } displayServicesConfigNote(configFileName); } function formatDetectedServicesSummary(services) { if (services.length === 0) { return ""; } if (services.length === 1) { return `"${services[0].name}"`; } if (services.length === 2) { return `"${services[0].name}" + "${services[1].name}"`; } const othersCount = services.length - 2; return `"${services[0].name}" + "${services[1].name}" + ${othersCount} ${othersCount === 1 ? "other" : "others"}`; } function toProjectRootDirectory(projectPath, selectedPath) { const rootDirectory = normalizePath2(relative(projectPath, selectedPath)); return rootDirectory === "" ? null : rootDirectory; } async function promptForInferredServicesSetup({ client, autoConfirm, nonInteractive, workPath, inferred, inferredWriteBlocker, allowChooseDifferentProjectDirectory = false }) { if (!inferred) { return null; } if (inferredWriteBlocker) { output_manager_default.warn( `Multiple services were detected, but your existing project config uses \`${inferredWriteBlocker}\`. To deploy multiple services in one project, see ${output_manager_default.link("Services", SERVICES_DOCS_URL)}.` ); return null; } displayDetectedServices(inferred.services); let choice = null; if (autoConfirm) { choice = { type: "services" }; } else if (!nonInteractive) { const webServices = inferred.services.filter( (service) => service.type === "web" ); const choices = [ { name: `Set up project with all detected services: ${formatDetectedServicesSummary( inferred.services )}`, value: "services" }, ...webServices.map((service, index) => ({ name: `Set up project with "${service.name}"`, value: `single-app:${index}` })), ...allowChooseDifferentProjectDirectory ? [ { name: "Choose a different root directory", value: "project-directory" } ] : [] ]; const selected = await client.input.select({ message: INFERRED_SERVICES_PROMPT, choices }); if (selected === "services") { choice = { type: "services" }; } else if (selected === "project-directory") { choice = { type: "project-directory" }; } else if (typeof selected === "string" && selected.startsWith("single-app:")) { const index = Number.parseInt(selected.slice("single-app:".length), 10); const service = webServices[index]; if (service) { choice = { type: "single-app", selectedPath: service.workspace === "." ? workPath : join(workPath, service.workspace) }; } } } if (choice?.type !== "services") { return choice; } const { configFileName } = await writeServicesConfig( workPath, inferred.config ); output_manager_default.print(` Added services configuration to ${configFileName}. `); return { type: "services" }; } // src/util/projects/search-project-across-teams.ts var import_slugify2 = __toESM(require_slugify(), 1); import { relative as relative2 } from "path"; async function searchProjectAcrossTeams(client, projectName, cwd, { autoConfirm = false, nonInteractive = false, teams, skipLimited, gitProjectName } = {}) { const teamsToSearch = teams ?? await getTeams(client); const shouldSkipLimited = skipLimited ?? true; const accessibleTeams = []; const skippedTeams = []; const skippedSlugs = []; for (const t of teamsToSearch) { if (shouldSkipLimited && t.limited) { skippedTeams.push(t); skippedSlugs.push(t.slug); } else { accessibleTeams.push(t); } } if (skippedSlugs.length > 0) { output_manager_default.debug( `Skipping limited teams during cross-team project search: ${skippedSlugs.join(", ")}` ); } const searchedTeamSlugs = accessibleTeams.map((team) => team.slug); const orgs = accessibleTeams.map((t) => ({ type: "team", id: t.id, slug: t.slug })); const repoMatchesPromise = searchProjectsByRepoRoot({ client, cwd, gitProjectName, orgs, autoConfirm, nonInteractive }); const slugifiedName = (0, import_slugify2.default)(projectName); const searchNames = [projectName]; if (slugifiedName !== projectName) { searchNames.push(slugifiedName); } const folderNameSearchPromises = orgs.flatMap( (org) => searchNames.map( (name) => getProjectByNameOrId(client, name, org.id).then( (result) => result instanceof ProjectNotFound ? null : { project: result, org, reason: "folder-name" } ).catch(() => null) ) ); const [repoMatches, folderNameMatches] = await Promise.all([ repoMatchesPromise, Promise.all(folderNameSearchPromises) ]); const results = [...repoMatches, ...folderNameMatches]; const seen = /* @__PURE__ */ new Set(); const matches = []; for (const r of results) { if (r && r.project.id && !seen.has(r.project.id)) { seen.add(r.project.id); matches.push(r); } } return { matches, searchedTeamSlugs, skippedLimitedTeamSlugs: skippedSlugs, skippedLimitedTeams: skippedTeams }; } async function searchProjectsByRepoRoot({ client, cwd, gitProjectName, orgs, autoConfirm, nonInteractive }) { const rootPath = await findRepoRoot(cwd); if (!rootPath) { return []; } let remote; try { remote = await resolveGitRemote(client, rootPath, { yes: autoConfirm || nonInteractive }); } catch (error) { output_manager_default.debug( `Failed to resolve Git remote for cross-team search: ${error}` ); return []; } if (!remote) { return []; } const relativePath = relative2(rootPath, cwd); const results = await Promise.all( orgs.map(async (org) => { try { const projects = await fetchProjectsForRepoUrl( client, remote.repoUrl, org.id ); const repoProjectConfigs = projects.filter( (project) => !gitProjectName || project.id === gitProjectName || project.name === gitProjectName ).map((project) => ({ id: project.id, name: project.name, directory: project.rootDirectory || ".", orgId: org.id })); const matchingProjects = findProjectsFromPath( repoProjectConfigs, relativePath ); return matchingProjects.map((match) => { const project = projects.find((p) => p.id === match.id); if (!project) { return null; } return { project, org, reason: "repo-root", repo: { ...remote, directory: match.directory } }; }).filter(Boolean); } catch (error) { output_manager_default.debug( `Failed to search Git-linked projects under ${org.slug}: ${error}` ); return []; } }) ); return results.flat(); } // src/util/link/setup-and-link.ts function formatMatchReason(match) { if (match.reason === "repo-root") { return import_chalk6.default.gray("(linked by git)"); } return import_chalk6.default.gray("(folder name)"); } function formatCrossTeamMatch(match) { return `${import_chalk6.default.bold(match.org.slug)}/${match.project.name} ${formatMatchReason( match )}`; } function formatTeamList(slugs) { const shown = slugs.slice(0, 5); const suffix = slugs.length > shown.length ? `, and ${slugs.length - shown.length} more` : ""; return `${shown.join(", ")}${suffix}`; } function printCrossTeamSearchScope({ searchedTeamSlugs, skippedLimitedTeamSlugs }) { if (searchedTeamSlugs.length > 0) { output_manager_default.print(` Searched teams: ${formatTeamList(searchedTeamSlugs)} `); } if (skippedLimitedTeamSlugs.length > 0) { output_manager_default.print( ` Skipped ${skippedLimitedTeamSlugs.length} SSO-protected ${skippedLimitedTeamSlugs.length === 1 ? "team" : "teams"} ` ); } } function isErrnoException(err) { return err instanceof Error && typeof err.code === "string"; } async function hasWorkspaces(cwd) { try { const fs = new import_fs_detectors2.LocalFileSystemDetector(cwd); const workspaces = await (0, import_fs_detectors2.getWorkspaces)({ fs }); return workspaces.length > 0; } catch (err) { if (isErrnoException(err) && err.code && ["ENOENT", "EACCES", "ENOTDIR"].includes(err.code)) { output_manager_default.debug(`getWorkspaces failed for ${cwd}: ${err}`); return false; } throw err; } } async function shouldPromptForRootDirectory(opts) { if (opts.servicesChoice?.type === "project-directory") { return true; } if (await hasWorkspaces(opts.path)) { return true; } try { const detected = await detectProjects(opts.path); const frameworksAtRoot = detected.get("") ?? []; return frameworksAtRoot.length === 0; } catch (err) { output_manager_default.debug(`detectProjects failed at root: ${err}`); return true; } } async function maybePullEnvAfterLink(client, path2, autoConfirm, pullEnv) { if (!pullEnv || !client.stdin.isTTY || client.nonInteractive) { return; } const pullEnvConfirmed = autoConfirm || await client.input.confirm( "Would you like to pull environment variables now?", true ); if (!pullEnvConfirmed) { return; } const originalCwd = client.cwd; try { client.cwd = path2; const args = autoConfirm ? ["--yes"] : []; const exitCode = await pull(client, args, "vercel-cli:link"); if (exitCode !== 0) { output_manager_default.error( "Failed to pull environment variables. You can run `vc env pull` manually." ); } } catch (_error) { output_manager_default.error( "Failed to pull environment variables. You can run `vc env pull` manually." ); } finally { client.cwd = originalCwd; } } async function linkCrossTeamMatch({ client, path: path2, match, successEmoji, autoConfirm, pullEnv }) { client.config.currentTeam = match.org.type === "team" ? match.org.id : void 0; if (match.reason === "repo-root" && match.repo) { await linkRepoProject(client, path2, { project: match.project, orgId: match.org.id, orgSlug: match.org.slug, remoteName: match.repo.remoteName, successEmoji }); await maybePullEnvAfterLink(client, path2, autoConfirm, pullEnv); return { status: "linked", org: match.org, project: match.project, repoRoot: match.repo.rootPath }; } await linkFolderToProject( client, path2, { projectId: match.project.id, orgId: match.org.id }, match.project.name, match.org.slug, successEmoji, autoConfirm, pullEnv ); return { status: "linked", org: match.org, project: match.project }; } async function promptForLimitedTeams(client, teams) { if (teams.length === 0) { return []; } return await client.input.checkbox({ message: "Which SSO-protected teams should be searched?", choices: teams.map((team) => ({ name: team.name ? `${team.name} (${team.slug})` : team.slug, value: team })) }); } async function searchSelectedLimitedTeams({ client, path: path2, projectName, gitProjectName, teams }) { const selectedTeams = await promptForLimitedTeams(client, teams); if (selectedTeams.length === 0) { return []; } output_manager_default.spinner("Searching selected SSO-protected teams\u2026", 1e3); try { const result = await searchProjectAcrossTeams(client, projectName, path2, { teams: selectedTeams, skipLimited: false, gitProjectName }); printCrossTeamSearchScope({ searchedTeamSlugs: result.searchedTeamSlugs, skippedLimitedTeamSlugs: [] }); return result.matches; } catch (err) { output_manager_default.debug(`Selected SSO-protected team search failed: ${err}`); return []; } finally { output_manager_default.stopSpinner(); } } async function linkCrossTeamMatches({ client, path: path2, matches, successEmoji, autoConfirm, nonInteractive, pullEnv }) { if (matches.length === 0) { return null; } if (matches.length === 1) { const match = matches[0]; if (autoConfirm || nonInteractive) { return await linkCrossTeamMatch({ client, path: path2, match, successEmoji, autoConfirm, pullEnv }); } const confirmed = await client.input.confirm( `Found project ${formatCrossTeamMatch(match)}. Link to it?`, true ); if (confirmed) { return await linkCrossTeamMatch({ client, path: path2, match, successEmoji, autoConfirm, pullEnv }); } return null; } const currentTeamMatch = matches.find( (match) => match.org.id === client.config.currentTeam ); if (autoConfirm && currentTeamMatch) { return await linkCrossTeamMatch({ client, path: path2, match: currentTeamMatch, successEmoji, autoConfirm, pullEnv }); } if (nonInteractive) { return null; } const choices = matches.map((match) => ({ name: formatCrossTeamMatch(match), value: match })); choices.push({ name: "Not one of these projects", value: null }); const selected = await client.input.select({ message: "Found matching projects across teams. Which one do you want to link?", choices, default: currentTeamMatch ?? void 0 }); if (!selected) { return null; } return await linkCrossTeamMatch({ client, path: path2, match: selected, successEmoji, autoConfirm, pullEnv }); } async function setupAndLink(client, path2, { autoConfirm = false, forceDelete = false, link, successEmoji = "link", setupMsg = "Set up", projectName, nonInteractive = false, pullEnv = true, v0, searchAcrossTeams = false }) { const { config } = client; const gitProjectName = projectName; projectName = projectName ?? basename(path2); if (!isDirectory(path2)) { output_manager_default.error(`Expected directory but found file: ${path2}`); return { status: "error", exitCode: 1, reason: "PATH_IS_FILE" }; } if (!link) { link = await getLinkedProject(client, path2); } const isTTY = client.stdin.isTTY; let rootDirectory = null; let newProjectName; let org; if (!forceDelete && link.status === "linked") { return link; } if (forceDelete) { const vercelDir = getVercelDirectory(path2); (0, import_fs_extra2.remove)(join2(vercelDir, VERCEL_DIR_README)); (0, import_fs_extra2.remove)(join2(vercelDir, VERCEL_DIR_PROJECT)); } if (!isTTY && !autoConfirm && !nonInteractive) { return { status: "error", exitCode: 1, reason: "HEADLESS" }; } output_manager_default.print( ` ${import_chalk6.default.bold(setupMsg)} ${import_chalk6.default.dim(`"${humanizePath(path2)}"`)} ` ); let skipAutoDetect = false; if (searchAcrossTeams) { let crossTeamMatches = []; let searchedTeamSlugs = []; let skippedLimitedTeamSlugs = []; let skippedLimitedTeams = []; output_manager_default.spinner("Searching for existing projects\u2026", 1e3); try { const searchResult = await searchProjectAcrossTeams( client, projectName, path2, { autoConfirm, nonInteractive, gitProjectName } ); crossTeamMatches = searchResult.matches; searchedTeamSlugs = searchResult.searchedTeamSlugs; skippedLimitedTeamSlugs = searchResult.skippedLimitedTeamSlugs; skippedLimitedTeams = searchResult.skippedLimitedTeams; } catch (err) { output_manager_default.debug(`Cross-team search failed: ${err}`); } finally { output_manager_default.stopSpinner(); } if (crossTeamMatches.length > 0 && !autoConfirm && !nonInteractive) { printCrossTeamSearchScope({ searchedTeamSlugs, skippedLimitedTeamSlugs }); } const linkedMatch = await linkCrossTeamMatches({ client, path: path2, matches: crossTeamMatches, successEmoji, autoConfirm, nonInteractive, pullEnv }); if (linkedMatch) { return linkedMatch; } if (!autoConfirm && !nonInteractive && skippedLimitedTeams.length > 0) { if (crossTeamMatches.length === 0) { output_manager_default.print( ` No matching projects found in the ${searchedTeamSlugs.length} ${searchedTeamSlugs.length === 1 ? "team" : "teams"} available in your current session. ` ); } const limitedTeamMatches = await searchSelectedLimitedTeams({ client, path: path2, projectName, gitProjectName, teams: skippedLimitedTeams }); const linkedLimitedMatch = await linkCrossTeamMatches({ client, path: path2, matches: limitedTeamMatches, successEmoji, autoConfirm, nonInteractive, pullEnv }); if (linkedLimitedMatch) { return linkedLimitedMatch; } if (limitedTeamMatches.length === 0) { output_manager_default.print( " No matching projects found in the selected SSO-protected teams.\n" ); } skipAutoDetect = skipAutoDetect || crossTeamMatches.length > 0 || limitedTeamMatches.length > 0; } else if (crossTeamMatches.length > 0) { skipAutoDetect = true; } } try { org = await selectOrg(client, "Which team?", autoConfirm); } catch (err) { if (isAPIError(err)) { if (err.code === "NOT_AUTHORIZED") { output_manager_default.prettyError(err); return { status: "error", exitCode: 1, reason: "NOT_AUTHORIZED" }; } if (err.code === "TEAM_DELETED") { output_manager_default.prettyError(err); return { status: "error", exitCode: 1, reason: "TEAM_DELETED" }; } } throw err; } let projectOrNewProjectName; try { projectOrNewProjectName = await inputProject( client, org, projectName, autoConfirm, skipAutoDetect ); } catch (err) { if (err instanceof Error && err.code === "HEADLESS") { return { status: "error", exitCode: 1, reason: "HEADLESS" }; } throw err; } if (typeof projectOrNewProjectName === "string") { newProjectName = projectOrNewProjectName; } else { const project = projectOrNewProjectName; await linkFolderToProject( client, path2, { projectId: project.id, orgId: org.id }, project.name, org.slug, successEmoji, autoConfirm, pullEnv ); return { status: "linked", org, project }; } config.currentTeam = org.type === "team" ? org.id : void 0; const rootServicesSetup = await getServicesSetupState(path2); const configFileName = await findSourceVercelConfigFile(path2) ?? "vercel.json"; try { let settings = {}; let pathWithRootDirectory = path2; let rootInferredServicesChoice = null; if (!rootServicesSetup.hasConfiguredServices) { rootInferredServicesChoice = await promptForInferredServicesSetup({ client, autoConfirm, nonInteractive, workPath: path2, inferred: rootServicesSetup.inferredServices, inferredWriteBlocker: rootServicesSetup.inferredServicesWriteBlocker, allowChooseDifferentProjectDirectory: true }); } if (rootServicesSetup.hasConfiguredServices) { displayConfiguredServicesSetup( rootServicesSetup.detectServicesResult, configFileName ); settings.framework = "services"; } else if (rootInferredServicesChoice?.type === "services") { settings.framework = "services"; } else { const skipSelectedRootInferredServicesPrompt = rootInferredServicesChoice?.type === "single-app"; if (rootInferredServicesChoice?.type === "single-app") { rootDirectory = toProjectRootDirectory( path2, rootInferredServicesChoice.selectedPath ); } else { const shouldPromptRoot = await shouldPromptForRootDirectory({ path: path2, servicesChoice: rootInferredServicesChoice }); if (shouldPromptRoot) { rootDirectory = await inputRootDirectory(client, path2, autoConfirm); if (rootDirectory && !await validateRootDirectory(path2, join2(path2, rootDirectory))) { return { status: "error", exitCode: 1, reason: "INVALID_ROOT_DIRECTORY" }; } } } pathWithRootDirectory = rootDirectory ? join2(path2, rootDirectory) : path2; const selectedRootServicesSetup = pathWithRootDirectory === path2 ? null : await getServicesSetupState(pathWithRootDirectory); let selectedRootInferredServicesChoice = null; if (!skipSelectedRootInferredServicesPrompt) { selectedRootInferredServicesChoice = await promptForInferredServicesSetup({ client, autoConfirm, nonInteractive, workPath: pathWithRootDirectory, inferred: selectedRootServicesSetup?.inferredServices ?? null, inferredWriteBlocker: selectedRootServicesSetup?.inferredServicesWriteBlocker ?? null }); } if (selectedRootServicesSetup?.hasConfiguredServices) { displayConfiguredServicesSetup( selectedRootServicesSetup.detectServicesResult, configFileName ); settings.framework = "services"; } else if (selectedRootInferredServicesChoice?.type === "services") { settings.framework = "services"; } else { if (selectedRootInferredServicesChoice?.type === "single-app") { rootDirectory = toProjectRootDirectory( path2, selectedRootInferredServicesChoice.selectedPath ); pathWithRootDirectory = rootDirectory ? join2(path2, rootDirectory) : path2; } const localConfig = await readConfig(pathWithRootDirectory); if (localConfig instanceof CantParseJSONFile) { output_manager_default.prettyError(localConfig); return { status: "error", exitCode: 1 }; } const isZeroConfig = !localConfig || !localConfig.builds || localConfig.builds.length === 0; if (isZeroConfig) { const localConfigurationOverrides = { buildCommand: localConfig?.buildCommand, devCommand: localConfig?.devCommand, framework: localConfig?.framework, commandForIgnoringBuildStep: localConfig?.ignoreCommand, installCommand: localConfig?.installCommand, outputDirectory: localConfig?.outputDirectory }; const detectedProjectsForWorkspace = await detectProjects( pathWithRootDirectory ); const detectedProjects = detectedProjectsForWorkspace.get("") || []; const framework = detectedProjects[0] ?? import_frameworks3.frameworkList.find((f) => f.slug === null); settings = await editProjectSettings( client, {}, framework, autoConfirm, localConfigurationOverrides, configFileName ); } } } let changeAdditionalSettings = false; if (!autoConfirm) { changeAdditionalSettings = await client.input.confirm( "Do you want to change additional project settings?", false ); } let vercelAuthSetting = DEFAULT_VERCEL_AUTH_SETTING; if (changeAdditionalSettings) { vercelAuthSetting = await vercelAuth(client, { autoConfirm }); } if (rootDirectory) { settings.rootDirectory = rootDirectory; } const project = await createProject(client, { ...settings, name: newProjectName, vercelAuth: vercelAuthSetting, v0 }); await linkFolderToProject( client, path2, { projectId: project.id, orgId: org.id }, project.name, org.slug, successEmoji, autoConfirm, false // don't prompt to pull env for newly created projects ); await connectGitRepository(client, path2, project, autoConfirm, org); return { status: "linked", org, project }; } catch (err) { if (isAPIError(err) && err.code === "too_many_projects") { output_manager_default.prettyError(err); return { status: "error", exitCode: 1, reason: "TOO_MANY_PROJECTS" }; } if (err instanceof Error && err.code === "HEADLESS") { return { status: "error", exitCode: 1, reason: "HEADLESS" }; } printError(err); return { status: "error", exitCode: 1 }; } } async function connectGitRepository(client, path2, project, autoConfirm, org) { try { const gitConfig = await parseGitConfig(join2(path2, ".git/config")); if (!gitConfig) { return; } const remoteUrls = pluckRemoteUrls(gitConfig); if (!remoteUrls || Object.keys(remoteUrls).length === 0) { return; } const shouldConnect = autoConfirm || await client.input.confirm( `Detected a repository. Connect it to this project?`, true ); if (!shouldConnect) { return; } const repoInfo = await selectAndParseRemoteUrl(client, remoteUrls); if (!repoInfo) { return; } await checkExistsAndConnect({ client, confirm: autoConfirm, gitProviderLink: project.link, org, gitOrg: repoInfo.org, project, // Type assertion since we only need the id provider: repoInfo.provider, repo: repoInfo.repo, repoPath: `${repoInfo.org}/${repoInfo.repo}` }); } catch (error) { output_manager_default.debug(`Failed to connect git repository: ${error}`); } } export { validateRootDirectory, validatePaths, readConfig, displayDetectedServices, setupAndLink };