diff --git a/.changeset/tidy-connector-names.md b/.changeset/tidy-connector-names.md new file mode 100644 index 000000000..0e650d453 --- /dev/null +++ b/.changeset/tidy-connector-names.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +New Vercel Connect setup names include the project and service type, such as `my-agent-slack` and `my-agent-linear-mcp`, to avoid collisions with bare project or connection names. diff --git a/packages/eve/src/setup/boxes/add-connections.test.ts b/packages/eve/src/setup/boxes/add-connections.test.ts index 135e3596f..294d15797 100644 --- a/packages/eve/src/setup/boxes/add-connections.test.ts +++ b/packages/eve/src/setup/boxes/add-connections.test.ts @@ -51,6 +51,9 @@ function makeBoxes( function createDeps() { return { + deriveConnectorProjectSlug: vi.fn( + async () => "my-agent", + ), ensureConnection: vi.fn(async (options) => ({ slug: options.slug ?? options.entry.slug, protocol: options.protocol, @@ -129,7 +132,7 @@ describe("selectConnections + addConnections boxes", () => { // since `vercel connect create` opens a browser. expect(deps.setupConnectionConnector).not.toHaveBeenCalled(); expect(prompter.log.info).toHaveBeenCalledWith( - "Run `vercel connect create mcp.linear.app --name linear`, then set the connector UID in agent/connections/linear.ts.", + "Run `vercel connect create mcp.linear.app --name my-agent-linear-mcp`, then set the connector UID in agent/connections/linear.ts.", ); }); @@ -146,6 +149,7 @@ describe("selectConnections + addConnections boxes", () => { expect(deps.setupConnectionConnector).toHaveBeenCalledWith( expect.objectContaining({ slug: "linear", + connectorName: "my-agent-linear-mcp", service: "mcp.linear.app", projectRoot: "/tmp/project", }), diff --git a/packages/eve/src/setup/boxes/add-connections.ts b/packages/eve/src/setup/boxes/add-connections.ts index ff360ef73..288ef7a00 100644 --- a/packages/eve/src/setup/boxes/add-connections.ts +++ b/packages/eve/src/setup/boxes/add-connections.ts @@ -1,4 +1,5 @@ import { + deriveSlackConnectorSlug, ensureConnection, listAuthoredConnections, type ConnectionInput, @@ -10,6 +11,7 @@ import { cleanupCreatedConnectionConnector, setupConnectionConnector, } from "../connection-connector.js"; +import { connectConnectorName } from "../connect-connector-name.js"; import { canonicalConnectorNameForEntry } from "../scaffold/connections/catalog.js"; import { isProjectResolved, @@ -25,6 +27,7 @@ import { CONNECT_REQUIRES_VERCEL } from "./select-connections.js"; /** Injected for tests; defaults to the real scaffold and Connect effects. */ export interface AddConnectionsDeps { + deriveConnectorProjectSlug: (projectRoot: string, projectNameHint?: string) => Promise; ensureConnection: typeof ensureConnection; listAuthoredConnections: typeof listAuthoredConnections; readProjectLink: typeof readProjectLink; @@ -68,6 +71,7 @@ export function addConnections( options: AddConnectionsOptions, ): SetupBox { const deps = options.deps ?? { + deriveConnectorProjectSlug: deriveSlackConnectorSlug, ensureConnection, listAuthoredConnections, readProjectLink, @@ -93,6 +97,14 @@ export function addConnections( const noVercel = !hasVercelProject(state); const project = state.project; const authored = new Set(await deps.listAuthoredConnections(projectRoot)); + let connectorProjectSlug: string | undefined; + const getConnectorProjectSlug = async (): Promise => { + connectorProjectSlug ??= await deps.deriveConnectorProjectSlug( + projectRoot, + state.agentName, + ); + return connectorProjectSlug; + }; for (const plan of state.connectionSelection) { if (authored.has(plan.slug)) { @@ -115,11 +127,17 @@ export function addConnections( if (canonicalConnectorName === undefined) { throw new Error(`Connection ${plan.slug} has no canonical connector name.`); } + const connectorName = connectConnectorName( + await getConnectorProjectSlug(), + plan.provision.service, + plan.slug, + ); const connector = await deps.setupConnectionConnector({ log, prompter: options.prompter, projectRoot, slug: plan.slug, + connectorName, service: plan.provision.service, canonicalConnectorName, project: await resolveConnectionProject({ @@ -134,11 +152,17 @@ export function addConnections( if (connector.kind === "created") createdConnectorId = connector.connectorId; break; } - case "command-hint": + case "command-hint": { + const connectorName = connectConnectorName( + await getConnectorProjectSlug(), + plan.provision.service, + plan.slug, + ); log.info( - `Run \`vercel connect create ${plan.provision.service} --name ${plan.slug}\`, then set the connector UID in agent/connections/${plan.slug}.ts.`, + `Run \`vercel connect create ${plan.provision.service} --name ${connectorName}\`, then set the connector UID in agent/connections/${plan.slug}.ts.`, ); break; + } case "connect-manual": log.warning( `Could not determine a Connect service for ${plan.slug}. Create the connector manually and set its UID in agent/connections/${plan.slug}.ts.`, diff --git a/packages/eve/src/setup/connect-connector-name.test.ts b/packages/eve/src/setup/connect-connector-name.test.ts new file mode 100644 index 000000000..a46bddf2e --- /dev/null +++ b/packages/eve/src/setup/connect-connector-name.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; + +import { connectConnectorName } from "./connect-connector-name.js"; + +describe("connectConnectorName", () => { + it("qualifies a connector name with its project and Connect service type", () => { + expect(connectConnectorName("my-agent", "slack")).toBe("my-agent-slack"); + expect(connectConnectorName("my-agent", "mcp.linear.app", "linear")).toBe( + "my-agent-linear-mcp", + ); + }); +}); diff --git a/packages/eve/src/setup/connect-connector-name.ts b/packages/eve/src/setup/connect-connector-name.ts new file mode 100644 index 000000000..ed8116a1e --- /dev/null +++ b/packages/eve/src/setup/connect-connector-name.ts @@ -0,0 +1,14 @@ +/** Builds a project- and type-qualified name for a Vercel Connect connector. */ +export function connectConnectorName( + projectSlug: string, + service: string, + connectionSlug?: string, +): string { + const type = service.split(".", 1)[0]; + if (type === undefined || type.length === 0) { + throw new Error(`Invalid Connect service "${service}".`); + } + return connectionSlug === undefined + ? `${projectSlug}-${type}` + : `${projectSlug}-${connectionSlug}-${type}`; +} diff --git a/packages/eve/src/setup/connection-connector.integration.test.ts b/packages/eve/src/setup/connection-connector.integration.test.ts index 25ea23add..39c2a0864 100644 --- a/packages/eve/src/setup/connection-connector.integration.test.ts +++ b/packages/eve/src/setup/connection-connector.integration.test.ts @@ -64,6 +64,7 @@ describe("setupConnectionConnector", () => { prompter, projectRoot, slug: "linear", + connectorName: "my-agent-linear-mcp", service: SERVICE, canonicalConnectorName: CANONICAL_NAME, project: { projectId: "prj_1", orgId: "org_1" }, @@ -208,7 +209,17 @@ describe("setupConnectionConnector", () => { "Could not attach linear/linear-2", ); expect(create).toHaveBeenCalledWith( - ["connect", "create", SERVICE, "--name", "linear-2", "-F", "json", "--scope", "org_1"], + [ + "connect", + "create", + SERVICE, + "--name", + "my-agent-linear-mcp", + "-F", + "json", + "--scope", + "org_1", + ], expect.any(Object), ); expect(run).toHaveBeenLastCalledWith( diff --git a/packages/eve/src/setup/connection-connector.ts b/packages/eve/src/setup/connection-connector.ts index a8aa569f8..7c69badb1 100644 --- a/packages/eve/src/setup/connection-connector.ts +++ b/packages/eve/src/setup/connection-connector.ts @@ -10,6 +10,7 @@ export interface SetupConnectionConnectorOptions { prompter: Prompter; projectRoot: string; slug: string; + connectorName: string; service: string; canonicalConnectorName: string; project: VercelProjectReference; @@ -266,7 +267,7 @@ async function resolveFallbackConnector( const name = ( await options.prompter.text({ message: "New connector name", - defaultValue: nextConnectorName(options.slug, names), + defaultValue: nextConnectorName(options.connectorName, names), validate: (value) => { const normalized = value.trim().toLowerCase(); if (normalized.length === 0) return "A name is required."; diff --git a/packages/eve/src/setup/flows/connections.test.ts b/packages/eve/src/setup/flows/connections.test.ts index bf4bade3d..9b8527565 100644 --- a/packages/eve/src/setup/flows/connections.test.ts +++ b/packages/eve/src/setup/flows/connections.test.ts @@ -34,6 +34,7 @@ function scriptConnectionList(queue: Array) { function addConnectionDeps(): AddConnectionsDeps { return { + deriveConnectorProjectSlug: vi.fn(async () => "agent"), ensureConnection: vi.fn(async (options) => ({ slug: options.slug ?? options.entry.slug, protocol: options.protocol, diff --git a/packages/eve/src/setup/slack-connect-create.ts b/packages/eve/src/setup/slack-connect-create.ts index 0d070562a..80a59de40 100644 --- a/packages/eve/src/setup/slack-connect-create.ts +++ b/packages/eve/src/setup/slack-connect-create.ts @@ -13,6 +13,7 @@ import { CONNECT_LOOKUP_TIMEOUT_MS, fetchSlackConnectorDetails, } from "./slack-connect-lifecycle.js"; +import { connectConnectorName } from "./connect-connector-name.js"; export interface SlackConnectorCreateDeps { captureVercel: typeof captureVercel; @@ -163,7 +164,16 @@ export async function createSlackConnector(input: { }; const createWork = input.phase("Waiting for Slack setup to finish...", () => input.deps.runVercelCaptureStdout( - ["connect", "create", "slack", "--triggers", "--name", input.slug, "-F", "json"], + [ + "connect", + "create", + "slack", + "--triggers", + "--name", + connectConnectorName(input.slug, "slack"), + "-F", + "json", + ], { cwd: input.projectRoot, nonInteractive: true, diff --git a/packages/eve/src/setup/slackbot.test.ts b/packages/eve/src/setup/slackbot.test.ts index 293cb1909..a878ae196 100644 --- a/packages/eve/src/setup/slackbot.test.ts +++ b/packages/eve/src/setup/slackbot.test.ts @@ -567,9 +567,9 @@ describe("provisionSlackbot", () => { }, ); - // Created with --triggers, the slug name, and JSON output for deterministic UID capture. + // Created with --triggers, a service-specific name, and JSON output for deterministic UID capture. expect(mockedRunVercelCaptureStdout).toHaveBeenCalledWith( - ["connect", "create", "slack", "--triggers", "--name", "my-agent", "-F", "json"], + ["connect", "create", "slack", "--triggers", "--name", "my-agent-slack", "-F", "json"], expect.objectContaining({ cwd: "/tmp/eve-agent", nonInteractive: true }), ); expect(mockedCaptureVercel).toHaveBeenCalledWith(