Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/init-framework-scaffolding-gaps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"clerk": minor
---

Close the `clerk init` framework scaffolding gaps.

- Expo projects get their expo-router root layout wrapped with `<ClerkProvider>` and the secure token cache, and `clerk init --starter --framework expo` bootstraps a new Expo app.
- Express and Fastify projects get `clerkMiddleware()` / `clerkPlugin` wired into the server entry file (ESM and CommonJS), plus the request type augmentation for TypeScript Express apps.
- iOS (Swift) and Android (Kotlin) projects are now detected (via Xcode project bundles and AndroidManifest.xml) and `clerk init --framework ios|android` is accepted; init links the app, pulls API keys, and prints the exact SDK setup steps.
60 changes: 49 additions & 11 deletions packages/cli-core/src/commands/init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ clerk init --no-skills

## Options

| Option | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--framework <name>` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify` |
| `--pm <manager>` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) |
| `--name <project-name>` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators |
| `--app <id>` | Application ID to link (skips the interactive app picker during authenticated linking) |
| `--starter` | Bootstrap a new project from a starter template (runs the framework generator, installs deps, and scaffolds Clerk) |
| `--keyless` | Use auto-generated temporary development keys instead of logging in. Only valid when bootstrapping a new project on a keyless-capable framework |
| `-y, --yes` | Skip y/n confirmation prompts. Authentication is still required — unauthenticated users are prompted to log in via the browser unless `--keyless` is also passed |
| `--no-skills` | Skip the optional agent skills install prompt at the end of init |
| Option | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--framework <name>` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` |
| `--pm <manager>` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) |
| `--name <project-name>` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators |
| `--app <id>` | Application ID to link (skips the interactive app picker during authenticated linking) |
| `--starter` | Bootstrap a new project from a starter template (runs the framework generator, installs deps, and scaffolds Clerk) |
| `--keyless` | Use auto-generated temporary development keys instead of logging in. Only valid when bootstrapping a new project on a keyless-capable framework |
| `-y, --yes` | Skip y/n confirmation prompts. Authentication is still required — unauthenticated users are prompted to log in via the browser unless `--keyless` is also passed |
| `--no-skills` | Skip the optional agent skills install prompt at the end of init |

## Agent Mode

Expand Down Expand Up @@ -87,13 +87,22 @@ 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 |

Native mobile platforms may not have a `package.json`, so they are detected from project marker files when no npm framework matches:

| Marker files | Framework | Clerk SDK | Publishable Key Env Var |
| ------------------------------------------------------------------- | ---------------- | ------------------------------------- | ----------------------- |
| `*.xcodeproj` / `*.xcworkspace` | iOS (Swift) | `ClerkKit` (Swift Package Manager) | `CLERK_PUBLISHABLE_KEY` |
| `app/src/main/AndroidManifest.xml` / `src/main/AndroidManifest.xml` | Android (Kotlin) | `com.clerk:clerk-android-ui` (Gradle) | `CLERK_PUBLISHABLE_KEY` |

A bare `Package.swift` or `build.gradle` is intentionally **not** enough — those also match server-side Swift packages and non-Android JVM projects. For native platforms the Clerk SDK cannot be installed by a JS package manager, so init skips the SDK install step and the scaffold plan prints Swift Package Manager / Gradle install steps instead. The publishable key is configured in source code (`Clerk.configure(...)` / `Clerk.initialize(...)`), so init still pulls keys into the env file and instructs the user to copy the key over.

The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). Keyless mode is opt-in via `--keyless` and is only valid when bootstrapping a new project on a Yes-row framework — passing `--keyless` for a No-row framework or for an existing project exits with a usage error. By default, init authenticates the user (interactively when needed) and links a real app. In agent mode, an authenticated run on a keyless-capable framework creates a real app named after the project and links it; an unauthenticated agent run without `--keyless` prints manual setup guidance 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.

## Scaffolding

Scaffolding is supported for the first 9 frameworks above (through JavaScript/Vite). Expo, Express, and Fastify are detected (SDK is installed, env vars are pulled) but scaffolding is not yet supported — users are directed to the Clerk docs.
Scaffolding is supported for every detected framework. iOS and Android write no files (their SDKs are not npm packages and their build files are not safe to modify automatically) — instead they print the exact quickstart steps as post-instructions.

All scaffolding is idempotent — files are skipped if they already contain Clerk setup.

Expand Down Expand Up @@ -183,6 +192,35 @@ Nuxt's module system auto-configures middleware and auto-imports components.

If no entry file is found, a post-instruction is printed pointing to the Clerk JS quickstart.

### Expo

