diff --git a/.changeset/required-skills.md b/.changeset/required-skills.md new file mode 100644 index 00000000..21227a7e --- /dev/null +++ b/.changeset/required-skills.md @@ -0,0 +1,5 @@ +--- +"clerk": patch +--- + +Install the full Clerk core and feature skill sets by default during `clerk init`. Agents now get context for `clerk-custom-ui`, `clerk-backend-api`, `clerk-orgs`, `clerk-testing`, and `clerk-webhooks` in addition to the previous defaults, plus a framework-specific skill when one matches. Pass `--no-skills` to opt out. diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index 55676090..e4ec809a 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -63,7 +63,7 @@ Use `--prompt` to output a setup prompt for an AI agent without running init. 14. Prints a summary of created, modified, and skipped files with recommendations 15. **Authenticated mode**: pulls development instance API keys via `clerk env pull` 16. **Unauthenticated mode**: prints instructions for development without API keys and how to connect a Clerk account later -17. Optionally installs framework-specific Clerk agent skills via the project's package runner (see [Agent skills install](#agent-skills-install)) +17. Optionally installs Clerk agent skills (core + features, plus a framework-specific skill) via the project's package runner (see [Agent skills install](#agent-skills-install)) ## Framework Detection @@ -197,9 +197,14 @@ At install time, [`skills.ts`](./skills.ts) stages the bundled content into a fr The `skills` CLI writes the installed files into each agent's skill directory (`.claude/skills/clerk/`, `.cursor/skills/clerk/`, etc.) and records the entry in the project's `skills-lock.json` with `sourceType: "local"`, which correctly excludes it from `skills update` (the skill can only change when the CLI itself is upgraded). -### 2. The upstream framework-pattern skills +### 2. The upstream skills -The base skills `clerk` and `clerk-setup` are always included from [`clerk/skills`](https://github.com/clerk/skills). The detected framework dependency adds a matching skill: +A fixed default set is always installed from [`clerk/skills`](https://github.com/clerk/skills), covering the `core/` and `features/` directories: + +- **Core**: `clerk-setup`, `clerk-custom-ui`, `clerk-backend-api` +- **Features**: `clerk-orgs`, `clerk-testing`, `clerk-webhooks` + +The detected framework dependency adds one more skill on top: | Framework dep | Added skill | | ----------------------- | ----------------------------- | @@ -211,8 +216,8 @@ The base skills `clerk` and `clerk-setup` are always included from [`clerk/skill | `astro` | `clerk-astro-patterns` | | `@tanstack/react-start` | `clerk-tanstack-patterns` | | `expo` | `clerk-expo-patterns` | -| `express` | `clerk-backend-api` | -| `fastify` | `clerk-backend-api` | + +Express and Fastify projects don't get a framework-specific skill — `clerk-backend-api` (now a default) already covers their needs. These skills version independently of the CLI, so no pin is applied. diff --git a/packages/cli-core/src/commands/init/skills.test.ts b/packages/cli-core/src/commands/init/skills.test.ts new file mode 100644 index 00000000..9e7155eb --- /dev/null +++ b/packages/cli-core/src/commands/init/skills.test.ts @@ -0,0 +1,47 @@ +import { test, expect, describe } from "bun:test"; +import { formatSkillsPromptMessage, resolveUpstreamSkills } from "./skills.ts"; + +const DEFAULTS = [ + "clerk-setup", + "clerk-custom-ui", + "clerk-backend-api", + "clerk-orgs", + "clerk-testing", + "clerk-webhooks", +]; + +describe("resolveUpstreamSkills", () => { + test("returns the 6 defaults when no framework is detected", () => { + expect(resolveUpstreamSkills(undefined)).toEqual(DEFAULTS); + }); + + test("appends the framework skill for a known dep", () => { + expect(resolveUpstreamSkills("next")).toEqual([...DEFAULTS, "clerk-nextjs-patterns"]); + }); + + test("returns just the defaults for express (clerk-backend-api is already a default)", () => { + expect(resolveUpstreamSkills("express")).toEqual(DEFAULTS); + }); + + test("returns just the defaults for fastify (clerk-backend-api is already a default)", () => { + expect(resolveUpstreamSkills("fastify")).toEqual(DEFAULTS); + }); + + test("returns just the defaults for an unknown framework dep", () => { + expect(resolveUpstreamSkills("svelte")).toEqual(DEFAULTS); + }); +}); + +describe("formatSkillsPromptMessage", () => { + test("summarizes without a framework skill", () => { + expect(formatSkillsPromptMessage(undefined)).toBe( + "Install agent skills? (clerk core + features)", + ); + }); + + test("strips the clerk- prefix from the framework skill", () => { + expect(formatSkillsPromptMessage("clerk-nextjs-patterns")).toBe( + "Install agent skills? (clerk core + features + nextjs-patterns)", + ); + }); +}); diff --git a/packages/cli-core/src/commands/init/skills.ts b/packages/cli-core/src/commands/init/skills.ts index 47de35ae..d113da78 100644 --- a/packages/cli-core/src/commands/init/skills.ts +++ b/packages/cli-core/src/commands/init/skills.ts @@ -7,9 +7,10 @@ * imports). Delegated to the `clerk skill install` core helpers in * `commands/skill/install.ts`. * - * 2. The framework-pattern skills (`clerk-setup`, - * `clerk--patterns`) ship from the upstream `clerk/skills` - * repo and version independently of the CLI. + * 2. The upstream skills (`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 + * upstream `clerk/skills` repo and version independently of the CLI. * * The skills CLI itself handles agent auto-detection and scope selection: * in interactive mode we hand off entirely (no `--agent` / `-y`), so the @@ -23,10 +24,19 @@ import { confirm } from "../../lib/prompts.js"; import type { ProjectContext } from "./frameworks/types.js"; import { installClerkSkillCore, resolveSkillsRunner, runSkillsAdd } from "../skill/install.js"; -/** Upstream skills installed regardless of framework. */ -const BASE_SKILLS = ["clerk-setup"]; +/** Upstream skills from clerk/skills — installed on every project (excludes the bundled clerk skill). */ +const DEFAULT_UPSTREAM_SKILLS = [ + // core/ + "clerk-setup", + "clerk-custom-ui", + "clerk-backend-api", + // features/ + "clerk-orgs", + "clerk-testing", + "clerk-webhooks", +]; -/** Maps framework dep (from package.json) to the upstream skill name. */ +// 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", @@ -36,19 +46,39 @@ const FRAMEWORK_SKILL_MAP: Record = { astro: "clerk-astro-patterns", "@tanstack/react-start": "clerk-tanstack-patterns", expo: "clerk-expo-patterns", - express: "clerk-backend-api", - fastify: "clerk-backend-api", }; +// 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.`, + ); + } +} + /** Source for upstream framework-pattern skills. */ const UPSTREAM_SKILLS_SOURCE = "clerk/skills"; -function resolveUpstreamSkills(frameworkDep: string | undefined): string[] { - const skills = [...BASE_SKILLS]; +export function resolveUpstreamSkills(frameworkDep: string | undefined): string[] { + const skills = new Set(DEFAULT_UPSTREAM_SKILLS); if (frameworkDep && FRAMEWORK_SKILL_MAP[frameworkDep]) { - skills.push(FRAMEWORK_SKILL_MAP[frameworkDep]); + skills.add(FRAMEWORK_SKILL_MAP[frameworkDep]); } - return skills; + return [...skills]; +} + +export function getFrameworkSkill(frameworkDep: string | undefined): string | undefined { + return frameworkDep ? FRAMEWORK_SKILL_MAP[frameworkDep] : undefined; +} + +function formatSkillsSummary(frameworkSkill: string | undefined): string { + const framework = frameworkSkill ? ` + ${frameworkSkill.replace(/^clerk-/, "")}` : ""; + return `clerk core + features${framework}`; +} + +export function formatSkillsPromptMessage(frameworkSkill: string | undefined): string { + return `Install agent skills? (${formatSkillsSummary(frameworkSkill)})`; } export async function installSkills( @@ -58,11 +88,11 @@ export async function installSkills( skipPrompt: boolean, ): Promise { const upstreamSkills = resolveUpstreamSkills(frameworkDep); - const skillList = ["clerk", ...upstreamSkills].join(", "); + const frameworkSkill = getFrameworkSkill(frameworkDep); if (isHuman() && !skipPrompt) { const install = await confirm({ - message: `Install agent skills? (${skillList})`, + message: formatSkillsPromptMessage(frameworkSkill), default: true, }); if (!install) return; @@ -79,6 +109,7 @@ export async function installSkills( // problem with one source doesn't block the other. const cliSkillOk = await installClerkSkillCore(runner, cwd, interactive); + log.debug(`skills: upstream install — ${upstreamSkills.join(", ")}`); const upstreamOk = await runSkillsAdd( runner, cwd, @@ -86,7 +117,7 @@ export async function installSkills( upstreamSkills, interactive, false, - upstreamSkills.join(", "), + formatSkillsSummary(frameworkSkill), ); if (cliSkillOk && upstreamOk) {