diff --git a/.changeset/bright-walls-sip.md b/.changeset/bright-walls-sip.md new file mode 100644 index 000000000..b4549d67a --- /dev/null +++ b/.changeset/bright-walls-sip.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Prevent interactive setup from hanging after Vercel successfully pulls project environment variables. Environment pulls now run without terminal input and time out safely if the Vercel CLI does not exit. diff --git a/packages/eve/src/setup/boxes/deploy-project.test.ts b/packages/eve/src/setup/boxes/deploy-project.test.ts index 6f59287e3..679d12c0d 100644 --- a/packages/eve/src/setup/boxes/deploy-project.test.ts +++ b/packages/eve/src/setup/boxes/deploy-project.test.ts @@ -9,6 +9,7 @@ import type { Prompter } from "../prompter.js"; import { createDefaultSetupState, type SetupState } from "../state.js"; import type { OutputSink } from "../step.js"; import { runHeadless, runInteractive } from "../runner.js"; +import { VERCEL_ENV_PULL_TIMEOUT_MS } from "../run-vercel-link.js"; import { deployProject, type DeployProjectDeps } from "./deploy-project.js"; const silentSink: OutputSink = { write: () => {} }; @@ -116,6 +117,23 @@ describe("deployProject box", () => { ); }); + it("closes stdin and bounds the env pull after deployment", async () => { + const deps = createDeps(); + const box = headlessBox({ deps }); + + await runHeadless([box], pendingState(), silentSink); + + expect(deps.runVercel).toHaveBeenNthCalledWith( + 2, + ["env", "pull", "--yes"], + expect.objectContaining({ + cwd: "/tmp/project", + nonInteractive: true, + timeoutMs: VERCEL_ENV_PULL_TIMEOUT_MS, + }), + ); + }); + it("deploys with --yes but keeps stdin interactive on an interactive run", async () => { const deps = createDeps(); const box = deployProject({ prompter: createPrompter(), deps }); diff --git a/packages/eve/src/setup/boxes/deploy-project.ts b/packages/eve/src/setup/boxes/deploy-project.ts index 1b24f97c3..b647f39cf 100644 --- a/packages/eve/src/setup/boxes/deploy-project.ts +++ b/packages/eve/src/setup/boxes/deploy-project.ts @@ -13,6 +13,7 @@ import { type ProjectResolution, } from "../project-resolution.js"; import { hasVercelProject, requireProjectPath, type SetupState } from "../state.js"; +import { VERCEL_ENV_PULL_TIMEOUT_MS } from "../run-vercel-link.js"; import type { SetupBox } from "../step.js"; import { syncHostFrameworkPreset } from "../vercel-project-framework.js"; @@ -200,9 +201,13 @@ export function deployProject( () => deps.runVercel(["env", "pull", "--yes"], { cwd: projectPath, - nonInteractive: input.headless, + // `env pull --yes` never needs a prompt. Always detach its stdin: + // the CLI has been observed retaining an inherited terminal after + // writing .env.local, which otherwise leaves init stuck forever. + nonInteractive: true, onOutput, signal, + timeoutMs: VERCEL_ENV_PULL_TIMEOUT_MS, }), ); signal?.throwIfAborted(); diff --git a/packages/eve/src/setup/run-vercel-link.test.ts b/packages/eve/src/setup/run-vercel-link.test.ts new file mode 100644 index 000000000..4a9cac709 --- /dev/null +++ b/packages/eve/src/setup/run-vercel-link.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it, vi } from "vitest"; + +import { runVercel } from "#setup/primitives/index.js"; + +import { runVercelEnvPull, VERCEL_ENV_PULL_TIMEOUT_MS } from "./run-vercel-link.js"; + +vi.mock("#setup/primitives/index.js", () => ({ + runVercel: vi.fn(), +})); + +const mockedRunVercel = vi.mocked(runVercel); + +describe("runVercelEnvPull", () => { + it("closes stdin and bounds the non-interactive env pull", async () => { + mockedRunVercel.mockResolvedValue(true); + const signal = new AbortController().signal; + const onOutput = vi.fn(); + + await expect(runVercelEnvPull("/tmp/agent", onOutput, signal)).resolves.toBe(true); + + expect(mockedRunVercel).toHaveBeenCalledWith(["env", "pull", "--yes"], { + cwd: "/tmp/agent", + onOutput, + signal, + nonInteractive: true, + timeoutMs: VERCEL_ENV_PULL_TIMEOUT_MS, + }); + }); +}); diff --git a/packages/eve/src/setup/run-vercel-link.ts b/packages/eve/src/setup/run-vercel-link.ts index eaa06cf5c..e601952c3 100644 --- a/packages/eve/src/setup/run-vercel-link.ts +++ b/packages/eve/src/setup/run-vercel-link.ts @@ -2,6 +2,9 @@ import { runVercel, type RunVercelOptions } from "#setup/primitives/index.js"; type VercelOutputHandler = NonNullable; +/** Hard deadline for a completed-but-unclosed Vercel CLI env pull. */ +export const VERCEL_ENV_PULL_TIMEOUT_MS = 30_000; + /** * Runs `vercel env pull --yes` inside a linked project so `.env.local` * picks up the latest values, including `VERCEL_OIDC_TOKEN`, for local @@ -13,5 +16,14 @@ export async function runVercelEnvPull( onOutput?: VercelOutputHandler, signal?: AbortSignal, ): Promise { - return runVercel(["env", "pull", "--yes"], { cwd: projectRoot, onOutput, signal }); + return runVercel(["env", "pull", "--yes"], { + cwd: projectRoot, + onOutput, + signal, + // `env pull --yes` has no legitimate prompt. Closing stdin prevents a + // Vercel CLI child that ignores its non-interactive flag from retaining + // eve's TTY after it has written the environment files. + nonInteractive: true, + timeoutMs: VERCEL_ENV_PULL_TIMEOUT_MS, + }); }