Skip to content
Merged
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
40 changes: 9 additions & 31 deletions packages/eve/src/cli/commands/integration-setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import { createFakePrompter } from "#internal/testing/fake-prompter.js";
import type { AddChannelsDeps } from "#setup/integrations/channels/setup.js";
import { deriveSlackConnectorSlug } from "#setup/scaffold/index.js";
import { runIntegrationSetup } from "#setup/integrations/runner.js";

import { runIntegrationSetupCommand } from "./integration-setup.js";
import type { RegistryCommandLogger } from "./registry.js";
Expand All @@ -13,41 +12,22 @@ vi.mock("#setup/scaffold/index.js", async (importOriginal) => ({
...(await importOriginal<typeof import("#setup/scaffold/index.js")>()),
isEveProject,
}));
vi.mock("#setup/integrations/runner.js", () => ({ runIntegrationSetup: vi.fn() }));

function logger(): RegistryCommandLogger & { errors: string[] } {
const errors: string[] = [];
return { errors, error: (message) => errors.push(message), log: () => {} };
}

function addChannelsDeps(): AddChannelsDeps {
return {
ensureChannel: vi.fn<AddChannelsDeps["ensureChannel"]>(async (options) => ({
kind: "web",
action: "created",
filesWritten: [`${options.projectRoot}/app/page.tsx`],
filesSkipped: [],
packageJsonUpdated: [],
})),
deriveSlackConnectorSlug,
provisionSlackbot: vi.fn(),
reconcileSlackUid: vi.fn(async () => true),
detectPackageManager: vi.fn<AddChannelsDeps["detectPackageManager"]>(async () => ({
kind: "pnpm",
source: "default",
})),
runPackageManagerInstall: vi.fn(async () => true),
ensureVercelProject: vi.fn(async () => ({ orgId: "team-id", projectId: "project-id" })),
};
}

afterEach(() => {
process.exitCode = undefined;
vi.clearAllMocks();
});

