Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-walls-sip.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions packages/eve/src/setup/boxes/deploy-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {} };
Expand Down Expand Up @@ -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 });
Expand Down
7 changes: 6 additions & 1 deletion packages/eve/src/setup/boxes/deploy-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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();
Expand Down
29 changes: 29 additions & 0 deletions packages/eve/src/setup/run-vercel-link.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
14 changes: 13 additions & 1 deletion packages/eve/src/setup/run-vercel-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { runVercel, type RunVercelOptions } from "#setup/primitives/index.js";

type VercelOutputHandler = NonNullable<RunVercelOptions["onOutput"]>;

/** 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
Expand All @@ -13,5 +16,14 @@ export async function runVercelEnvPull(
onOutput?: VercelOutputHandler,
signal?: AbortSignal,
): Promise<boolean> {
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,
});
}