Skip to content

Commit dae4ecc

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 a4185bb commit dae4ecc

File tree

7 files changed

+719
-47
lines changed

7 files changed

+719
-47
lines changed

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: 120 additions & 38 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
@@ -24,10 +32,10 @@ import {
2432
import { isNonEmpty } from "../../lib/helpers/arrays.js";
2533
import type { ProjectContext } from "./frameworks/types.js";
2634

27-
/** Skills installed regardless of framework. */
35+
/** Upstream skills installed regardless of framework. */
2836
const BASE_SKILLS = ["clerk", "clerk-setup"];
2937

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

44-
const SKILLS_SOURCE = "clerk/skills";
52+
/** Source for upstream framework-pattern skills. */
53+
const UPSTREAM_SKILLS_SOURCE = "clerk/skills";
4554

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

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

71152
export async function installSkills(
@@ -74,8 +155,9 @@ export async function installSkills(
74155
packageManager: ProjectContext["packageManager"] | undefined,
75156
skipPrompt: boolean,
76157
): Promise<void> {
77-
const skills = resolveSkills(frameworkDep);
78-
const skillList = skills.join(", ");
158+
const upstreamSkills = resolveUpstreamSkills(frameworkDep);
159+
const skillRef = clerkCliSkillRef();
160+
const skillList = ["clerk-cli", ...upstreamSkills].join(", ");
79161

80162
if (isHuman() && !skipPrompt) {
81163
const install = await confirm({
@@ -92,7 +174,7 @@ export async function installSkills(
92174
console.log(
93175
yellow(
94176
"\nNo package runner found on PATH (looked for bunx, npx, pnpm, yarn). " +
95-
`Install one and run \`${suggested.display} skills add ${SKILLS_SOURCE}\` manually.`,
177+
`Install one and run \`${suggested.display} skills add ${UPSTREAM_SKILLS_SOURCE}\` manually.`,
96178
),
97179
);
98180
return;
@@ -114,31 +196,31 @@ export async function installSkills(
114196
}
115197

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

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

0 commit comments

Comments
 (0)