describe("runIntegrationSetupCommand", () => {
it("runs registry-owned setup without mutating or installing dependencies", async () => {
it("delegates registry-owned setup to the integration runner", async () => {
vi.mocked(runIntegrationSetup).mockResolvedValue({ kind: "done" });
const output = logger();
const deps = addChannelsDeps();
const fake = createFakePrompter();

await runIntegrationSetupCommand(
Expand All @@ -57,16 +37,14 @@ describe("runIntegrationSetupCommand", () => {
{},
{
createPrompter: () => fake.prompter,
detectDeployment: vi.fn(async () => ({ state: "unlinked" as const })),
getVercelAuthStatus: vi.fn(async () => "cli-missing" as const),
addChannelsDeps: deps,
},
);

expect(deps.ensureChannel).toHaveBeenCalledWith(
expect.objectContaining({ kind: "web", skipDependencyMutation: true }),
expect(runIntegrationSetup).toHaveBeenCalledWith(
"web",
expect.objectContaining({ appRoot: "/project", prompter: fake.prompter }),
undefined,
);
expect(deps.runPackageManagerInstall).not.toHaveBeenCalled();
expect(output.errors).toEqual([]);
});
});
72 changes: 18 additions & 54 deletions packages/eve/src/cli/commands/integration-setup.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { interactiveAsker } from "#setup/ask.js";
import type { AddChannelsDeps } from "#setup/integrations/channels/setup.js";
import {
channelSetupEnvironment,
describeChannelSetupEnvironment,
} from "#setup/integrations/channels/environment.js";
import {
channelSetupIntegration,
createChannelSetupUi,
} from "#setup/integrations/channels/index.js";
import { detectDeployment, projectResolutionFromDeployment } from "#setup/project-resolution.js";
import { createPrompter, type Prompter } from "#setup/prompter.js";
import { createRegistrySetupClient } from "#setup/registry-setup-client.js";
import { isEveProject, type ChannelKind } from "#setup/scaffold/index.js";
import { createDefaultSetupState } from "#setup/state.js";
import { getVercelAuthStatus } from "#setup/vercel-project.js";
import {
runIntegrationSetup,
type IntegrationSetupRunnerDeps,
} from "#setup/integrations/runner.js";
import { isEveProject } from "#setup/scaffold/index.js";

import { NOT_AN_AGENT_MESSAGE } from "./preconditions.js";
import type { RegistryCommandLogger } from "./registry.js";
Expand All @@ -25,17 +16,12 @@ export interface IntegrationSetupOptions {

export interface IntegrationSetupDependencies {
createPrompter?: () => Prompter;
detectDeployment: typeof detectDeployment;
getVercelAuthStatus: typeof getVercelAuthStatus;
addChannelsDeps?: AddChannelsDeps;
runnerDeps?: IntegrationSetupRunnerDeps;
}

const defaultIntegrationSetupDependencies: IntegrationSetupDependencies = {
detectDeployment,
getVercelAuthStatus,
};
const defaultIntegrationSetupDependencies: IntegrationSetupDependencies = {};

/** Runs a built-in integration setup after its registry payload is installed. */
/** Runs built-in integration setup after its registry payload is installed. */
export async function runIntegrationSetupCommand(
logger: RegistryCommandLogger,
appRoot: string,
Expand All @@ -51,46 +37,24 @@ export async function runIntegrationSetupCommand(

const client = createRegistrySetupClient({ signal: options.signal });
try {
if (kind !== "slack" && kind !== "web") {
throw new Error(
`Integration setup "${kind}" is not available in this version of eve. Upgrade eve and try again.`,
);
}
const channelKind: ChannelKind = kind;
const prompter = client?.prompter ?? dependencies.createPrompter?.() ?? createPrompter();
const signal = client?.signal ?? options.signal;
const integration = channelSetupIntegration(channelKind);
prompter.intro(`Set up ${integration.label}`);
prompter.log.message("Checking Vercel setup...");
const [deployment, authStatus] = await Promise.all([
dependencies.detectDeployment(appRoot, { signal }),
dependencies.getVercelAuthStatus(appRoot, { signal }),
]);
const project = projectResolutionFromDeployment(deployment);
const environment = channelSetupEnvironment(authStatus, project);
prompter.log.info(describeChannelSetupEnvironment(environment));
const result = await integration.setup({
environment,
state: {
...createDefaultSetupState(),
project,
projectPath: { kind: "resolved", inPlace: true, path: appRoot },
channelSelection: [channelKind],
const result = await runIntegrationSetup(
kind,
{
appRoot,
prompter,
signal: client?.signal ?? options.signal,
yes: options.yes,
},
ui: createChannelSetupUi({ asker: interactiveAsker(prompter), prompter }),
presetCreateSlackbot: options.yes ? true : undefined,
presetPortableCredentials: options.yes ? true : undefined,
skipDependencyMutation: true,
deps: dependencies.addChannelsDeps,
signal,
});
dependencies.runnerDeps,
);
if (result.kind === "cancelled") {
client?.cancel();
if (process.env.EVE_SETUP === "1") process.exitCode = 130;
return;
}
prompter.outro("Integration set up.");
client?.complete();
client?.complete(result.facts);
} catch (error) {
client?.fail(error);
logger.error(error instanceof Error ? error.message : String(error));
Expand Down
38 changes: 0 additions & 38 deletions packages/eve/src/setup/boxes/resolve-provisioning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,29 +408,6 @@ describe("resolveProvisioning box", () => {
expect(deps.requireAuth).toHaveBeenCalled();
});

it("resolves to Vercel without asking when Slack was selected earlier", async () => {
const deps = fakeDeps();
// Only the project sub-question is asked; the where-to-run select would
// consume a value the prompter does not have, so reaching it throws.
const prompter = createPrompter({ selectValues: ["new"] });
const box = makeBox({
prompter,
targetDirectory: "/tmp/parent",
mode: { headless: false },
deps,
});
const state: SetupState = { ...stateWithAgentName("my-agent"), channelSelection: ["slack"] };

const result = await runInteractive([box], state, silentSink);

expect(result.kind).toBe("done");
if (result.kind !== "done") return;
expect(result.state.vercelProject).toEqual({ kind: "new", project: "my-agent", team: "team" });
expect(prompter.log.info).toHaveBeenCalledWith(
"Slack needs a public URL, so your agent will run on Vercel.",
);
});

it("resolves to Vercel without asking when a Connect-backed connection was selected", async () => {
const deps = fakeDeps();
const prompter = createPrompter({ selectValues: ["new"] });
Expand Down Expand Up @@ -484,21 +461,6 @@ describe("resolveProvisioning box", () => {
expect(provider?.hint).toBe("OPENAI_API_KEY");
});

it("headless: refuses --skip-vercel against a Slack selection before any effect", async () => {
const deps = fakeDeps();
const box = makeBox({
prompter: createPrompter(),
targetDirectory: "/tmp/parent",
mode: { headless: true, project: { skipVercel: true }, aiGateway: {} },
deps,
});
const state: SetupState = { ...stateWithAgentName("my-agent"), channelSelection: ["slack"] };

await expect(runHeadless([box], state, silentSink)).rejects.toThrow(
"Slack requires a Vercel project. Remove --skip-vercel to add Slack.",
);
});

it("headless: refuses --skip-vercel against a Connect-backed connection selection", async () => {
const deps = fakeDeps();
const box = makeBox({
Expand Down
8 changes: 0 additions & 8 deletions packages/eve/src/setup/boxes/resolve-provisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,11 @@ export interface ResolvedProvisioning {
* flags.
*/
interface VercelDemands {
slack: boolean;
connectSlugs: string[];
}

function vercelDemands(state: Readonly<SetupState>): VercelDemands {
return {
slack: state.channelSelection.includes("slack"),
connectSlugs: state.connectionSelection
.filter((plan) => plan.entry.auth?.kind === "connect")
.map((plan) => plan.slug),
Expand All @@ -127,9 +125,6 @@ function connectClause(connectSlugs: readonly string[]): string {
/** The reasons behind a forced Vercel resolution; empty when free to choose. */
function vercelRequirements(demands: VercelDemands): string[] {
const reasons: string[] = [];
if (demands.slack) {
reasons.push("Slack needs a public URL");
}
if (demands.connectSlugs.length > 0) {
reasons.push(connectClause(demands.connectSlugs));
}
Expand Down Expand Up @@ -438,9 +433,6 @@ export function resolveProvisioning(
// resolves to Vercel for those selections.
if (plans.vercelProject.kind === "none") {
const demands = vercelDemands(state);
if (demands.slack) {
throw new Error("Slack requires a Vercel project. Remove --skip-vercel to add Slack.");
}
if (demands.connectSlugs.length > 0) {
throw new Error(
`${connectClause(demands.connectSlugs)}, which needs a Vercel project. Remove --skip-vercel to add ${demands.connectSlugs.length === 1 ? "it" : "them"}.`,
Expand Down
74 changes: 0 additions & 74 deletions packages/eve/src/setup/integrations/channels/index.test.ts

This file was deleted.

19 changes: 0 additions & 19 deletions packages/eve/src/setup/integrations/channels/index.ts

This file was deleted.

33 changes: 0 additions & 33 deletions packages/eve/src/setup/integrations/channels/runner.ts

This file was deleted.

Loading
Loading