Skip to content

Commit f0d314f

Browse files
committed
feat(init): version-pinned clerk-cli skill installer
Adds the clerk-cli skill at <repo-root>/skills/clerk-cli/ as a sibling to the CLI source, distributed alongside the CLI but not bundled into the binary. The skill is installed at clerk init time via the same `npx skills add` mechanism as the upstream framework-pattern skills. The clerk-cli source URL is constructed from CLI_VERSION: https://github.com/clerk/cli/tree/v${CLI_VERSION}/skills/clerk-cli Every published release (stable, canary, snapshot) tags as v${version} per release.yml/snapshot.ts, so the same template works for all of them. Dev builds (CLI_VERSION undefined or "0.0.0-dev") fall back to the main branch. The two installer calls (clerk-cli + upstream framework skills) share one runner detection and fail independently, a problem with one source doesn't block the other.
1 parent a8903e7 commit f0d314f

7 files changed

Lines changed: 720 additions & 50 deletions

File tree

packages/cli-core/src/commands/init/README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,28 @@ If no entry file is found, a post-instruction is printed pointing to the Clerk J
165165

166166
## Agent skills install
167167

168-
After scaffolding (and after env keys are pulled or keyless instructions are printed), `clerk init` offers to install Clerk's framework-specific agent skills from [`clerk/skills`](https://github.com/clerk/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 clerk/skills`, a pnpm project via `pnpm dlx skills add clerk/skills`, and so on. This step is optional and non-fatal: if no package runner is available on PATH or the install command exits non-zero, init prints a yellow warning with a runner-appropriate manual command and still exits successfully.
168+
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.
169169

170-
- **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).
171-
- **Agent mode / `--prompt`**: `clerk init` exits early before the skills step runs (see the `if (options.prompt || isAgent()) { ... return }` branch in [`index.ts`](./index.ts)), so nothing is installed. Agent users should run `skills add clerk/skills` via their preferred runner manually, or have their agent do it.
170+
- **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 (`bunx`, `npx`, `pnpm dlx`, `yarn dlx`) is available, a second prompt picks the runner; the project's package manager is the default choice.
171+
- **Agent mode / `--prompt`**: `clerk init` exits early before the skills step runs (see the `if (options.prompt || isAgent()) { ... return }` branch in [`index.ts`](./index.ts)), so nothing is installed. Agent users should run `skills add clerk/skills` (and the version-pinned `clerk-cli` skill URL) via their preferred runner manually, or have their agent do it.
172172

173-
The base skills `clerk` and `clerk-setup` are always included. The detected framework dependency adds a matching skill:
173+
Two install commands run, sharing one runner:
174+
175+
### 1. The version-pinned `clerk-cli` skill
176+
177+
The `clerk-cli` skill ships **from this repo** at [`<repo-root>/skills/clerk-cli/`](../../../../../skills/clerk-cli/). It is **not** bundled into the binary, it's fetched at install time by the `skills` CLI, the same way any other skill would be.
178+
179+
The source URL is constructed from `CLI_VERSION` (the compile-time global injected by `scripts/build.ts`):
180+
181+
```
182+
https://github.com/clerk/cli/tree/v<CLI_VERSION>/skills/clerk-cli
183+
```
184+
185+
Every published release (stable, canary, and snapshot) tags as `v${CLI_VERSION}` (see `.github/workflows/release.yml`), so the same `tree/v<version>/skills/clerk-cli` URL works for all of them. The only case without a matching tag is the local-dev sentinel `0.0.0-dev` (set by `scripts/build.ts` when no `--version` is passed), which falls back to `main`.
186+
187+
### 2. The upstream framework-pattern skills
188+
189+
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:
174190

175191
| Framework dep | Added skill |
176192
| ----------------------- | ----------------------------- |
@@ -185,7 +201,13 @@ The base skills `clerk` and `clerk-setup` are always included. The detected fram
185201
| `express` | `clerk-backend-api` |
186202
| `fastify` | `clerk-backend-api` |
187203

