Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/remove-clerk-cli-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": major
---

Remove the bundled `clerk skill install` command and install the `clerk-cli` agent skill from the [`clerk/skills`](https://github.com/clerk/skills) repository during `clerk init`.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ Commands:
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
doctor [options] Check your project's Clerk integration health
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
$ clerk skill install
```
86 changes: 7 additions & 79 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test, expect, describe } from "bun:test";
import { createProgram, formatApiBody } from "./cli-program.ts";
import { ApiError } from "./lib/errors.ts";
import { STANDARD_AGENT_DIRS, EXTRA_REL_PATHS } from "./lib/skill-detection.ts";

test("registers users as a top-level command", () => {
const program = createProgram();
const users = program.commands.find((command) => command.name() === "users");
expect(users).toBeDefined();
});

test("does not register the removed clerk skill command", () => {
const program = createProgram();
const skill = program.commands.find((command) => command.name() === "skill");
expect(skill).toBeUndefined();
});

test("registers users create and list as subcommands", () => {
const program = createProgram();
const users = program.commands.find((command) => command.name() === "users")!;
Expand Down Expand Up @@ -344,77 +346,3 @@ describe("formatApiBody", () => {
expect(result).toBe("Plan limitation");
});
});

describe("help: clerk skill install tip", () => {
const TIP_SUBSTR = "Give AI agents better Clerk context";

// Capture help output including `addHelpText("after", ...)`. The custom
// formatter in lib/help.ts rebuilds help from scratch, so the "after"
// listener only fires during outputHelp (which writes via stdout).
function renderHelp(): string {
const program = createProgram();
let captured = "";
const origWrite = process.stdout.write.bind(process.stdout);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(process.stdout as unknown as { write: (chunk: any) => boolean }).write = (chunk) => {
captured += typeof chunk === "string" ? chunk : chunk.toString();
return true;
};
try {
program.outputHelp();
} finally {
(process.stdout as unknown as { write: typeof origWrite }).write = origWrite;
}
return captured;
}

let tmpHome: string;
let tmpCwd: string;
let originalHome: string | undefined;
let originalCwd: string;

beforeEach(() => {
originalHome = process.env.HOME;
originalCwd = process.cwd();
tmpHome = mkdtempSync(join(tmpdir(), "clerk-help-home-"));
tmpCwd = mkdtempSync(join(tmpdir(), "clerk-help-cwd-"));
process.env.HOME = tmpHome;
process.chdir(tmpCwd);
});

afterEach(() => {
process.chdir(originalCwd);
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
rmSync(tmpHome, { recursive: true, force: true });
rmSync(tmpCwd, { recursive: true, force: true });
});

test("shows the tip when no skill is installed anywhere", () => {
const help = renderHelp();
expect(help).toContain(TIP_SUBSTR);
expect(help).toContain("clerk skill install");
});

const AGENT_DIR_CASES = STANDARD_AGENT_DIRS.flatMap((dir) =>
(["HOME", "cwd"] as const).map((root) => ({ dir, root })),
);

test.each(AGENT_DIR_CASES)(
"hides the tip when $dir/skills/clerk-cli/SKILL.md exists under $root",
({ dir, root }) => {
const base = root === "HOME" ? tmpHome : tmpCwd;
const target = join(base, dir, "skills/clerk-cli");
mkdirSync(target, { recursive: true });
writeFileSync(join(target, "SKILL.md"), "ok");
expect(renderHelp()).not.toContain(TIP_SUBSTR);
},
);

test.each([...EXTRA_REL_PATHS])("hides the tip when %s exists under cwd", (rel) => {
const full = join(tmpCwd, rel);
mkdirSync(dirname(full), { recursive: true });
writeFileSync(full, "ok");
expect(renderHelp()).not.toContain(TIP_SUBSTR);
});
});
41 changes: 1 addition & 40 deletions packages/cli-core/src/cli-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
import { completion, SUPPORTED_SHELLS } from "./commands/completion/index.ts";
import { FRAMEWORK_NAMES } from "./lib/framework.ts";
import { PACKAGE_MANAGERS } from "./lib/package-manager.ts";
import { skillInstall } from "./commands/skill/install.ts";
import {
CliError,
UserAbortError,
Expand All @@ -47,7 +46,6 @@ import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { registerExtras } from "@clerk/cli-extras";
Expand Down Expand Up @@ -110,14 +108,7 @@ export function createProgram() {
"--mode <mode>",
"Force interaction mode (human or agent). Defaults to auto-detect based on TTY.",
)
.option("--verbose", "Show detailed output (enables debug messages)")
.addHelpText("after", () =>
isClerkSkillInstalled()
? ""
: `
Give AI agents better Clerk context: install the Clerk skills
$ clerk skill install`,
);
.option("--verbose", "Show detailed output (enables debug messages)");

program.hook("preAction", async () => {
// Reset log level at the start of each command invocation so a previous
Expand Down Expand Up @@ -880,36 +871,6 @@ Tutorial — enable completions for your shell:
)
.action(completion);

const skill = program
.command("skill")
.description("Manage the bundled Clerk CLI agent skill")
.setExamples([
{ command: "clerk skill install", description: "Install the clerk agent skill" },
{
command: "clerk skill install -y",
description: "Install non-interactively (auto-detect agents, global scope)",
},
]);

skill
.command("install")
.description("Install the bundled clerk agent skill")
.option("-y, --yes", "Skip prompts and run the `skills` CLI unattended")
.addOption(
createOption("--pm <manager>", "Package manager hint for runner detection").choices(
PACKAGE_MANAGERS,
),
)
.setExamples([
{ command: "clerk skill install", description: "Install with an interactive runner picker" },
{ command: "clerk skill install -y", description: "Install unattended" },
{
command: "clerk skill install --pm bun",
description: "Force bunx as the runner",
},
])
.action(skillInstall);

program
.command("update")
.description("Update the Clerk CLI to the latest version")
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-core/src/commands/billing/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function resetSkillStubs() {
skillCalls.length = 0;
resolveSkillsRunnerStub = () => ({ id: "bunx", display: "bunx" });
}
mock.module("../skill/install.ts", () => ({
mock.module("../../lib/skills.ts", () => ({
resolveSkillsRunner: async () => resolveSkillsRunnerStub(),
runSkillsAdd: async (
_runner: unknown,
Expand Down
3 changes: 1 addition & 2 deletions packages/cli-core/src/commands/billing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { log } from "../../lib/log.ts";
import { confirm } from "../../lib/prompts.ts";
import { detectPackageManager } from "../../lib/package-manager.ts";
import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts";
import { resolveSkillsRunner, runSkillsAdd } from "../../lib/skills.ts";
import { applyConfigPatch } from "../config/apply-patch.ts";
import { resolveSkillsRunner, runSkillsAdd } from "../skill/install.ts";

interface BillingOptions {
app?: string;
Expand Down Expand Up @@ -109,7 +109,6 @@ async function offerBillingSkillInstall(options: BillingOptions): Promise<void>
"clerk/skills",
["clerk-billing"],
interactive,
false,
"clerk-billing",
);
if (installed) {
Expand Down
19 changes: 4 additions & 15 deletions packages/cli-core/src/commands/init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful
13. Prints a summary of created, modified, and skipped files with recommendations
14. **Authenticated mode**: pulls development instance API keys via `clerk env pull`
15. **Unauthenticated mode**: prints instructions for development without API keys and how to connect a Clerk account later
16. 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))
16. Optionally installs Clerk agent skills (cli + core + features, plus a framework-specific skill) via the project's package runner (see [Agent skills install](#agent-skills-install))

## Framework Detection

Expand Down Expand Up @@ -190,20 +190,9 @@ After scaffolding (and after env keys are pulled or keyless instructions are pri
- **Human mode**: prompts `Install agent skills? (...)` defaulting to yes. Pass `--no-skills` to suppress the prompt entirely, or `-y/--yes` to accept it without confirmation. When more than one runner is available, a second prompt picks which one to use (the project's package manager wins by default).
- **Agent mode**: skills are installed non-interactively with `-y -g` flags (no prompt shown). Pass `--no-skills` to skip entirely.

Two install commands run, sharing one runner:

### 1. The bundled `clerk-cli` skill

The `clerk-cli` skill ships **inside the CLI binary**. Its markdown files at [`<repo-root>/skills/clerk-cli/`](../../../../../skills/clerk-cli/) are pulled into [`skills.ts`](./skills.ts) as [text imports](https://bun.com/docs/bundler/loaders#text) (`import md from "./SKILL.md" with { type: "text" }`) and embedded by `bun build --compile`, so the skill content always matches the binary running it. No network, no tag, no version fallback.

At install time, [`skills.ts`](./skills.ts) stages the bundled content into a fresh temp directory (`mkdtemp`) and invokes `<runner> skills add <tmpdir> --copy`. The `--copy` flag is required: the default symlink mode would point each agent's skill dir at the temp dir, which we delete immediately after the install completes.

The `skills` CLI writes the installed files into each agent's skill directory (`.claude/skills/clerk-cli/`, `.cursor/skills/clerk-cli/`, 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 skills

A fixed default set is always installed from [`clerk/skills`](https://github.com/clerk/skills), covering the `core/` and `features/` directories:
A fixed default set is installed from [`clerk/skills`](https://github.com/clerk/skills), covering the `cli/`, `core/`, and `features/` directories:

- **CLI**: `clerk-cli`
- **Core**: `clerk-setup`, `clerk-custom-ui`, `clerk-backend-api`
- **Features**: `clerk-orgs`, `clerk-testing`, `clerk-webhooks`

Expand All @@ -226,7 +215,7 @@ These skills version independently of the CLI, so no pin is applied.

### Failure handling

The two install commands fail independently: a problem with the bundled `clerk-cli` skill install (e.g. the `skills` CLI can't be fetched by the runner) does not block the upstream skills install, and vice versa. Each failure prints its own yellow warning with a manual install command (where applicable — the bundled `clerk-cli` skill has no standalone manual command, since its source lives in the binary). Init continues and exits successfully either way.
The skills install is optional and non-fatal. If the `skills` CLI can't be fetched by the runner or exits non-zero, init prints a yellow warning with a manual install command and still exits successfully.

Implementation lives in [`skills.ts`](./skills.ts). Note that the E2E fixture setup runs `clerk init --yes --no-skills` because the framework template skills reference auto-generated types (e.g. React Router's `./+types/root`) that don't exist outside a real app directory and would break the fixture's `tsc` step.

Expand Down
7 changes: 4 additions & 3 deletions packages/cli-core/src/commands/init/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test, expect, describe } from "bun:test";
import { formatSkillsPromptMessage, resolveUpstreamSkills } from "./skills.ts";

const DEFAULTS = [
"clerk-cli",
"clerk-setup",
"clerk-custom-ui",
"clerk-backend-api",
Expand All @@ -11,7 +12,7 @@ const DEFAULTS = [
];

describe("resolveUpstreamSkills", () => {
test("returns the 6 defaults when no framework is detected", () => {
test("returns the 7 defaults when no framework is detected", () => {
expect(resolveUpstreamSkills(undefined)).toEqual(DEFAULTS);
});

Expand All @@ -35,13 +36,13 @@ describe("resolveUpstreamSkills", () => {
describe("formatSkillsPromptMessage", () => {
test("summarizes without a framework skill", () => {
expect(formatSkillsPromptMessage(undefined)).toBe(
"Install agent skills? (clerk core + features)",
"Install agent skills? (clerk-cli + 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)",
"Install agent skills? (clerk-cli + core + features + nextjs-patterns)",
);
});
});
30 changes: 10 additions & 20 deletions packages/cli-core/src/commands/init/skills.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
/**
* Install Clerk agent skills after scaffolding.
*
* Two installer calls share one runner detection:
*
* 1. The bundled `clerk` skill (embedded in the binary via text
* imports). Delegated to the `clerk skill install` core helpers in
* `commands/skill/install.ts`.
*
* 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 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
* 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
Expand All @@ -21,11 +15,13 @@
import { isHuman } from "../../mode.js";
import { log } from "../../lib/log.js";
import { confirm } from "../../lib/prompts.js";
import { resolveSkillsRunner, runSkillsAdd } from "../../lib/skills.js";
import type { ProjectContext } from "./frameworks/types.js";
import { installClerkSkillCore, resolveSkillsRunner, runSkillsAdd } from "../skill/install.js";

/** Upstream skills from clerk/skills — installed on every project (excludes the bundled clerk skill). */
/** Upstream skills from clerk/skills — installed on every project. */
const DEFAULT_UPSTREAM_SKILLS = [
// cli/
"clerk-cli",
// core/
"clerk-setup",
"clerk-custom-ui",
Expand Down Expand Up @@ -74,7 +70,7 @@ export function getFrameworkSkill(frameworkDep: string | undefined): string | un

function formatSkillsSummary(frameworkSkill: string | undefined): string {
const framework = frameworkSkill ? ` + ${frameworkSkill.replace(/^clerk-/, "")}` : "";
return `clerk core + features${framework}`;
return `clerk-cli + core + features${framework}`;
}

export function formatSkillsPromptMessage(frameworkSkill: string | undefined): string {
Expand Down Expand Up @@ -104,23 +100,17 @@ export async function installSkills(
const runner = await resolveSkillsRunner(packageManager, interactive);
if (!runner) return;

// Install the bundled clerk skill from a staged temp dir, then the
// upstream framework patterns. Each call soft-fails independently so a
// 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,
UPSTREAM_SKILLS_SOURCE,
upstreamSkills,
interactive,
false,
formatSkillsSummary(frameworkSkill),
);

if (cliSkillOk && upstreamOk) {
if (upstreamOk) {
log.blank();
log.success("Agent skills installed. AI agents now have Clerk context in this project.");
}
Expand Down
34 changes: 0 additions & 34 deletions packages/cli-core/src/commands/skill/README.md

This file was deleted.

Loading