diff --git a/bun.lock b/bun.lock index 125023a5..55551620 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "oxfmt": "^0.36.0", "oxlint": "^1.58.0", "playwright": "^1.59.1", - "typescript": "^5", + "typescript": "^6.0.2", }, }, "packages/cli": { @@ -450,7 +450,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], diff --git a/package.json b/package.json index d44c42e0..156446a9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "bun run --filter @clerk/cli-core build", "dev": "bun run --cwd packages/cli-core dev", + "typecheck": "bun run --filter @clerk/cli-core typecheck", "test": "bun run scripts/run-tests.ts --pattern 'packages/cli-core/src/**/*.test.ts' --pattern 'scripts/**/*.test.ts'", "test:e2e": "bun run scripts/run-tests.ts --pattern 'test/e2e/*.test.ts' --retries 1", "test:e2e:op": "bun run scripts/run-e2e-op.ts", @@ -37,7 +38,7 @@ "oxfmt": "^0.36.0", "oxlint": "^1.58.0", "playwright": "^1.59.1", - "typescript": "^5" + "typescript": "^6.0.2" }, "nano-staged": { "*.{ts,tsx,js,jsx}": [ diff --git a/packages/cli-core/mocks/bun.lock b/packages/cli-core/mocks/bun.lock index 9fa0c179..512895e9 100644 --- a/packages/cli-core/mocks/bun.lock +++ b/packages/cli-core/mocks/bun.lock @@ -1,10 +1,11 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "clerk-cli-mock-auth", "devDependencies": { - "typescript": "^5", + "typescript": "^6.0.2", "vite": "^6.3.5", }, }, @@ -134,7 +135,7 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], } diff --git a/packages/cli-core/mocks/package.json b/packages/cli-core/mocks/package.json index d7f08b02..6a401f75 100644 --- a/packages/cli-core/mocks/package.json +++ b/packages/cli-core/mocks/package.json @@ -6,7 +6,7 @@ "dev": "vite" }, "devDependencies": { - "typescript": "^5", + "typescript": "^6.0.2", "vite": "^6.3.5" } } diff --git a/packages/cli-core/mocks/src/main.ts b/packages/cli-core/mocks/src/main.ts index 0df3200a..ec7c5bd7 100644 --- a/packages/cli-core/mocks/src/main.ts +++ b/packages/cli-core/mocks/src/main.ts @@ -4,9 +4,9 @@ const app = document.getElementById("app")!; const params = new URLSearchParams(window.location.search); const callbackPort = params.get("callback_port"); -type Screen = "sign-in" | "sign-up" | "select-app" | "success-new" | "success-existing"; +type AppScreen = "sign-in" | "sign-up" | "select-app" | "success-new" | "success-existing"; -function render(screen: Screen) { +function render(screen: AppScreen) { switch (screen) { case "sign-in": return renderSignIn(); diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index c2e7663f..0bc51620 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -10,6 +10,7 @@ "build": "bun build ./src/cli.ts --outfile ./dist/cli.js --target node --external @napi-rs/keyring", "build:compile": "bun build --compile --no-compile-autoload-dotenv ./src/cli.ts --outfile ./dist/clerk", "dev": "bun run ./src/cli.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", "lint": "oxlint src/", "format": "oxfmt --write src/", "format:check": "oxfmt --check src/" diff --git a/packages/cli-core/src/commands/api/catalog.test.ts b/packages/cli-core/src/commands/api/catalog.test.ts index a7e1b2c7..a7c5e675 100644 --- a/packages/cli-core/src/commands/api/catalog.test.ts +++ b/packages/cli-core/src/commands/api/catalog.test.ts @@ -146,13 +146,17 @@ describe("filterEndpoints", () => { test("filters by summary", () => { const results = filterEndpoints(catalog, "retrieve"); expect(results.length).toBe(1); - expect(results[0].operationId).toBe("GetUser"); + const [firstResult] = results; + expect(firstResult).toBeDefined(); + expect(firstResult?.operationId).toBe("GetUser"); }); test("filters by tag", () => { const results = filterEndpoints(catalog, "organizations"); expect(results.length).toBe(1); - expect(results[0].tag).toBe("Organizations"); + const [firstResult] = results; + expect(firstResult).toBeDefined(); + expect(firstResult?.tag).toBe("Organizations"); }); test("is case-insensitive", () => { diff --git a/packages/cli-core/src/commands/api/index.test.ts b/packages/cli-core/src/commands/api/index.test.ts index 04bdcd94..3670df5f 100644 --- a/packages/cli-core/src/commands/api/index.test.ts +++ b/packages/cli-core/src/commands/api/index.test.ts @@ -9,6 +9,7 @@ import { promptsStubs, stubFetch, } from "../../test/lib/stubs.ts"; +import { getPlapiBaseUrl } from "../../lib/environment.ts"; let mockStoredToken: string | null = null; mock.module("../../lib/credential-store.ts", () => ({ @@ -399,8 +400,7 @@ describe("api command", () => { }); await runApi("/v1/platform/applications", { platform: true }); - expect(capturedUrl).toContain("api.clerk.com"); - expect(capturedUrl).not.toContain("api.clerk.dev"); + expect(capturedUrl).toStartWith(`${getPlapiBaseUrl()}/`); expect(capturedHeaders?.get("Authorization")).toBe("Bearer plat_key_123"); }); diff --git a/packages/cli-core/src/commands/api/interactive.test.ts b/packages/cli-core/src/commands/api/interactive.test.ts index 9285d2a8..60dbbced 100644 --- a/packages/cli-core/src/commands/api/interactive.test.ts +++ b/packages/cli-core/src/commands/api/interactive.test.ts @@ -156,8 +156,10 @@ describe("apiInteractive", () => { await apiInteractive({}); expect(fetchCalls.length).toBe(1); - expect(fetchCalls[0].url).toContain("/v1/users"); - expect(fetchCalls[0].method).toBe("GET"); + const [firstCall] = fetchCalls; + expect(firstCall).toBeDefined(); + expect(firstCall?.url).toContain("/v1/users"); + expect(firstCall?.method).toBe("GET"); }); test("prompts for path parameters", async () => { @@ -180,7 +182,9 @@ describe("apiInteractive", () => { await apiInteractive({}); expect(fetchCalls.length).toBe(1); - expect(fetchCalls[0].url).toContain("/v1/users/user_abc123"); + const [firstCall] = fetchCalls; + expect(firstCall).toBeDefined(); + expect(firstCall?.url).toContain("/v1/users/user_abc123"); }); test("aborts when user declines confirmation", async () => { diff --git a/packages/cli-core/src/commands/auth/login.test.ts b/packages/cli-core/src/commands/auth/login.test.ts index 5b2417cb..32f1b331 100644 --- a/packages/cli-core/src/commands/auth/login.test.ts +++ b/packages/cli-core/src/commands/auth/login.test.ts @@ -335,7 +335,7 @@ describe("login", () => { expect( consoleErrorSpy.mock.calls.some( - (c) => typeof c[0] === "string" && (c[0] as string).includes("Next steps:"), + (c: unknown[]) => typeof c[0] === "string" && (c[0] as string).includes("Next steps:"), ), ).toBe(false); }); diff --git a/packages/cli-core/src/commands/init/frameworks/astro.test.ts b/packages/cli-core/src/commands/init/frameworks/astro.test.ts index b7caf5f0..1309d280 100644 --- a/packages/cli-core/src/commands/init/frameworks/astro.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/astro.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "Astro", sdk: "@clerk/astro", envVar: "PUBLIC_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: false, diff --git a/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts b/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts index 520dbb15..631c4991 100644 --- a/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "Next.js", sdk: "@clerk/nextjs", envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local", }, variant: "app-router", typescript: true, diff --git a/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts b/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts index aa36716d..f3f8ed73 100644 --- a/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "Next.js", sdk: "@clerk/nextjs", envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local", }, variant: "pages-router", typescript: true, diff --git a/packages/cli-core/src/commands/init/frameworks/nuxt.test.ts b/packages/cli-core/src/commands/init/frameworks/nuxt.test.ts index 3b5c9a6d..6a74dd37 100644 --- a/packages/cli-core/src/commands/init/frameworks/nuxt.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/nuxt.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "Nuxt", sdk: "@clerk/nuxt", envVar: "NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: false, diff --git a/packages/cli-core/src/commands/init/frameworks/react-router.test.ts b/packages/cli-core/src/commands/init/frameworks/react-router.test.ts index c1450aee..6dde5cec 100644 --- a/packages/cli-core/src/commands/init/frameworks/react-router.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/react-router.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "React Router", sdk: "@clerk/react-router", envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: false, diff --git a/packages/cli-core/src/commands/init/frameworks/react-router.ts b/packages/cli-core/src/commands/init/frameworks/react-router.ts index 90196ee2..01801c1e 100644 --- a/packages/cli-core/src/commands/init/frameworks/react-router.ts +++ b/packages/cli-core/src/commands/init/frameworks/react-router.ts @@ -226,7 +226,8 @@ function ensureRouteImported(source: string): string { const importMatch = source.match( /import\s*\{([^}]*)\}\s*from\s*["']@react-router\/dev\/routes["']/, ); - if (!importMatch || /\broute\b/.test(importMatch[1])) return source; + const importedNames = importMatch?.[1]; + if (!importedNames || /\broute\b/.test(importedNames)) return source; return source.replace( /(\bimport\s*\{[^}]*)(\}\s*from\s*["']@react-router\/dev\/routes["'])/, @@ -302,7 +303,7 @@ function injectRouteEntries( const canonicalPattern = /export default \[([^\]]*)\]\s*satisfies\s*RouteConfig\s*;/s; const canonical = source.match(canonicalPattern); if (canonical) { - const innerContent = canonical[1].trimEnd(); + const innerContent = (canonical[1] ?? "").trimEnd(); const separator = innerContent.length > 0 && !innerContent.endsWith(",") ? "," : ""; const newInner = `${innerContent}${separator}\n ${newEntries},\n`; return source.replace(canonicalPattern, `export default [${newInner}] satisfies RouteConfig;`); @@ -312,7 +313,7 @@ function injectRouteEntries( const simplePattern = /(export\s+default\s+\[)([\s\S]*?)(\]\s*;)/; const simple = source.match(simplePattern); if (simple) { - const innerContent = simple[2].trimEnd(); + const innerContent = (simple[2] ?? "").trimEnd(); const separator = innerContent.length > 0 && !innerContent.endsWith(",") ? "," : ""; const newInner = `${innerContent}${separator}\n ${newEntries},\n`; return source.replace(simplePattern, `$1${newInner}$3`); diff --git a/packages/cli-core/src/commands/init/frameworks/react.test.ts b/packages/cli-core/src/commands/init/frameworks/react.test.ts index 32c50ea7..7932a1cf 100644 --- a/packages/cli-core/src/commands/init/frameworks/react.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/react.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "React", sdk: "@clerk/react", envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: true, diff --git a/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts b/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts index c2ea0238..23791914 100644 --- a/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "TanStack Start", sdk: "@clerk/tanstack-react-start", envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: true, diff --git a/packages/cli-core/src/commands/init/frameworks/transformations.ts b/packages/cli-core/src/commands/init/frameworks/transformations.ts index 2be47a2e..202902d7 100644 --- a/packages/cli-core/src/commands/init/frameworks/transformations.ts +++ b/packages/cli-core/src/commands/init/frameworks/transformations.ts @@ -81,7 +81,7 @@ export function wrapBodyWithProvider(content: string, provider: string): string const providerIndent = bodyIndent + " "; const contentIndent = providerIndent + " "; - const trimmedInner = inner.trim(); + const trimmedInner = (inner ?? "").trim(); const reindented = trimmedInner .split("\n") .map((line) => { diff --git a/packages/cli-core/src/commands/init/frameworks/vue.test.ts b/packages/cli-core/src/commands/init/frameworks/vue.test.ts index 8e8833ef..688c2aa9 100644 --- a/packages/cli-core/src/commands/init/frameworks/vue.test.ts +++ b/packages/cli-core/src/commands/init/frameworks/vue.test.ts @@ -15,6 +15,7 @@ function makeCtx(overrides?: Partial): ProjectContext { name: "Vue", sdk: "@clerk/vue", envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: true, diff --git a/packages/cli-core/src/commands/init/heuristics.ts b/packages/cli-core/src/commands/init/heuristics.ts index 806df2f7..93a37157 100644 --- a/packages/cli-core/src/commands/init/heuristics.ts +++ b/packages/cli-core/src/commands/init/heuristics.ts @@ -17,6 +17,9 @@ export async function installSdk(ctx: ProjectContext): Promise { // the actual binary being installed (e.g. teammate committed bun.lock, you // only have npm). Fail fast with a useful message rather than a raw ENOENT. const pmBinary = addCmd.split(" ")[0]; + if (!pmBinary) { + throw new Error(`Invalid package manager install command: ${addCmd}`); + } if (Bun.which(pmBinary) === null) { console.log( yellow( diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts index 9cd31ad2..555e29ab 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -19,14 +19,17 @@ import * as heuristics from "./heuristics.ts"; import * as skillsMod from "./skills.ts"; import * as bootstrapMod from "./bootstrap.ts"; import { init } from "./index.ts"; +import type { FrameworkInfo } from "../../lib/framework.ts"; +import type { ProjectContext } from "./frameworks/types.ts"; -const FAKE_CTX = { +const FAKE_CTX: ProjectContext = { cwd: "/tmp/test", framework: { dep: "react", name: "React", sdk: "@clerk/react", envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }, typescript: true, srcDir: false, @@ -43,9 +46,11 @@ const FAKE_BOOTSTRAP = { }; describe("init", () => { + const originalEnv = { ...process.env }; let spies: ReturnType[]; afterEach(() => { + process.env = { ...originalEnv }; for (const s of spies) s.mockRestore(); }); @@ -95,6 +100,7 @@ describe("init", () => { } test("suppresses auth next-steps when login runs during init", async () => { + delete process.env.CLERK_PLATFORM_API_KEY; setup({ email: null }); spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); spyOn(heuristics, "getAuthenticatedEmail").mockResolvedValue(null); @@ -173,11 +179,12 @@ describe("init", () => { }); test("passes frameworkOverride to bootstrap when provided", async () => { - const fwOverride = { + const fwOverride: FrameworkInfo = { dep: "next", name: "Next.js", sdk: "@clerk/nextjs", envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + envFile: ".env", }; setup({ email: "test@test.com" }); spyOn(frameworkMod, "lookupFramework").mockReturnValue(fwOverride); @@ -228,15 +235,24 @@ describe("init", () => { test("short-circuits env pull and skills install when already set up", async () => { const { gatherContextSpy } = setup({ email: "test@test.com" }); - gatherContextSpy.mockResolvedValueOnce({ + const existingCtx: ProjectContext = { cwd: "/tmp/fake", - framework: { name: "Next.js", dep: "next", sdk: "@clerk/nextjs", publishableKeyEnv: "x" }, + framework: { + name: "Next.js", + dep: "next", + sdk: "@clerk/nextjs", + envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + envFile: ".env", + }, deps: { next: "15.0.0" }, packageManager: "bun", typescript: true, srcDir: false, existingClerk: true, - } as never); + envFile: ".env", + }; + + gatherContextSpy.mockResolvedValueOnce(existingCtx); await init({ yes: true }); @@ -268,7 +284,7 @@ describe("init", () => { gatherContextSpy.mockResolvedValue(mockCtx); spyOn(scaffoldMod, "scaffold").mockResolvedValue({ - actions: [{ type: "write", path: "app/layout.tsx", content: "" }], + actions: [{ type: "modify", path: "app/layout.tsx", content: "", description: "" }], postInstructions: [], }); diff --git a/packages/cli-core/src/commands/init/prompts/index.ts b/packages/cli-core/src/commands/init/prompts/index.ts index 246de78e..1dc36bbc 100644 --- a/packages/cli-core/src/commands/init/prompts/index.ts +++ b/packages/cli-core/src/commands/init/prompts/index.ts @@ -33,7 +33,7 @@ const TEMPLATES = { } satisfies Record; type TemplateName = keyof typeof TEMPLATES; -type FrameworkTemplateName = Exclude; +type FrameworkTemplateName = Exclude; type FrameworkPromptInfo = { template: FrameworkTemplateName; docsUrl: string }; function loadTemplate(name: TemplateName): string { diff --git a/packages/cli-core/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts index 239ac3b5..d74e76c3 100644 --- a/packages/cli-core/src/commands/link/index.test.ts +++ b/packages/cli-core/src/commands/link/index.test.ts @@ -114,9 +114,11 @@ const mockApp = { }; describe("link", () => { + const originalEnv = { ...process.env }; let consoleSpy: ReturnType; afterEach(() => { + process.env = { ...originalEnv }; _modeOverride = undefined; mockIsAgent.mockReset(); mockGetToken.mockReset(); @@ -221,6 +223,7 @@ describe("link", () => { describe("authentication", () => { test("calls login when no token exists", async () => { + delete process.env.CLERK_PLATFORM_API_KEY; mockIsAgent.mockReturnValue(false); mockGetToken.mockResolvedValue(null); mockLogin.mockResolvedValue({ userId: "user_1", email: "test@test.com" }); @@ -233,6 +236,7 @@ describe("link", () => { }); test("skips login when token exists", async () => { + delete process.env.CLERK_PLATFORM_API_KEY; mockIsAgent.mockReturnValue(false); mockGetToken.mockResolvedValue("oauth_token_123"); mockFetchApplication.mockResolvedValue(mockApp); @@ -244,6 +248,7 @@ describe("link", () => { }); test("suppresses auth next-steps when login runs during link", async () => { + delete process.env.CLERK_PLATFORM_API_KEY; mockIsAgent.mockReturnValue(false); mockGetToken.mockResolvedValue(null); mockLogin.mockResolvedValue({ userId: "user_1", email: "test@test.com" }); diff --git a/packages/cli-core/src/lib/auth-server.ts b/packages/cli-core/src/lib/auth-server.ts index c72bd962..bc490c4d 100644 --- a/packages/cli-core/src/lib/auth-server.ts +++ b/packages/cli-core/src/lib/auth-server.ts @@ -146,8 +146,13 @@ export function startAuthServer(expectedState: string): AuthServerResult { }, }); + const port = server.port; + if (port === undefined) { + throw new Error("Failed to determine auth server port."); + } + return { - port: server.port, + port, waitForCallback: () => callbackPromise, stop: () => { clearTimeout(timeout); diff --git a/packages/cli-core/src/lib/constants.ts b/packages/cli-core/src/lib/constants.ts index 9ce491e8..c52cf2bf 100644 --- a/packages/cli-core/src/lib/constants.ts +++ b/packages/cli-core/src/lib/constants.ts @@ -12,7 +12,7 @@ import envPaths from "env-paths"; // ── File paths ────────────────────────────────────────────────────────────── const clerkConfigDir = process.env.CLERK_CONFIG_DIR; -const paths = envPaths("clerk-cli", { suffix: false }); +const paths = envPaths("clerk-cli", { suffix: "" }); export const CONFIG_FILE = join(clerkConfigDir ?? paths.config, "config.json"); export const CREDENTIALS_FILE = join(clerkConfigDir ?? paths.data, "credentials"); diff --git a/packages/cli-core/src/lib/dotenv.ts b/packages/cli-core/src/lib/dotenv.ts index 3d62a6a8..8f14b1a2 100644 --- a/packages/cli-core/src/lib/dotenv.ts +++ b/packages/cli-core/src/lib/dotenv.ts @@ -43,7 +43,8 @@ export function parseEnvFile(content: string): EnvLine[] { const match = line.match(ENTRY_RE); if (!match) return { type: "comment", raw: line }; const key = match[2]; - let value = match[3]; + if (!key) return { type: "comment", raw: line }; + let value = match[3] ?? ""; // Strip surrounding quotes if ( (value.startsWith('"') && value.endsWith('"')) || @@ -60,6 +61,7 @@ export function mergeEnvVars(lines: EnvLine[], vars: Record): En const result = lines.map((line): EnvLine => { if (line.type !== "entry" || !(line.key in remaining)) return line; const value = remaining[line.key]; + if (value === undefined) return line; delete remaining[line.key]; return { type: "entry", key: line.key, value, raw: `${line.key}=${value}` }; }); diff --git a/packages/cli-core/src/lib/help.ts b/packages/cli-core/src/lib/help.ts index 04a1b446..c6c88b14 100644 --- a/packages/cli-core/src/lib/help.ts +++ b/packages/cli-core/src/lib/help.ts @@ -102,14 +102,17 @@ export function clerkHelpConfig(): Partial { c.argsStr ? c.name.padEnd(maxNameLen + 2) + c.argsStr : c.name, ); const termWidth = Math.max(...terms.map((t) => helper.displayWidth(t))); - const items = terms.map((term, i) => + const items = terms.map((term, i) => { + const description = cmdData[i]?.description ?? ""; + return ( helper.formatItem( helper.styleSubcommandTerm(term), termWidth, - helper.styleSubcommandDescription(cmdData[i].description), + helper.styleSubcommandDescription(description), helper, - ), - ); + ) + ); + }); output = output.concat(helper.formatItemList("Commands:", items, helper)); } diff --git a/packages/cli-core/src/lib/plapi.test.ts b/packages/cli-core/src/lib/plapi.test.ts index 9f133e3b..0dd0fd94 100644 --- a/packages/cli-core/src/lib/plapi.test.ts +++ b/packages/cli-core/src/lib/plapi.test.ts @@ -1,5 +1,6 @@ import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test"; import { credentialStoreStubs, stubFetch } from "../test/lib/stubs.ts"; +import { getPlapiBaseUrl } from "./environment.ts"; const mockGetToken = mock(); mock.module("./credential-store.ts", () => ({ @@ -16,10 +17,12 @@ const { createApplication, } = await import("./plapi.ts"); const { PlapiError } = await import("./errors.ts"); +type PlapiErrorInstance = InstanceType; describe("plapi", () => { const originalEnv = { ...process.env }; const originalFetch = globalThis.fetch; + const expectedBaseUrl = getPlapiBaseUrl(); beforeEach(() => { mockGetToken.mockResolvedValue(null); @@ -73,7 +76,7 @@ describe("plapi", () => { await fetchInstanceConfig("app_abc", "ins_def"); expect(requestedUrl).toBe( - "https://api.clerk.com/v1/platform/applications/app_abc/instances/ins_def/config", + `${expectedBaseUrl}/v1/platform/applications/app_abc/instances/ins_def/config`, ); }); @@ -105,12 +108,12 @@ describe("plapi", () => { expect(true).toBe(false); // should not reach } catch (error) { expect(error).toBeInstanceOf(PlapiError); - expect((error as PlapiError).status).toBe(404); - expect((error as PlapiError).body).toBe("Not Found"); + expect((error as PlapiErrorInstance).status).toBe(404); + expect((error as PlapiErrorInstance).body).toBe("Not Found"); } }); - test("default base URL is api.clerk.com", async () => { + test("uses the active Platform API base URL", async () => { let requestedUrl = ""; stubFetch(async (input) => { requestedUrl = input.toString(); @@ -118,7 +121,7 @@ describe("plapi", () => { }); await fetchInstanceConfig("app_1", "ins_1"); - expect(requestedUrl).toStartWith("https://api.clerk.com/"); + expect(requestedUrl).toStartWith(`${expectedBaseUrl}/`); }); describe("putInstanceConfig", () => { @@ -134,7 +137,7 @@ describe("plapi", () => { await putInstanceConfig("app_abc", "ins_def", { session: { lifetime: 3600 } }); expect(capturedMethod).toBe("PUT"); expect(capturedUrl).toBe( - "https://api.clerk.com/v1/platform/applications/app_abc/instances/ins_def/config", + `${expectedBaseUrl}/v1/platform/applications/app_abc/instances/ins_def/config`, ); }); @@ -179,7 +182,7 @@ describe("plapi", () => { expect(true).toBe(false); } catch (error) { expect(error).toBeInstanceOf(PlapiError); - expect((error as PlapiError).status).toBe(400); + expect((error as PlapiErrorInstance).status).toBe(400); } }); }); @@ -197,7 +200,7 @@ describe("plapi", () => { await patchInstanceConfig("app_abc", "ins_def", { session: { lifetime: 3600 } }); expect(capturedMethod).toBe("PATCH"); expect(capturedUrl).toBe( - "https://api.clerk.com/v1/platform/applications/app_abc/instances/ins_def/config", + `${expectedBaseUrl}/v1/platform/applications/app_abc/instances/ins_def/config`, ); }); @@ -241,7 +244,7 @@ describe("plapi", () => { expect(true).toBe(false); } catch (error) { expect(error).toBeInstanceOf(PlapiError); - expect((error as PlapiError).status).toBe(422); + expect((error as PlapiErrorInstance).status).toBe(422); } }); }); @@ -282,7 +285,7 @@ describe("plapi", () => { expect(true).toBe(false); } catch (error) { expect(error).toBeInstanceOf(PlapiError); - expect((error as PlapiError).status).toBe(404); + expect((error as PlapiErrorInstance).status).toBe(404); } }); }); @@ -304,7 +307,7 @@ describe("plapi", () => { await createApplication("My App"); expect(capturedMethod).toBe("POST"); - expect(capturedUrl).toBe("https://api.clerk.com/v1/platform/applications"); + expect(capturedUrl).toBe(`${expectedBaseUrl}/v1/platform/applications`); expect(JSON.parse(capturedBody)).toEqual({ name: "My App" }); }); @@ -339,7 +342,7 @@ describe("plapi", () => { expect(true).toBe(false); } catch (error) { expect(error).toBeInstanceOf(PlapiError); - expect((error as PlapiError).status).toBe(400); + expect((error as PlapiErrorInstance).status).toBe(400); } }); }); @@ -353,7 +356,7 @@ describe("plapi", () => { }); await listApplications(); - expect(requestedUrl).toBe("https://api.clerk.com/v1/platform/applications"); + expect(requestedUrl).toBe(`${expectedBaseUrl}/v1/platform/applications`); }); test("returns parsed application list", async () => { @@ -375,7 +378,7 @@ describe("plapi", () => { expect(true).toBe(false); } catch (error) { expect(error).toBeInstanceOf(PlapiError); - expect((error as PlapiError).status).toBe(403); + expect((error as PlapiErrorInstance).status).toBe(403); } }); }); diff --git a/packages/cli-core/src/lib/spinner.ts b/packages/cli-core/src/lib/spinner.ts index 4f186d33..e987483b 100644 --- a/packages/cli-core/src/lib/spinner.ts +++ b/packages/cli-core/src/lib/spinner.ts @@ -96,7 +96,7 @@ function createSpinner() { } stream.write("\x1b[?25l"); // hide cursor timer = setInterval(() => { - const char = cyan(FRAMES[frame++ % FRAMES.length]); + const char = cyan(FRAMES[frame++ % FRAMES.length] ?? FRAMES[0]!); stream.write(`\r\x1b[K${char} ${message}`); }, INTERVAL); }, diff --git a/packages/cli-core/src/lib/token-exchange.test.ts b/packages/cli-core/src/lib/token-exchange.test.ts index 5c0609b2..e47e1fc2 100644 --- a/packages/cli-core/src/lib/token-exchange.test.ts +++ b/packages/cli-core/src/lib/token-exchange.test.ts @@ -1,7 +1,19 @@ -import { test, expect, describe, afterEach, mock } from "bun:test"; +import { test, expect, describe, afterEach, mock, type Mock } from "bun:test"; import { exchangeCodeForToken, fetchUserInfo } from "./token-exchange.ts"; const originalFetch = globalThis.fetch; +type FetchImpl = (...args: Parameters) => ReturnType; +type FetchMock = Mock; +type MockedFetch = typeof fetch & FetchMock; + +function setFetchMock(impl: FetchImpl): MockedFetch { + const fetchMock = mock(impl); + const mockedFetch: MockedFetch = Object.assign(fetchMock, { + preconnect: originalFetch.preconnect.bind(originalFetch), + }); + globalThis.fetch = mockedFetch; + return mockedFetch; +} describe("exchangeCodeForToken", () => { afterEach(() => { @@ -15,12 +27,12 @@ describe("exchangeCodeForToken", () => { expires_in: 3600, }; - globalThis.fetch = mock(async () => { + const fetchMock = setFetchMock(async () => { return new Response(JSON.stringify(tokenResponse), { status: 200, headers: { "Content-Type": "application/json" }, }); - }) as typeof fetch; + }); const result = await exchangeCodeForToken({ code: "auth-code", @@ -29,13 +41,17 @@ describe("exchangeCodeForToken", () => { }); expect(result).toEqual(tokenResponse); - expect(globalThis.fetch).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(1); - const [, calledInit] = (globalThis.fetch as ReturnType).mock.calls[0]; + const calledInit = fetchMock.mock.calls[0]?.[1]; + expect(calledInit).toBeDefined(); + if (!calledInit) throw new Error("Expected fetch init options"); expect(calledInit.method).toBe("POST"); - expect(calledInit.headers["Content-Type"]).toBe("application/x-www-form-urlencoded"); + expect(new Headers(calledInit.headers).get("Content-Type")).toBe( + "application/x-www-form-urlencoded", + ); - const body = new URLSearchParams(calledInit.body); + const body = new URLSearchParams(String(calledInit.body)); expect(body.get("grant_type")).toBe("authorization_code"); expect(body.get("code")).toBe("auth-code"); expect(body.get("code_verifier")).toBe("test-verifier"); @@ -50,9 +66,9 @@ describe("exchangeCodeForToken", () => { refresh_token: "refresh-123", }; - globalThis.fetch = mock(async () => { + setFetchMock(async () => { return new Response(JSON.stringify(tokenResponse), { status: 200 }); - }) as typeof fetch; + }); const result = await exchangeCodeForToken({ code: "code", @@ -64,9 +80,9 @@ describe("exchangeCodeForToken", () => { }); test("throws on non-OK response with status code", async () => { - globalThis.fetch = mock(async () => { + setFetchMock(async () => { return new Response("invalid_grant", { status: 400 }); - }) as typeof fetch; + }); await expect( exchangeCodeForToken({ @@ -78,9 +94,9 @@ describe("exchangeCodeForToken", () => { }); test("includes error body in thrown message", async () => { - globalThis.fetch = mock(async () => { + setFetchMock(async () => { return new Response("detailed error info", { status: 401 }); - }) as typeof fetch; + }); await expect( exchangeCodeForToken({ @@ -98,40 +114,42 @@ describe("fetchUserInfo", () => { }); test("returns userId and email from userinfo response", async () => { - globalThis.fetch = mock(async () => { + setFetchMock(async () => { return new Response( JSON.stringify({ sub: "user_abc", email: "user@example.com", name: "Test" }), { status: 200 }, ); - }) as typeof fetch; + }); const result = await fetchUserInfo("valid-token"); expect(result).toEqual({ userId: "user_abc", email: "user@example.com" }); }); test("sends Bearer token in Authorization header", async () => { - globalThis.fetch = mock(async () => { + const fetchMock = setFetchMock(async () => { return new Response(JSON.stringify({ sub: "u", email: "e" }), { status: 200 }); - }) as typeof fetch; + }); await fetchUserInfo("my-secret-token"); - const [, init] = (globalThis.fetch as ReturnType).mock.calls[0]; - expect(init.headers.Authorization).toBe("Bearer my-secret-token"); + const init = fetchMock.mock.calls[0]?.[1]; + expect(init).toBeDefined(); + if (!init) throw new Error("Expected fetch init options"); + expect(new Headers(init.headers).get("Authorization")).toBe("Bearer my-secret-token"); }); test("throws on non-OK response with status code", async () => { - globalThis.fetch = mock(async () => { + setFetchMock(async () => { return new Response("Unauthorized", { status: 401 }); - }) as typeof fetch; + }); await expect(fetchUserInfo("expired-token")).rejects.toThrow("API error (401)"); }); test("includes response body in error message", async () => { - globalThis.fetch = mock(async () => { + setFetchMock(async () => { return new Response("token_revoked", { status: 403 }); - }) as typeof fetch; + }); await expect(fetchUserInfo("bad")).rejects.toThrow("token_revoked"); }); diff --git a/packages/cli-core/src/test/integration/config-management.test.ts b/packages/cli-core/src/test/integration/config-management.test.ts index 51708c6f..b4ad215d 100644 --- a/packages/cli-core/src/test/integration/config-management.test.ts +++ b/packages/cli-core/src/test/integration/config-management.test.ts @@ -17,9 +17,11 @@ import { useIntegrationTestHarness(); +const mockConfigSession = MOCK_CONFIG.session as { lifetime: number }; + test.each([{ mode: "human" }, { mode: "agent" }])( "pull config, check schema, patch settings ($mode mode)", - async ({ mode }) => { + async ({ mode }: { mode: "human" | "agent" }) => { const devInstance = getInstance(MOCK_APP, "development"); await setProfile("github.com/test/project", { @@ -34,7 +36,7 @@ test.each([{ mode: "human" }, { mode: "agent" }])( // Pull config const { stdout: pullOutput } = await clerk("--mode", mode, "config", "pull"); - expect(pullOutput).toContain(`"lifetime": ${MOCK_CONFIG.session.lifetime}`); + expect(pullOutput).toContain(`"lifetime": ${mockConfigSession.lifetime}`); // Pull schema http.mock({ diff --git a/packages/cli-core/tsconfig.json b/packages/cli-core/tsconfig.json index 190ca926..801e41ef 100644 --- a/packages/cli-core/tsconfig.json +++ b/packages/cli-core/tsconfig.json @@ -7,6 +7,7 @@ "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, + "types": ["bun"], // Bundler mode "moduleResolution": "bundler", diff --git a/test/e2e/fixtures/nextjs-app-router-next14/package.json b/test/e2e/fixtures/nextjs-app-router-next14/package.json index 0a673026..16e609a5 100644 --- a/test/e2e/fixtures/nextjs-app-router-next14/package.json +++ b/test/e2e/fixtures/nextjs-app-router-next14/package.json @@ -18,6 +18,6 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "typescript": "^5" + "typescript": "^6.0.2" } } diff --git a/test/e2e/fixtures/nextjs-app-router-next14/tsconfig.json b/test/e2e/fixtures/nextjs-app-router-next14/tsconfig.json index e7ff90fd..e5b8b1b5 100644 --- a/test/e2e/fixtures/nextjs-app-router-next14/tsconfig.json +++ b/test/e2e/fixtures/nextjs-app-router-next14/tsconfig.json @@ -4,6 +4,7 @@ "allowJs": true, "skipLibCheck": true, "strict": true, + "noUncheckedSideEffectImports": false, "noEmit": true, "esModuleInterop": true, "module": "esnext", diff --git a/test/e2e/fixtures/nextjs-app-router/package.json b/test/e2e/fixtures/nextjs-app-router/package.json index 2b11e27e..baeecbc3 100644 --- a/test/e2e/fixtures/nextjs-app-router/package.json +++ b/test/e2e/fixtures/nextjs-app-router/package.json @@ -17,7 +17,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^5" + "typescript": "^6.0.2" }, "ignoreScripts": [ "sharp", diff --git a/test/e2e/fixtures/nextjs-pages-router/package.json b/test/e2e/fixtures/nextjs-pages-router/package.json index 9203774e..d786f14e 100644 --- a/test/e2e/fixtures/nextjs-pages-router/package.json +++ b/test/e2e/fixtures/nextjs-pages-router/package.json @@ -17,7 +17,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^5" + "typescript": "^6.0.2" }, "ignoreScripts": [ "sharp", diff --git a/test/e2e/fixtures/react-router/package.json b/test/e2e/fixtures/react-router/package.json index 4ca8115d..edb0a502 100644 --- a/test/e2e/fixtures/react-router/package.json +++ b/test/e2e/fixtures/react-router/package.json @@ -24,7 +24,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "tailwindcss": "^4.1.13", - "typescript": "^5.9.2", + "typescript": "^6.0.2", "vite": "^7.1.7", "vite-tsconfig-paths": "^5.1.4" } diff --git a/test/e2e/fixtures/react-router/tsconfig.json b/test/e2e/fixtures/react-router/tsconfig.json index a6b90b7c..438d26f8 100644 --- a/test/e2e/fixtures/react-router/tsconfig.json +++ b/test/e2e/fixtures/react-router/tsconfig.json @@ -8,7 +8,6 @@ "moduleResolution": "bundler", "jsx": "react-jsx", "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", "paths": { "~/*": ["./app/*"] }, diff --git a/test/e2e/fixtures/react/package.json b/test/e2e/fixtures/react/package.json index f3ee5ad5..1eea19ab 100644 --- a/test/e2e/fixtures/react/package.json +++ b/test/e2e/fixtures/react/package.json @@ -24,8 +24,8 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.56.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", "vite": "^8.0.0" } } diff --git a/test/e2e/fixtures/tanstack-start/package.json b/test/e2e/fixtures/tanstack-start/package.json index bc915d68..41ed6167 100644 --- a/test/e2e/fixtures/tanstack-start/package.json +++ b/test/e2e/fixtures/tanstack-start/package.json @@ -35,7 +35,7 @@ "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^5.1.4", "jsdom": "^28.1.0", - "typescript": "^5.7.2", + "typescript": "^6.0.2", "vite": "^7.3.1", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.5" diff --git a/test/e2e/fixtures/tanstack-start/tsconfig.json b/test/e2e/fixtures/tanstack-start/tsconfig.json index 47543b2b..14525f4a 100644 --- a/test/e2e/fixtures/tanstack-start/tsconfig.json +++ b/test/e2e/fixtures/tanstack-start/tsconfig.json @@ -4,7 +4,6 @@ "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", - "baseUrl": ".", "paths": { "#/*": ["./src/*"], "@/*": ["./src/*"] diff --git a/test/e2e/fixtures/vue/package.json b/test/e2e/fixtures/vue/package.json index 1b7bc73c..5b86a4ca 100644 --- a/test/e2e/fixtures/vue/package.json +++ b/test/e2e/fixtures/vue/package.json @@ -16,7 +16,7 @@ "@types/node": "^24.12.0", "@vitejs/plugin-vue": "^6.0.5", "@vue/tsconfig": "^0.9.0", - "typescript": "~5.9.3", + "typescript": "~6.0.2", "vite": "^8.0.0", "vue-tsc": "^3.2.5" } diff --git a/tsconfig.json b/tsconfig.json index bfa0fead..b2e7497d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, + "types": ["bun"], // Bundler mode "moduleResolution": "bundler",