| Action | File | Description |
| ------------- | ----------------------- | -------------------------------------------------------------------- |
| CREATE/MODIFY | `[src/]app/_layout.tsx` | Wrap the expo-router root layout with `ClerkProvider` + `tokenCache` |

The root layout is created (with a `<Slot />`) when missing and `expo-router` is a dependency; existing layouts have their main JSX return wrapped (guard returns like `if (!loaded) return null` are left alone). Post-instructions cover `npx expo install expo-secure-store` (required by `@clerk/expo/token-cache`, installed via `expo install` so the version matches the project's Expo SDK), enabling the Native API in the Dashboard, and adding sign-in/sign-up screens.

**Bootstrap (new project)**: `clerk init --starter --framework expo` scaffolds a new app via `create-expo-app`.

### Express

| Action | File | Description |
| ------ | ------------------------ | -------------------------------------------------------- |
| MODIFY | server entry (see below) | Add `clerkMiddleware()` right after `express()` creation |
| CREATE | `types/globals.d.ts` | `@clerk/express/env` type reference (TypeScript only) |

### Fastify

| Action | File | Description |
| ------ | ------------------------ | -------------------------------------------------------------- |
| MODIFY | server entry (see below) | Register `clerkPlugin` right after the `Fastify(...)` creation |

Express and Fastify share the server-entry scaffolding in [`node-server.ts`](./frameworks/node-server.ts). The entry file is resolved from `package.json#main` (ignored when it points at build output like `dist/`) and common candidates (`[src/]index|server|app|main` with `.ts/.mts/.js/.mjs/.cjs`). Both ESM (`import`) and CommonJS (`require`, including the inline `require("fastify")(...)` form) are supported; injection lands after the full creation statement, so multi-line options objects and chained calls (e.g. `.withTypeProvider()`) are safe. When no entry or creation call is found, a post-instruction with the quickstart link is printed instead.

### iOS (Swift) / Android (Kotlin)

No files are written. The scaffold plan prints the quickstart steps: SDK install (Swift Package Manager for `ClerkKit`/`ClerkKitUI`, Gradle for `com.clerk:clerk-android-*`), enabling the Native API and registering the app on the Dashboard's Native Applications page, and configuring the publishable key in source (`Clerk.configure(...)` / `Clerk.initialize(...)`) by copying it from the pulled env file.

## Agent skills install

After scaffolding (and after env keys are pulled or keyless instructions are printed), `clerk init` offers to install Clerk's agent skills via the [`skills`](https://www.npmjs.com/package/skills) CLI. The runner is detected from the project's package manager (`bunx`, `npx`, `pnpm dlx`, or `yarn dlx`), so a Bun project installs via `bunx skills add ...`, a pnpm project via `pnpm dlx skills add ...`, and so on. This step is optional and non-fatal: if no package runner is available on PATH or an install command exits non-zero, init prints a yellow warning with a runner-appropriate manual command and still exits successfully.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli-core/src/commands/init/bootstrap-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ export const BOOTSTRAP_AUTHENTICATED_REGISTRY: BootstrapEntry[] = [
"vanilla",
],
},
{
label: "Expo",
dep: "expo",
defaultProjectName: "my-clerk-expo-app",
// create-expo-app has no git flag; it skips `git init` on its own inside
// an existing repo or CI. --no-install keeps parity with the other entries
// (our own installDependencies() step runs with the selected PM).
buildCommand: (pm, name) => [
...runner(pm),
"create-expo-app@latest",
name,
"--template",
"default",
"--no-install",
"--yes",
],
},
];

