From aa36552baf79058e2f27c8edfb980b7d6058dc60 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Wed, 10 Jun 2026 15:23:29 -0500 Subject: [PATCH 1/2] feat(init): install the clerk-expo setup skill for Expo projects The expo entry in FRAMEWORK_SKILL_MAP (added in #86, 2026-04) predates the mobile/clerk-expo setup skill (added to clerk/skills in 2026-05), so Expo inits only got clerk-expo-patterns. The map now supports multiple skills per framework and Expo installs both. Co-Authored-By: Claude Fable 5 --- .changeset/init-expo-setup-skill.md | 5 ++ .../cli-core/src/commands/init/skills.test.ts | 20 ++++++- packages/cli-core/src/commands/init/skills.ts | 55 ++++++++++--------- 3 files changed, 51 insertions(+), 29 deletions(-) create mode 100644 .changeset/init-expo-setup-skill.md diff --git a/.changeset/init-expo-setup-skill.md b/.changeset/init-expo-setup-skill.md new file mode 100644 index 00000000..f1e785b7 --- /dev/null +++ b/.changeset/init-expo-setup-skill.md @@ -0,0 +1,5 @@ +--- +"clerk": minor +--- + +`clerk init` on Expo projects now installs the `clerk-expo` agent skill (Expo/React Native setup) alongside `clerk-expo-patterns`. The framework skill map supports multiple skills per framework. diff --git a/packages/cli-core/src/commands/init/skills.test.ts b/packages/cli-core/src/commands/init/skills.test.ts index badda335..a0479649 100644 --- a/packages/cli-core/src/commands/init/skills.test.ts +++ b/packages/cli-core/src/commands/init/skills.test.ts @@ -20,6 +20,14 @@ describe("resolveUpstreamSkills", () => { expect(resolveUpstreamSkills("next")).toEqual([...DEFAULTS, "clerk-nextjs-patterns"]); }); + test("appends both the setup and patterns skills for expo", () => { + expect(resolveUpstreamSkills("expo")).toEqual([ + ...DEFAULTS, + "clerk-expo", + "clerk-expo-patterns", + ]); + }); + test("returns just the defaults for express (clerk-backend-api is already a default)", () => { expect(resolveUpstreamSkills("express")).toEqual(DEFAULTS); }); @@ -34,15 +42,21 @@ describe("resolveUpstreamSkills", () => { }); describe("formatSkillsPromptMessage", () => { - test("summarizes without a framework skill", () => { - expect(formatSkillsPromptMessage(undefined)).toBe( + test("summarizes without framework skills", () => { + expect(formatSkillsPromptMessage([])).toBe( "Install agent skills? (clerk-cli + core + features)", ); }); test("strips the clerk- prefix from the framework skill", () => { - expect(formatSkillsPromptMessage("clerk-nextjs-patterns")).toBe( + expect(formatSkillsPromptMessage(["clerk-nextjs-patterns"])).toBe( "Install agent skills? (clerk-cli + core + features + nextjs-patterns)", ); }); + + test("lists every framework skill when a dep maps to more than one", () => { + expect(formatSkillsPromptMessage(["clerk-expo", "clerk-expo-patterns"])).toBe( + "Install agent skills? (clerk-cli + core + features + expo + expo-patterns)", + ); + }); }); diff --git a/packages/cli-core/src/commands/init/skills.ts b/packages/cli-core/src/commands/init/skills.ts index d460b631..097b95e9 100644 --- a/packages/cli-core/src/commands/init/skills.ts +++ b/packages/cli-core/src/commands/init/skills.ts @@ -3,7 +3,7 @@ * * The upstream skills (`clerk-cli`, `clerk-setup`, `clerk-custom-ui`, * `clerk-backend-api`, `clerk-orgs`, `clerk-testing`, `clerk-webhooks`, - * plus a framework-specific skill when one matches) ship from the + * plus framework-specific skills when any match) ship from the * upstream `clerk/skills` repo and version independently of the CLI. * * The skills CLI itself handles agent auto-detection and scope selection: @@ -33,23 +33,26 @@ const DEFAULT_UPSTREAM_SKILLS = [ ]; // Express/Fastify have no entry — their skill is clerk-backend-api, which is a default. -const FRAMEWORK_SKILL_MAP: Record = { - next: "clerk-nextjs-patterns", - react: "clerk-react-patterns", - "react-router": "clerk-react-router-patterns", - vue: "clerk-vue-patterns", - nuxt: "clerk-nuxt-patterns", - astro: "clerk-astro-patterns", - "@tanstack/react-start": "clerk-tanstack-patterns", - expo: "clerk-expo-patterns", +const FRAMEWORK_SKILL_MAP: Record = { + next: ["clerk-nextjs-patterns"], + react: ["clerk-react-patterns"], + "react-router": ["clerk-react-router-patterns"], + vue: ["clerk-vue-patterns"], + nuxt: ["clerk-nuxt-patterns"], + astro: ["clerk-astro-patterns"], + "@tanstack/react-start": ["clerk-tanstack-patterns"], + // Expo gets the mobile setup skill (mobile/clerk-expo) plus the patterns skill. + expo: ["clerk-expo", "clerk-expo-patterns"], }; // Guard against accidental overlap: Set.add() silently deduplicates, masking dead entries. -for (const [dep, skill] of Object.entries(FRAMEWORK_SKILL_MAP)) { - if (DEFAULT_UPSTREAM_SKILLS.includes(skill)) { - throw new Error( - `FRAMEWORK_SKILL_MAP['${dep}'] maps to '${skill}', which is already in DEFAULT_UPSTREAM_SKILLS. Remove it from the map.`, - ); +for (const [dep, skills] of Object.entries(FRAMEWORK_SKILL_MAP)) { + for (const skill of skills) { + if (DEFAULT_UPSTREAM_SKILLS.includes(skill)) { + throw new Error( + `FRAMEWORK_SKILL_MAP['${dep}'] includes '${skill}', which is already in DEFAULT_UPSTREAM_SKILLS. Remove it from the map.`, + ); + } } } @@ -58,23 +61,23 @@ const UPSTREAM_SKILLS_SOURCE = "clerk/skills"; export function resolveUpstreamSkills(frameworkDep: string | undefined): string[] { const skills = new Set(DEFAULT_UPSTREAM_SKILLS); - if (frameworkDep && FRAMEWORK_SKILL_MAP[frameworkDep]) { - skills.add(FRAMEWORK_SKILL_MAP[frameworkDep]); + for (const skill of getFrameworkSkills(frameworkDep)) { + skills.add(skill); } return [...skills]; } -export function getFrameworkSkill(frameworkDep: string | undefined): string | undefined { - return frameworkDep ? FRAMEWORK_SKILL_MAP[frameworkDep] : undefined; +export function getFrameworkSkills(frameworkDep: string | undefined): string[] { + return (frameworkDep && FRAMEWORK_SKILL_MAP[frameworkDep]) || []; } -function formatSkillsSummary(frameworkSkill: string | undefined): string { - const framework = frameworkSkill ? ` + ${frameworkSkill.replace(/^clerk-/, "")}` : ""; +function formatSkillsSummary(frameworkSkills: readonly string[]): string { + const framework = frameworkSkills.map((skill) => ` + ${skill.replace(/^clerk-/, "")}`).join(""); return `clerk-cli + core + features${framework}`; } -export function formatSkillsPromptMessage(frameworkSkill: string | undefined): string { - return `Install agent skills? (${formatSkillsSummary(frameworkSkill)})`; +export function formatSkillsPromptMessage(frameworkSkills: readonly string[]): string { + return `Install agent skills? (${formatSkillsSummary(frameworkSkills)})`; } export async function installSkills( @@ -84,11 +87,11 @@ export async function installSkills( skipPrompt: boolean, ): Promise { const upstreamSkills = resolveUpstreamSkills(frameworkDep); - const frameworkSkill = getFrameworkSkill(frameworkDep); + const frameworkSkills = getFrameworkSkills(frameworkDep); if (isHuman() && !skipPrompt) { const install = await confirm({ - message: formatSkillsPromptMessage(frameworkSkill), + message: formatSkillsPromptMessage(frameworkSkills), default: true, }); if (!install) return; @@ -107,7 +110,7 @@ export async function installSkills( UPSTREAM_SKILLS_SOURCE, upstreamSkills, interactive, - formatSkillsSummary(frameworkSkill), + formatSkillsSummary(frameworkSkills), ); if (upstreamOk) { From 438581cb6b153590a1cd08f292c9fe444b43cc24 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Fri, 12 Jun 2026 12:14:03 -0500 Subject: [PATCH 2/2] refactor(init): make framework skill arrays readonly Review feedback on the FRAMEWORK_SKILL_MAP leak: type the map as Record so immutability is declared at the source and getFrameworkSkills inherits it, instead of copying at runtime. Co-Authored-By: Claude Fable 5 --- packages/cli-core/src/commands/init/skills.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli-core/src/commands/init/skills.ts b/packages/cli-core/src/commands/init/skills.ts index 097b95e9..78723fb7 100644 --- a/packages/cli-core/src/commands/init/skills.ts +++ b/packages/cli-core/src/commands/init/skills.ts @@ -33,7 +33,7 @@ const DEFAULT_UPSTREAM_SKILLS = [ ]; // Express/Fastify have no entry — their skill is clerk-backend-api, which is a default. -const FRAMEWORK_SKILL_MAP: Record = { +const FRAMEWORK_SKILL_MAP: Record = { next: ["clerk-nextjs-patterns"], react: ["clerk-react-patterns"], "react-router": ["clerk-react-router-patterns"], @@ -67,7 +67,7 @@ export function resolveUpstreamSkills(frameworkDep: string | undefined): string[ return [...skills]; } -export function getFrameworkSkills(frameworkDep: string | undefined): string[] { +export function getFrameworkSkills(frameworkDep: string | undefined): readonly string[] { return (frameworkDep && FRAMEWORK_SKILL_MAP[frameworkDep]) || []; }