188-
Implementation lives in [`skills.ts`](./skills.ts). Note that the E2E fixture setup runs `clerk init --yes --no-skills` because the skill templates reference framework-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.
204+
These skills version independently of the CLI, so no pin is applied.
205+
206+
### Failure handling
207+
208+
The two install commands fail independently, a problem fetching the version-pinned `clerk-cli` skill (e.g. tag doesn't exist yet, network error) does not block the upstream skills install, and vice versa. Each failure prints its own yellow warning with a manual install command. Init continues and exits successfully either way.
209+
210+
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.
189211

190212
## API Endpoints
191213

packages/cli-core/src/commands/init/skills.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { buildSkillsArgs } from "./skills.ts";
33

44
describe("buildSkillsArgs", () => {
55
const skills = ["clerk", "clerk-setup", "clerk-nextjs-patterns"];
6+
const upstream = "clerk/skills";
67

78
test("interactive mode: no -y or -g, lets skills CLI take over", () => {
8-
const args = buildSkillsArgs(skills, true);
9+
const args = buildSkillsArgs(upstream, skills, true);
910
expect(args).toEqual([
1011
"skills",
1112
"add",
@@ -23,14 +24,21 @@ describe("buildSkillsArgs", () => {
2324
});
2425

2526
test("non-interactive mode: includes -y and -g for global auto-detect", () => {
26-
const args = buildSkillsArgs(skills, false);
27+
const args = buildSkillsArgs(upstream, skills, false);
2728
expect(args).toContain("-y");
2829
expect(args).toContain("-g");
2930
expect(args).not.toContain("--agent");
3031
});
3132

3233
test("never passes --agent (lets skills CLI auto-detect)", () => {
33-
expect(buildSkillsArgs(skills, true)).not.toContain("--agent");
34-
expect(buildSkillsArgs(skills, false)).not.toContain("--agent");
34+
expect(buildSkillsArgs(upstream, skills, true)).not.toContain("--agent");
35+
expect(buildSkillsArgs(upstream, skills, false)).not.toContain("--agent");
36+
});
37+
38+
test("empty skillNames omits --skill flags (used for the clerk-cli source)", () => {
39+
const cliUrl = "https://github.com/clerk/cli/tree/main/skills/clerk-cli";
40+
const args = buildSkillsArgs(cliUrl, [], true);
41+
expect(args).toEqual(["skills", "add", cliUrl]);
42+
expect(args).not.toContain("--skill");
3543
});
3644
});

packages/cli-core/src/commands/init/skills.ts

Lines changed: 121 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
/**
22
* Install Clerk agent skills after scaffolding.
33
*
4-
* Maps the detected framework to the appropriate skill set from
5-
* github.com/clerk/skills, then installs via the user's package runner
6-
* (bunx, npx, pnpm dlx, or yarn dlx).
4+
* Two installer calls share one runner detection:
5+
*
6+
* 1. The `clerk-cli` skill ships from this repo (`clerk/cli`) at the
7+
* same git tag as the CLI binary, so the skill content always matches
8+
* the binary the user is running. The source URL is constructed from
9+
* `CLI_VERSION` (compile-time global, falls back to "main" for the
10+
* local-dev sentinel "0.0.0-dev").
11+
*
12+
* 2. The framework-pattern skills (`clerk`, `clerk-setup`,
13+
* `clerk-<framework>-patterns`) ship from the upstream `clerk/skills`
14+
* repo and version independently of the CLI.
715
*
816
* The skills CLI itself handles agent auto-detection and scope selection:
917
* in interactive mode we hand off entirely (no `--agent` / `-y`), so the
@@ -25,10 +33,10 @@ import {
2533
import { isNonEmpty } from "../../lib/helpers/arrays.js";
2634
import type { ProjectContext } from "./frameworks/types.js";
2735

28-
/** Skills installed regardless of framework. */
36+
/** Upstream skills installed regardless of framework. */
2937
const BASE_SKILLS = ["clerk", "clerk-setup"];
3038

31-
/** Maps framework dep (from package.json) to the skill name in clerk/skills. */
39+
/** Maps framework dep (from package.json) to the upstream skill name. */
3240
const FRAMEWORK_SKILL_MAP: Record<string, string> = {
3341
next: "clerk-nextjs-patterns",
3442
react: "clerk-react-patterns",
@@ -42,9 +50,13 @@ const FRAMEWORK_SKILL_MAP: Record<string, string> = {
4250
fastify: "clerk-backend-api",
4351
};
4452

45-
const SKILLS_SOURCE = "clerk/skills";
53+
/** Source for upstream framework-pattern skills. */
54+
const UPSTREAM_SKILLS_SOURCE = "clerk/skills";
4655

47-
function resolveSkills(frameworkDep: string | undefined): string[] {
56+
/** Repo for the version-pinned clerk-cli skill (this repo). */
57+
const CLERK_CLI_REPO = "https://github.com/clerk/cli";
58+
59+
function resolveUpstreamSkills(frameworkDep: string | undefined): string[] {
4860
const skills = [...BASE_SKILLS];
4961
if (frameworkDep && FRAMEWORK_SKILL_MAP[frameworkDep]) {
5062
skills.push(FRAMEWORK_SKILL_MAP[frameworkDep]);
@@ -53,8 +65,30 @@ function resolveSkills(frameworkDep: string | undefined): string[] {
5365
}
5466

5567
/**
56-
* Build the runner-agnostic argv for `skills add ...`. The caller prepends
57-
* the runner (bunx / npx / pnpm dlx / yarn dlx) via {@link runnerCommand}.
68+
* Resolve the git ref to use when installing the clerk-cli skill from this
69+
* repo. Every release (stable, canary, snapshot) tags as `v${CLI_VERSION}` —
70+
* see `.github/workflows/release.yml` and `scripts/snapshot.ts`. The only
71+
* case without a matching tag is the local-dev sentinel `0.0.0-dev` (set
72+
* by `scripts/build.ts` when no `--version` is passed), which falls back
73+
* to `main`.
74+
*/
75+
function clerkCliSkillRef(): string {
76+
const version = typeof CLI_VERSION !== "undefined" ? CLI_VERSION : "0.0.0-dev";
77+
if (version === "0.0.0-dev") return "main";
78+
return `v${version}`;
79+
}
80+
81+
function clerkCliSkillUrl(): string {
82+
return `${CLERK_CLI_REPO}/tree/${clerkCliSkillRef()}/skills/clerk-cli`;
83+
}
84+
85+
/**
86+
* Build the runner-agnostic argv for `skills add <source> ...`. The caller
87+
* prepends the runner (bunx / npx / pnpm dlx / yarn dlx) via
88+
* {@link runnerCommand}.
89+
*
90+
* `skillNames` becomes `--skill <name>` pairs; leave empty to install every
91+
* skill from `source` (what we do for the clerk-cli source).
5892
*
5993
* Interactive mode: hand off to the skills CLI's native UX (auto-detect
6094
* installed agents, scope picker) by omitting `--agent` and `-y`.
@@ -63,10 +97,58 @@ function resolveSkills(frameworkDep: string | undefined): string[] {
6397
*
6498
* Exported for tests.
6599
*/
66-
export function buildSkillsArgs(skills: string[], interactive: boolean): string[] {
67-
const skillFlags = skills.flatMap((s) => ["--skill", s]);
100+
export function buildSkillsArgs(
101+
source: string,
102+
skillNames: readonly string[],
103+
interactive: boolean,
104+
): string[] {
105+
const skillFlags = skillNames.flatMap((s) => ["--skill", s]);
68106
const extraFlags = interactive ? [] : ["-y", "-g"];
69-
return ["skills", "add", SKILLS_SOURCE, ...skillFlags, ...extraFlags];
107+
return ["skills", "add", source, ...skillFlags, ...extraFlags];
108+
}
109+
110+
/**
111+
* Run a single `skills add ...` invocation. Returns true on success, false
112+
* on any failure (spawn error, non-zero exit). Failures print a yellow
113+
* warning but never throw — skills are optional and shouldn't tear down
114+
* a successful scaffold.
115+
*/
116+
async function runSkillsAdd(
117+
runner: Runner,
118+
cwd: string,
119+
source: string,
120+
skillNames: readonly string[],
121+
interactive: boolean,
122+
label: string,
123+
): Promise<boolean> {
124+
const command = runnerCommand(runner, buildSkillsArgs(source, skillNames, interactive));
125+
const displayCommand = `${runner.display} skills add ${source}`;
126+
127+
log.blank();
128+
log.info(`Installing \`${label}\` with \`${runner.display}\`...`);
129+
130+
let exitCode: number;
131+
try {
132+
const proc = Bun.spawn(command, {
133+
cwd,
134+
stdin: "inherit",
135+
stdout: "inherit",
136+
stderr: "inherit",
137+
});
138+
exitCode = await proc.exited;
139+
} catch {
140+
log.blank();
141+
log.warn(`Could not run \`${displayCommand}\`. You can install manually later.`);
142+
return false;
143+
}
144+
145+
if (exitCode !== 0) {
146+
log.blank();
147+
log.warn(`\`${label}\` installation failed. You can install manually: \`${displayCommand}\``);
148+
return false;
149+
}
150+
151+
return true;
70152
}
71153

72154
export async function installSkills(
@@ -75,8 +157,9 @@ export async function installSkills(
75157
packageManager: ProjectContext["packageManager"] | undefined,
76158
skipPrompt: boolean,
77159
): Promise<void> {
78-
const skills = resolveSkills(frameworkDep);
79-
const skillList = skills.join(", ");
160+
const upstreamSkills = resolveUpstreamSkills(frameworkDep);
161+
const skillRef = clerkCliSkillRef();
162+
const skillList = ["clerk-cli", ...upstreamSkills].join(", ");
80163

81164
if (isHuman() && !skipPrompt) {
82165
const install = await confirm({
@@ -93,7 +176,7 @@ export async function installSkills(
93176
log.blank();
94177
log.warn(
95178
"No package runner found on PATH (looked for bunx, npx, pnpm, yarn). " +
96-
`Install one and run \`${suggested.display} skills add ${SKILLS_SOURCE}\` manually.`,
179+
`Install one and run \`${suggested.display} skills add ${UPSTREAM_SKILLS_SOURCE}\` manually.`,
97180
);
98181
return;
99182
}
@@ -114,33 +197,30 @@ export async function installSkills(
114197
}
115198

116199
const interactive = isHuman() && !skipPrompt;
117-
const command = runnerCommand(runner, buildSkillsArgs(skills, interactive));
118-
const displayCommand = `${runner.display} skills add ${SKILLS_SOURCE}`;
119-
120-
log.blank();
121-
log.info(`Installing skills with \`${runner.display}\`: \`${skillList}\``);
122-
123-
let exitCode: number;
124-
try {
125-
const proc = Bun.spawn(command, {
126-
cwd,
127-
stdin: "inherit",
128-
stdout: "inherit",
129-
stderr: "inherit",
130-
});
131-
exitCode = await proc.exited;
132-
} catch {
133-
log.blank();
134-
log.warn(`Could not run \`${displayCommand}\`. You can install manually later.`);
135-
return;
136-
}
137200

138-
if (exitCode !== 0) {
201+
// Install the version-pinned clerk-cli skill from this repo, then the
202+
// upstream framework patterns. Each call soft-fails independently so a
203+
// problem with one source doesn't block the other.
204+
const cliSkillOk = await runSkillsAdd(
205+
runner,
206+
cwd,
207+
clerkCliSkillUrl(),
208+
[],
209+
interactive,
210+
`clerk-cli skill (${skillRef})`,
211+
);
212+
213+
const upstreamOk = await runSkillsAdd(
214+
runner,
215+
cwd,
216+
UPSTREAM_SKILLS_SOURCE,
217+
upstreamSkills,
218+
interactive,
219+
upstreamSkills.join(", "),
220+
);
221+
222+
if (cliSkillOk && upstreamOk) {
139223
log.blank();
140-
log.warn(`Skills installation failed. You can install manually: \`${displayCommand}\``);
141-
return;
224+
log.success("Agent skills installed. AI agents now have Clerk context in this project.");
142225
}
143-
144-
log.blank();
145-
log.success("Agent skills installed. AI agents now have Clerk context in this project.");
146226
}

0 commit comments

Comments
 (0)