From bc4212ca711861a94d67f79a9150755ddbefba35 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 1 May 2026 14:18:09 -0600 Subject: [PATCH 1/2] feat(init): create real app when authed in agent mode When `clerk init` runs in agent mode without `--app` and the user is already authenticated, create a real Clerk application named after the project and link it instead of falling back to keyless setup. Keyless still runs in agent mode when the user is not authenticated. Previously an authed agent run created an unowned keyless app and wrote a deferred-claim breadcrumb, leaving the project unlinked even though the user had logged in before the run; auto-claim only fired on a subsequent `clerk auth login`, which authed users had no reason to re-run. Adds a `createIfMissing` library option to `link` so the agent-mode autolink path can fall through to a deterministic create instead of the existing usage error, and threads a project-name derivation (`package.json#name`, `--name`, or directory basename) through `init`'s authenticated branch. --- .changeset/agent-init-authed-link.md | 5 +++ packages/cli-core/src/commands/init/README.md | 9 +++-- .../cli-core/src/commands/init/index.test.ts | 37 ++++++++++++++++++- packages/cli-core/src/commands/init/index.ts | 35 +++++++++++++----- packages/cli-core/src/commands/link/README.md | 6 +++ .../cli-core/src/commands/link/index.test.ts | 30 +++++++++++++++ packages/cli-core/src/commands/link/index.ts | 20 ++++++++-- packages/cli-core/src/lib/autoclaim.ts | 27 +------------- packages/cli-core/src/lib/project-name.ts | 32 ++++++++++++++++ .../src/test/integration/agent-mode.test.ts | 19 +++++----- 10 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 .changeset/agent-init-authed-link.md create mode 100644 packages/cli-core/src/lib/project-name.ts diff --git a/.changeset/agent-init-authed-link.md b/.changeset/agent-init-authed-link.md new file mode 100644 index 00000000..801ebb36 --- /dev/null +++ b/.changeset/agent-init-authed-link.md @@ -0,0 +1,5 @@ +--- +"clerk": minor +--- + +`clerk init` in agent mode now creates and links a real Clerk application when the user is authenticated, instead of falling back to keyless setup. Keyless still runs in agent mode when the user is not authenticated, but authenticated agent runs leave the project properly linked with real development keys in `.env`. diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index 1d656f30..bd5f6f5d 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -36,7 +36,9 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful - For **existing projects**: framework and package manager are auto-detected, no flags required - For **new projects** (`--starter` or blank directory): `--framework` is required (no way to auto-detect in an empty dir). Package manager is auto-selected by availability (bun → pnpm → yarn → npm) unless `--pm` is provided - Project name defaults to the framework's default (e.g. `my-clerk-next-app`) unless `--name` is provided -- For keyless-capable frameworks with no `--app` and no linked profile, init uses keyless and does not require auth +- For keyless-capable frameworks with no `--app` and no linked profile: + - When **authenticated**, init creates a real Clerk app named after the project (`package.json#name`, `--name`, or directory basename) and links it. No keyless detour, no second `clerk auth login` to claim. + - When **unauthenticated**, init uses keyless and writes a breadcrumb so the next `clerk auth login` claims the app automatically. - For frameworks that require API keys, init will not pick or create an app in agent mode; pass `--app ` or link the project first to pull real keys ## Flow @@ -44,7 +46,8 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful 1. Gathers project context (framework, router variant, TypeScript, `src/` directory, package manager) 2. Determines auth mode: - **Real app target** (`--app` or linked profile): authenticates, links if needed, and pulls real API keys into `.env` - - **Agent + keyless-capable framework + no real app target**: uses keyless mode — the app runs on auto-generated dev keys and the user can connect a Clerk account later with `clerk auth login` + - **Agent + keyless-capable framework + authenticated + no real app target**: creates a real Clerk app named after the project, links it, and pulls real API keys into `.env` + - **Agent + keyless-capable framework + unauthenticated + no real app target**: uses keyless mode — the app runs on auto-generated dev keys and the user can connect a Clerk account later with `clerk auth login` - **Agent + non-keyless framework + no real app target**: scaffolds locally and prints manual setup instructions instead of selecting or creating an app - **Human mode + bootstrap + keyless-capable framework + not authenticated**: uses keyless mode - **Human mode + existing project + not authenticated**: runs the authenticated flow, which triggers an interactive login so real keys can be pulled @@ -81,7 +84,7 @@ Detects the project's framework from `package.json` dependencies (checked top-to | `express` | Express | `@clerk/express` | `CLERK_PUBLISHABLE_KEY` | No | | `fastify` | Fastify | `@clerk/fastify` | `CLERK_PUBLISHABLE_KEY` | No | -The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). In human mode, keyless auto-selection only applies during bootstrap (new projects). In agent mode, keyless-capable frameworks use keyless whenever no real app target is provided by `--app` or a linked profile. For non-keyless frameworks without a real app target, agent mode prints manual setup instructions instead of selecting or creating an app. +The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). In human mode, keyless auto-selection only applies during bootstrap (new projects) when the user is not authenticated. In agent mode, keyless-capable frameworks use keyless only when the user is not authenticated and no real app target is provided by `--app` or a linked profile; an authenticated agent run instead creates a real app named after the project and links it. For non-keyless frameworks without a real app target, agent mode prints manual setup instructions instead of selecting or creating an app. Package manager is detected from lock files: `bun.lockb`/`bun.lock` → bun, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, else npm. diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts index 450a24bd..4e36b988 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -142,6 +142,7 @@ describe("init", () => { skipIfLinked: true, app: "app_abc", cwd: FAKE_CTX.cwd, + createIfMissing: undefined, }); }); @@ -156,6 +157,7 @@ describe("init", () => { skipIfLinked: true, app: "app_abc", cwd: FAKE_CTX.cwd, + createIfMissing: undefined, }); }); @@ -341,8 +343,8 @@ describe("init", () => { expect(linkMod.link).not.toHaveBeenCalled(); }); - test("agent mode with keyless framework uses keyless without an app target", async () => { - setup({ isAgent: true, email: "user@example.com" }); + test("agent mode with keyless framework goes keyless when unauthenticated", async () => { + setup({ isAgent: true, email: null }); const keylessCtx = { ...FAKE_CTX, @@ -362,6 +364,35 @@ describe("init", () => { expect(pullMod.pull).not.toHaveBeenCalled(); }); + test("agent mode with keyless framework + authed creates and links a real app", async () => { + setup({ isAgent: true, email: "user@example.com" }); + + const keylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, + }; + spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); + // Override potential leakage from earlier tests that spy on resolveProfile + // with a non-undefined value but don't track those spies for restoration. + spyOn(config, "resolveProfile").mockResolvedValue(undefined); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], + postInstructions: [], + }); + + await init({}); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalledWith({ + skipIfLinked: true, + app: undefined, + cwd: keylessCtx.cwd, + createIfMissing: expect.any(String), + }); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: keylessCtx.cwd }); + }); + test("agent mode with keyless framework uses linked profile as a real app target", async () => { setup({ isAgent: true, email: "user@example.com" }); @@ -407,6 +438,7 @@ describe("init", () => { skipIfLinked: true, app: "app_abc", cwd: keylessCtx.cwd, + createIfMissing: expect.any(String), }); expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: keylessCtx.cwd }); }); @@ -451,6 +483,7 @@ describe("init", () => { skipIfLinked: true, app: "app_abc", cwd: FAKE_CTX.cwd, + createIfMissing: expect.any(String), }); }); diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index d89580f8..664926cd 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -6,6 +6,7 @@ import { dim, bold } from "../../lib/color.js"; import { throwUserAbort, CliError, errorMessage } from "../../lib/errors.js"; import { lookupFramework, type FrameworkInfo } from "../../lib/framework.js"; import { resolveProfile } from "../../lib/config.js"; +import { deriveProjectName } from "../../lib/project-name.js"; import { log } from "../../lib/log.js"; import { createAccountlessApp, @@ -97,12 +98,21 @@ export async function init(options: InitOptions = {}) { }); ctx.keyless = keyless; + // Agents can auto-create an app on keyless-capable frameworks when authed (see + // `link`'s `createIfMissing` path). Non-keyless frameworks without a real app + // target still need explicit human input, so they fall through to manual setup. const manualSetup = - !keyless && (agent ? !hasRealAppTarget : bootstrap != null && overrides.skipConfirm && !authed); + !keyless && + (agent + ? !hasRealAppTarget && !ctx.framework.supportsKeyless + : bootstrap != null && overrides.skipConfirm && !authed); if (!keyless && !manualSetup) { bar(); - await authenticateAndLink(ctx.cwd, options.app); + const createIfMissing = agent + ? await deriveProjectName(ctx.cwd, bootstrap?.projectName) + : undefined; + await authenticateAndLink(ctx.cwd, options.app, createIfMissing); } // Short-circuit on a fully-clean re-run so env pull / skills prompt don't @@ -240,17 +250,18 @@ function resolveKeylessMode({ hasRealAppTarget: boolean; }): boolean { if (ctx.framework.supportsKeyless) { - if (agent) return !hasRealAppTarget; + // Authenticated (OAuth token or CLERK_PLATFORM_API_KEY) — use the + // authenticated flow so a real app is created/linked and real keys get + // pulled into .env. Otherwise fall back to keyless: the app runs on + // auto-generated dev keys and the user can connect their account later + // via `clerk auth login`. + if (agent) return !hasRealAppTarget && !authed; // Auto-keyless is scoped to bootstrap (new-project) flows only in human // mode. Existing projects keep the authenticated flow so real keys can be - // pulled unless an agent explicitly chooses keyless by omitting an app. + // pulled. if (!bootstrap) return false; - // Authenticated (OAuth token or CLERK_PLATFORM_API_KEY) — use the - // authenticated flow so real keys get pulled into .env. Otherwise fall - // back to keyless: the app runs on auto-generated dev keys and the user - // can connect their account later via `clerk auth login`. return !authed; } @@ -275,7 +286,11 @@ async function resolveAuthLabel(): Promise { return ""; } -async function authenticateAndLink(cwd: string, app: string | undefined): Promise { +async function authenticateAndLink( + cwd: string, + app: string | undefined, + createIfMissing: string | undefined, +): Promise { const label = await resolveAuthLabel(); const profile = await resolveProfile(cwd); @@ -290,7 +305,7 @@ async function authenticateAndLink(cwd: string, app: string | undefined): Promis log.info(dim(label)); } - await link({ skipIfLinked: true, app, cwd }); + await link({ skipIfLinked: true, app, cwd, createIfMissing }); } // --- Keyless app setup --- diff --git a/packages/cli-core/src/commands/link/README.md b/packages/cli-core/src/commands/link/README.md index 0f303a3c..2797566c 100644 --- a/packages/cli-core/src/commands/link/README.md +++ b/packages/cli-core/src/commands/link/README.md @@ -28,6 +28,12 @@ deterministic paths: - if no unambiguous app can be determined, the command exits with a usage error telling the caller to pass `--app` +`clerk init` calls `link` with an internal `createIfMissing` option (the +project name) so an authenticated agent run that finds no autolink match auto- +creates a fresh app named after the project instead of erroring. The CLI does +not expose `createIfMissing` as a flag — it is library-only and reserved for +`clerk init`'s flow. + ## Flow 1. Resolves the normalized git remote URL (e.g., `github.com/org/repo`) for cross-clone matching diff --git a/packages/cli-core/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts index bbad9dda..f6523b72 100644 --- a/packages/cli-core/src/commands/link/index.test.ts +++ b/packages/cli-core/src/commands/link/index.test.ts @@ -244,6 +244,36 @@ describe("link", () => { expect(mockSearch).not.toHaveBeenCalled(); expect(mockListApplications).not.toHaveBeenCalled(); }); + + test("creates and links a new app when createIfMissing is provided", async () => { + mockIsAgent.mockReturnValue(true); + mockGetToken.mockResolvedValue("token"); + mockCreateApplication.mockResolvedValue({ application_id: "app_new" }); + mockFetchApplication.mockResolvedValue({ ...mockApp, application_id: "app_new" }); + consoleSpy = spyOn(console, "log").mockImplementation(() => {}); + + await runLink({ createIfMissing: "my-project" }); + + expect(mockCreateApplication).toHaveBeenCalledWith("my-project"); + expect(mockFetchApplication).toHaveBeenCalledWith("app_new"); + expect(mockSearch).not.toHaveBeenCalled(); + expect(mockListApplications).not.toHaveBeenCalled(); + expect(mockSetProfile).toHaveBeenCalled(); + }); + + test("autolink takes precedence over createIfMissing when keys match", async () => { + mockIsAgent.mockReturnValue(true); + mockAutolink.mockResolvedValue({ + path: "github.com/org/repo", + profile: { workspaceId: "", appId: "app_existing", instances: { development: "ins_1" } }, + }); + consoleSpy = spyOn(console, "log").mockImplementation(() => {}); + + await runLink({ createIfMissing: "my-project" }); + + expect(mockAutolink).toHaveBeenCalled(); + expect(mockCreateApplication).not.toHaveBeenCalled(); + }); }); describe("already linked", () => { diff --git a/packages/cli-core/src/commands/link/index.ts b/packages/cli-core/src/commands/link/index.ts index 796be87d..eaf04b94 100644 --- a/packages/cli-core/src/commands/link/index.ts +++ b/packages/cli-core/src/commands/link/index.ts @@ -3,7 +3,7 @@ import { confirm } from "../../lib/prompts.ts"; import { isAgent } from "../../mode.ts"; import { getToken } from "../../lib/credential-store.ts"; import { login } from "../auth/login.ts"; -import { fetchApplication, type Application } from "../../lib/plapi.ts"; +import { createApplication, fetchApplication, type Application } from "../../lib/plapi.ts"; import { appLabel, fetchAppsTolerantly, pickOrCreateApp } from "../../lib/app-picker.ts"; import { setProfile, resolveProfile, moveProfile } from "../../lib/config.ts"; import { autolink, findClerkKeys, matchKeyToApp } from "../../lib/autolink.ts"; @@ -18,6 +18,13 @@ interface LinkOptions { app?: string; skipIfLinked?: boolean; cwd?: string; + /** + * In agent mode without `--app` and no existing profile, auto-create a new + * Clerk application with this name and link to it instead of failing with a + * usage error. Used by `clerk init` to keep the authed-agent flow non- + * interactive end-to-end. + */ + createIfMissing?: string; } export async function link(options: LinkOptions = {}): Promise { @@ -42,7 +49,7 @@ export async function link(options: LinkOptions = {}): Promise { if (autolinked) return; } - if (agent && !existing && !options.app) { + if (agent && !existing && !options.app && !options.createIfMissing) { throwUsageError( "Cannot select an application in agent mode. Pass --app , or run `clerk apps list --json` and retry.", ); @@ -68,7 +75,9 @@ export async function link(options: LinkOptions = {}): Promise { const app = options.app ? await withApiContext(fetchApplication(options.app), "Failed to fetch application") - : await resolveApp(cwd, displayPath, !existing); + : agent && options.createIfMissing + ? await createAndFetchApp(options.createIfMissing) + : await resolveApp(cwd, displayPath, !existing); const devInstance = app.instances.find((i) => i.environment_type === "development"); const prodInstance = app.instances.find((i) => i.environment_type === "production"); @@ -184,3 +193,8 @@ async function resolveApp( message: `Select a Clerk application to link ${dim(`(repo: ${basename(displayPath)})`)}`, }); } + +async function createAndFetchApp(name: string): Promise { + const created = await withApiContext(createApplication(name), "Failed to create application"); + return withApiContext(fetchApplication(created.application_id), "Failed to fetch application"); +} diff --git a/packages/cli-core/src/lib/autoclaim.ts b/packages/cli-core/src/lib/autoclaim.ts index 9df13dc2..c71bf053 100644 --- a/packages/cli-core/src/lib/autoclaim.ts +++ b/packages/cli-core/src/lib/autoclaim.ts @@ -1,8 +1,8 @@ -import { basename, join } from "node:path"; import { readKeylessBreadcrumb, clearKeylessBreadcrumb } from "./keyless.ts"; import { claimApplication, type Application } from "./plapi.ts"; import { PlapiError, errorMessage } from "./errors.ts"; import { linkApp } from "./autolink.ts"; +import { deriveProjectName } from "./project-name.ts"; import { pull } from "../commands/env/pull.ts"; import { log } from "./log.ts"; @@ -15,40 +15,17 @@ export type AutoclaimResult = Claimed | Terminal | Failed | Skipped; type ClaimAttempt = { status: "claimed"; app: Application } | Terminal | Failed; -const APP_NAME_MAX_CHARS = 50; - const TERMINAL_BY_STATUS: Record = { 404: "not_found", 403: "no_organization", }; -async function deriveAppName(cwd: string): Promise { - try { - const pkg: { name?: unknown } = await Bun.file(join(cwd, "package.json")).json(); - if (typeof pkg.name === "string" && pkg.name.trim()) return pkg.name.trim(); - } catch { - // fall through - } - return basename(cwd); -} - -function truncateToChars(str: string, max: number): string { - const segments = [...new Intl.Segmenter().segment(str)]; - return segments.length <= max - ? str - : segments - .slice(0, max) - .map((s) => s.segment) - .join(""); -} - /** Orchestrates post-login claim of a keyless app. Never throws. */ export async function attemptAutoclaim(cwd: string): Promise { const breadcrumb = await readKeylessBreadcrumb(cwd); if (!breadcrumb) return { status: "not_keyless" }; - const rawName = await deriveAppName(cwd); - const appName = truncateToChars(rawName, APP_NAME_MAX_CHARS); + const appName = await deriveProjectName(cwd); const result = await tryClaim(breadcrumb.claimToken, appName); if (result.status === "failed") return result; diff --git a/packages/cli-core/src/lib/project-name.ts b/packages/cli-core/src/lib/project-name.ts new file mode 100644 index 00000000..86e5f93a --- /dev/null +++ b/packages/cli-core/src/lib/project-name.ts @@ -0,0 +1,32 @@ +import { basename, join } from "node:path"; + +const APP_NAME_MAX_CHARS = 50; + +function truncateToChars(str: string, max: number): string { + const segments = [...new Intl.Segmenter().segment(str)]; + return segments.length <= max + ? str + : segments + .slice(0, max) + .map((s) => s.segment) + .join(""); +} + +/** + * Derives a Clerk application name from the current project. Reads + * `package.json#name` first, then falls back to the directory basename. + * Result is truncated to a length safe for the PLAPI app-name field. + */ +export async function deriveProjectName(cwd: string, override?: string): Promise { + if (override?.trim()) return truncateToChars(override.trim(), APP_NAME_MAX_CHARS); + + try { + const pkg: { name?: unknown } = await Bun.file(join(cwd, "package.json")).json(); + if (typeof pkg.name === "string" && pkg.name.trim()) { + return truncateToChars(pkg.name.trim(), APP_NAME_MAX_CHARS); + } + } catch { + // fall through + } + return truncateToChars(basename(cwd), APP_NAME_MAX_CHARS); +} diff --git a/packages/cli-core/src/test/integration/agent-mode.test.ts b/packages/cli-core/src/test/integration/agent-mode.test.ts index 35058867..ad1f477b 100644 --- a/packages/cli-core/src/test/integration/agent-mode.test.ts +++ b/packages/cli-core/src/test/integration/agent-mode.test.ts @@ -104,25 +104,24 @@ test("unlink --yes removes the profile in agent mode", async () => { expect(config.profiles["github.com/test/project"]).toBeUndefined(); }); -test("init uses keyless for keyless framework without an app target in agent mode", async () => { +test("init creates and links a real app for keyless framework when authed in agent mode", async () => { await writeNextAppProject(); + const devInstance = getInstance(MOCK_APP, "development"); http.mock({ - "/v1/accountless_applications": { - publishable_key: "pk_test_keyless", - secret_key: "sk_test_keyless", - claim_url: "/apps/claim?token=keyless_token", - }, + [`/v1/platform/applications/${MOCK_APP.application_id}`]: MOCK_APP, + "/v1/platform/applications": MOCK_APP, }); await clerk("--mode", "agent", "init", "--no-skills"); const env = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local"); - expect(env.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe("pk_test_keyless"); - expect(env.get("CLERK_SECRET_KEY")).toBe("sk_test_keyless"); + expect(env.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe(devInstance.publishable_key); + expect(env.get("CLERK_SECRET_KEY")).toBe(devInstance.secret_key); const config = await readConfig(); - expect(config.profiles["github.com/test/project"]).toBeUndefined(); - expect(http.requests.some((r) => r.url.includes("/v1/platform/applications"))).toBe(false); + expect(config.profiles["github.com/test/project"]?.appId).toBe(MOCK_APP.application_id); + // Accountless endpoint should not be hit when the user is already authed. + expect(http.requests.some((r) => r.url.includes("/v1/accountless_applications"))).toBe(false); }); test("init prints manual setup for non-keyless framework without an app target in agent mode", async () => { From b175cd371ab37550eb8df8cab99b46d678ce2fbb Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 1 May 2026 15:32:25 -0600 Subject: [PATCH 2/2] refactor(link): drop redundant fetchApplication after createApplication The PLAPI POST /v1/platform/applications response already returns the full SerializableStack (application_id, name, instances[] with instance_id, environment_type, secret_key, publishable_key), so the follow-up GET was an extra round-trip that produced an identical shape. --- packages/cli-core/src/commands/link/index.test.ts | 5 ++--- packages/cli-core/src/commands/link/index.ts | 10 ++++------ .../cli-core/src/test/integration/agent-mode.test.ts | 1 - 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/cli-core/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts index f6523b72..e7627af7 100644 --- a/packages/cli-core/src/commands/link/index.test.ts +++ b/packages/cli-core/src/commands/link/index.test.ts @@ -248,14 +248,13 @@ describe("link", () => { test("creates and links a new app when createIfMissing is provided", async () => { mockIsAgent.mockReturnValue(true); mockGetToken.mockResolvedValue("token"); - mockCreateApplication.mockResolvedValue({ application_id: "app_new" }); - mockFetchApplication.mockResolvedValue({ ...mockApp, application_id: "app_new" }); + mockCreateApplication.mockResolvedValue({ ...mockApp, application_id: "app_new" }); consoleSpy = spyOn(console, "log").mockImplementation(() => {}); await runLink({ createIfMissing: "my-project" }); expect(mockCreateApplication).toHaveBeenCalledWith("my-project"); - expect(mockFetchApplication).toHaveBeenCalledWith("app_new"); + expect(mockFetchApplication).not.toHaveBeenCalled(); expect(mockSearch).not.toHaveBeenCalled(); expect(mockListApplications).not.toHaveBeenCalled(); expect(mockSetProfile).toHaveBeenCalled(); diff --git a/packages/cli-core/src/commands/link/index.ts b/packages/cli-core/src/commands/link/index.ts index eaf04b94..8e406a14 100644 --- a/packages/cli-core/src/commands/link/index.ts +++ b/packages/cli-core/src/commands/link/index.ts @@ -76,7 +76,10 @@ export async function link(options: LinkOptions = {}): Promise { const app = options.app ? await withApiContext(fetchApplication(options.app), "Failed to fetch application") : agent && options.createIfMissing - ? await createAndFetchApp(options.createIfMissing) + ? await withApiContext( + createApplication(options.createIfMissing), + "Failed to create application", + ) : await resolveApp(cwd, displayPath, !existing); const devInstance = app.instances.find((i) => i.environment_type === "development"); @@ -193,8 +196,3 @@ async function resolveApp( message: `Select a Clerk application to link ${dim(`(repo: ${basename(displayPath)})`)}`, }); } - -async function createAndFetchApp(name: string): Promise { - const created = await withApiContext(createApplication(name), "Failed to create application"); - return withApiContext(fetchApplication(created.application_id), "Failed to fetch application"); -} diff --git a/packages/cli-core/src/test/integration/agent-mode.test.ts b/packages/cli-core/src/test/integration/agent-mode.test.ts index ad1f477b..50ff159d 100644 --- a/packages/cli-core/src/test/integration/agent-mode.test.ts +++ b/packages/cli-core/src/test/integration/agent-mode.test.ts @@ -108,7 +108,6 @@ test("init creates and links a real app for keyless framework when authed in age await writeNextAppProject(); const devInstance = getInstance(MOCK_APP, "development"); http.mock({ - [`/v1/platform/applications/${MOCK_APP.application_id}`]: MOCK_APP, "/v1/platform/applications": MOCK_APP, });