/** All bootstrap-capable frameworks (keyless + authenticated). */
Expand Down
16 changes: 14 additions & 2 deletions packages/cli-core/src/commands/init/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ function entryFor(dep: string) {
describe("BOOTSTRAP_REGISTRY", () => {
const packageManagers = ["npm", "yarn", "pnpm", "bun"] as const;

test("contains all 8 supported frameworks", () => {
expect(BOOTSTRAP_REGISTRY).toHaveLength(8);
test("contains all 9 supported frameworks", () => {
expect(BOOTSTRAP_REGISTRY).toHaveLength(9);
const deps = BOOTSTRAP_REGISTRY.map((e) => e.dep);
expect(deps).toContain("next");
expect(deps).toContain("react");
Expand All @@ -29,6 +29,7 @@ describe("BOOTSTRAP_REGISTRY", () => {
expect(deps).toContain("nuxt");
expect(deps).toContain("@tanstack/react-start");
expect(deps).toContain("vite");
expect(deps).toContain("expo");
});

const REGISTRY_PM_PAIRS = BOOTSTRAP_REGISTRY.flatMap((entry) =>
Expand Down Expand Up @@ -114,6 +115,17 @@ describe("BOOTSTRAP_REGISTRY", () => {
expect(cmd).toContain("--no-git");
});

test("Expo uses create-expo-app with default template", () => {
const cmd = entryFor("expo").buildCommand("bun", "my-expo");
expect(cmd[0]).toBe("bunx");
expect(cmd).toContain("create-expo-app@latest");
expect(cmd).toContain("my-expo");
expect(cmd).toContain("--template");
expect(cmd).toContain("default");
expect(cmd).toContain("--no-install");
expect(cmd).toContain("--yes");
});

test("JavaScript uses create-vite with vanilla template", () => {
const cmd = entryFor("vite").buildCommand("npm", "my-js-app");
expect(cmd).toContain("create-vite@latest");
Expand Down
50 changes: 50 additions & 0 deletions packages/cli-core/src/commands/init/frameworks/android.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { test, expect } from "bun:test";
import { android } from "./android.ts";
import type { ProjectContext } from "./types.ts";

function makeCtx(): ProjectContext {
return {
cwd: "/tmp/android-app",
framework: {
dep: "android",
name: "Android (Kotlin)",
sdk: "com.clerk:clerk-android-ui",
envVar: "CLERK_PUBLISHABLE_KEY",
envFile: ".env" as const,
ecosystem: "gradle" as const,
},
typescript: false,
srcDir: false,
packageManager: "npm",
existingClerk: false,
deps: {},
envFile: ".env",
};
}

test("matches only the android framework", () => {
const ctx = makeCtx();
expect(android.matches(ctx)).toBe(true);
expect(android.matches({ ...ctx, framework: { ...ctx.framework, dep: "ios" } })).toBe(false);
});

test("writes no files and prints the quickstart steps", async () => {
const plan = await android.scaffold(makeCtx());

expect(plan.actions).toHaveLength(0);
expect(plan.postInstructions.some((i) => i.includes("com.clerk:clerk-android-ui"))).toBe(true);
expect(
plan.postInstructions.some((i) => i.includes("dashboard.clerk.com/~/native-applications")),
).toBe(true);
expect(plan.postInstructions.some((i) => i.includes("Clerk.initialize"))).toBe(true);
expect(plan.postInstructions.some((i) => i.includes("android.permission.INTERNET"))).toBe(true);
expect(
plan.postInstructions.some((i) => i.includes("docs/android/getting-started/quickstart")),
).toBe(true);
});

test("references the project's env file for the publishable key", async () => {
const plan = await android.scaffold({ ...makeCtx(), envFile: ".env.local" });

expect(plan.postInstructions.some((i) => i.includes(".env.local"))).toBe(true);
});
33 changes: 33 additions & 0 deletions packages/cli-core/src/commands/init/frameworks/android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { FrameworkScaffold, ProjectContext, ScaffoldPlan } from "./types.js";

/**
* Android (Kotlin) support for `clerk init`.
*
* The Clerk Android SDK ships via Gradle (`com.clerk:` artifacts) and the
* publishable key is configured in Kotlin source (`Clerk.initialize(...)`),
* not an env file. Gradle files are user-managed build scripts with too many
* layout variants (Groovy/Kotlin DSL, version catalogs) to modify safely, so
* this scaffolder prints the exact quickstart steps; `clerk init` still links
* the app and pulls real keys so the user can copy the publishable key.
*
* Docs: https://clerk.com/docs/android/getting-started/quickstart
*/
export const android: FrameworkScaffold = {
name: "Android (Kotlin)",
dep: "android",

matches: (ctx) => ctx.framework.dep === "android",

async scaffold(ctx: ProjectContext): Promise<ScaffoldPlan> {
return {
actions: [],
postInstructions: [
'Add the Clerk Android SDK to app/build.gradle.kts: `implementation("com.clerk:clerk-android-ui:<latest-version>")` and `implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2")` (requires minSdk 24+ and Java 17+; latest version: https://github.com/clerk/clerk-android/releases)',
"Enable the Native API and register your Android app on the Native Applications page: https://dashboard.clerk.com/~/native-applications",
'Add `<uses-permission android:name="android.permission.INTERNET"/>` to AndroidManifest.xml and register an Application subclass via `android:name`',
`Initialize Clerk in your Application subclass: \`Clerk.initialize(this, publishableKey = "<publishable key>")\` — copy CLERK_PUBLISHABLE_KEY from ${ctx.envFile} after \`clerk env pull\``,
"Full setup guide: https://clerk.com/docs/android/getting-started/quickstart",
],
};
},
};
Loading