From 944c1cd1319b92ca8ae167f6f5f67184abac0a38 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Tue, 21 Apr 2026 13:10:27 -0300 Subject: [PATCH] fix(init): pull API keys into bootstrapped project subdirectory After `clerk init` bootstraps a new project, the final `env pull` step resolved the linked profile via `process.cwd()` (still the parent directory), producing "No Clerk project linked to this directory" even though the freshly-created app was correctly linked under the subdir's absolute path. Thread `cwd` through `resolveAppContext` and `pull()` and pass `ctx.cwd` from init. --- .changeset/init-improvements.md | 5 +++ .../cli-core/src/commands/env/pull.test.ts | 32 +++++++++++++++++-- packages/cli-core/src/commands/env/pull.ts | 14 ++++---- .../cli-core/src/commands/init/index.test.ts | 2 +- packages/cli-core/src/commands/init/index.ts | 2 +- packages/cli-core/src/lib/config.test.ts | 12 +++++++ packages/cli-core/src/lib/config.ts | 17 ++++++---- 7 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 .changeset/init-improvements.md diff --git a/.changeset/init-improvements.md b/.changeset/init-improvements.md new file mode 100644 index 00000000..dfc2608b --- /dev/null +++ b/.changeset/init-improvements.md @@ -0,0 +1,5 @@ +--- +"clerk": patch +--- + +Fix `clerk init` bootstrap flow failing with "No Clerk project linked to this directory" when pulling API keys into a newly created project subdirectory. diff --git a/packages/cli-core/src/commands/env/pull.test.ts b/packages/cli-core/src/commands/env/pull.test.ts index f8ea8f3a..1a1ebf90 100644 --- a/packages/cli-core/src/commands/env/pull.test.ts +++ b/packages/cli-core/src/commands/env/pull.test.ts @@ -46,7 +46,7 @@ mock.module("../../lib/config.ts", () => ({ if (!id) throw new Error(`No ${env} instance configured. Run \`clerk link\` to set one up.`); return { id, label: env }; }, - resolveAppContext: async (options: { app?: string; instance?: string }) => { + resolveAppContext: async (options: { app?: string; instance?: string; cwd?: string }) => { if (options.app) { const app = { application_id: "app_1", @@ -92,7 +92,7 @@ mock.module("../../lib/config.ts", () => ({ }; } - const profile = _profiles[process.cwd()]; + const profile = _profiles[options.cwd ?? process.cwd()]; if (!profile) throw new Error("No Clerk project linked"); const instance = !options.instance ? { id: profile.instances.development, label: "development" } @@ -182,7 +182,9 @@ describe("env pull", () => { await rm(tempDir, { recursive: true, force: true }); }); - async function runEnvPull(options: { app?: string; instance?: string; file?: string } = {}) { + async function runEnvPull( + options: { app?: string; instance?: string; file?: string; cwd?: string } = {}, + ) { const { pull } = await import("./pull.ts"); return captured.run(() => pull(options)); } @@ -565,6 +567,30 @@ describe("env pull", () => { expect(await Bun.file(join(tempDir, ".env")).exists()).toBe(false); }); + test("writes to options.cwd, not process.cwd(), when cwd is passed", async () => { + const projectDir = await mkdtemp(join(tmpdir(), "clerk-env-pull-project-")); + try { + await Bun.write( + join(projectDir, "package.json"), + JSON.stringify({ dependencies: { express: "4.0.0" } }), + ); + await setProfile(projectDir, { + workspaceId: "org_1", + appId: "app_1", + instances: { development: "ins_dev" }, + }); + + await runEnvPull({ cwd: projectDir }); + + const projectEnv = await Bun.file(join(projectDir, ".env.local")).text(); + expect(projectEnv).toContain("CLERK_SECRET_KEY=sk_test_xyz789"); + // The process.cwd() directory (tempDir) must not have received keys. + expect(await Bun.file(join(tempDir, ".env.local")).exists()).toBe(false); + } finally { + await rm(projectDir, { recursive: true, force: true }); + } + }); + test("detects Nuxt and uses NUXT_CLERK_SECRET_KEY", async () => { await setProfile(tempDir, { workspaceId: "org_1", diff --git a/packages/cli-core/src/commands/env/pull.ts b/packages/cli-core/src/commands/env/pull.ts index 7b48bc7b..7a8e1d97 100644 --- a/packages/cli-core/src/commands/env/pull.ts +++ b/packages/cli-core/src/commands/env/pull.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import { resolveAppContext } from "../../lib/config.ts"; +import { resolveAppContext, type AppContextOptions } from "../../lib/config.ts"; import { fetchApplication } from "../../lib/plapi.ts"; import { parseEnvFile, mergeEnvVars, serializeEnvFile } from "../../lib/dotenv.ts"; import { @@ -13,9 +13,7 @@ import { log } from "../../lib/log.ts"; const DEV_LOCAL_ENV_FILE = ".env.development.local"; -interface EnvPullOptions { - app?: string; - instance?: string; +interface EnvPullOptions extends AppContextOptions { file?: string; } @@ -49,9 +47,11 @@ async function resolveTargetFile( } export async function pull(options: EnvPullOptions): Promise { - const ctx = await resolveAppContext(options); - const cwd = process.cwd(); - const preferredEnvFile = await detectEnvFile(cwd); + const cwd = options.cwd ?? process.cwd(); + const [ctx, preferredEnvFile] = await Promise.all([ + resolveAppContext({ ...options, cwd }), + detectEnvFile(cwd), + ]); const targetFile = await resolveTargetFile(cwd, options.file, preferredEnvFile); const displayPath = options.file ?? targetFile.split("/").pop()!; diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts index 7eabfa99..4efcf8a7 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -668,7 +668,7 @@ describe("init", () => { await init({ yes: true }); - expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env" }); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: mockCtx.cwd }); }); test("bootstrap passes project dir to link, not parent cwd", async () => { diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index 81748f0a..79e35ace 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -121,7 +121,7 @@ export async function init(options: InitOptions = {}) { if (skipAuth) { printBootstrapManualSetupInfo(ctx.framework.name); } else if (!keyless) { - await pull({ file: ctx.envFile }); + await pull({ file: ctx.envFile, cwd: ctx.cwd }); } else { await setupKeylessApp(ctx.cwd, ctx.framework.dep, ctx.envFile); } diff --git a/packages/cli-core/src/lib/config.test.ts b/packages/cli-core/src/lib/config.test.ts index 2f886c5c..c1805e1f 100644 --- a/packages/cli-core/src/lib/config.test.ts +++ b/packages/cli-core/src/lib/config.test.ts @@ -204,5 +204,17 @@ describe("config", () => { const ctx = await resolveAppContext({}); expect(ctx.appLabel).toBe("app_1"); }); + + test("resolves via explicit cwd instead of process.cwd()", async () => { + const projectDir = join(tempDir, "child-project"); + await setProfile(projectDir, { + workspaceId: "org_1", + appId: "app_in_child", + instances: { development: "ins_dev" }, + }); + + const ctx = await resolveAppContext({ cwd: projectDir }); + expect(ctx.appId).toBe("app_in_child"); + }); }); }); diff --git a/packages/cli-core/src/lib/config.ts b/packages/cli-core/src/lib/config.ts index 7f484a45..cca8f8d5 100644 --- a/packages/cli-core/src/lib/config.ts +++ b/packages/cli-core/src/lib/config.ts @@ -231,6 +231,12 @@ export function resolveInstanceId(profile: Profile, flag?: string): { id: string return { id, label: env }; } +interface AppContextOptions { + app?: string; + instance?: string; + cwd?: string; +} + /** * Resolve app context from explicit flags or linked profile. * This is the isomorphic resolution chain used by profile-dependent commands: @@ -238,10 +244,9 @@ export function resolveInstanceId(profile: Profile, flag?: string): { id: string * 2. resolveProfile(cwd) (project-aware, existing behavior) * 3. Error with helpful message */ -export async function resolveAppContext(options: { - app?: string; - instance?: string; -}): Promise<{ appId: string; appLabel: string; instanceId: string; instanceLabel: string }> { +export async function resolveAppContext( + options: AppContextOptions, +): Promise<{ appId: string; appLabel: string; instanceId: string; instanceLabel: string }> { if (options.app) { const { fetchApplication } = await import("./plapi.ts"); const app = await fetchApplication(options.app); @@ -289,7 +294,7 @@ export async function resolveAppContext(options: { }; } - const resolved = await resolveProfile(process.cwd()); + const resolved = await resolveProfile(options.cwd ?? process.cwd()); if (!resolved) { throw new CliError( "No Clerk project linked to this directory.\n" + @@ -309,4 +314,4 @@ export async function resolveAppContext(options: { }; } -export type { Auth, Profile, ClerkConfig }; +export type { Auth, Profile, ClerkConfig, AppContextOptions };