Skip to content

Commit 2b430c5

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 1e94859 commit 2b430c5

7 files changed

Lines changed: 719 additions & 47 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
@@ -153,12 +153,28 @@ Nuxt's module system auto-configures middleware and auto-imports components.
153153

154154
## Agent skills install
155155

156-
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 `npx skills add`. This step is optional and non-fatal: if `npx` is missing or the install command exits non-zero, init prints a yellow warning with the manual install command and still exits successfully.
156+
After scaffolding (and after env keys are pulled or keyless instructions are printed), `clerk init` offers to install Clerk's agent skills via [`npx skills add`](https://www.npmjs.com/package/skills). This step is optional and non-fatal: if no package runner is available or an install command exits non-zero, init prints a yellow warning and still exits successfully.
157157

158-
- **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.
159-
- **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 `npx skills add clerk/skills` manually, or have their agent do it.
158+
- **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.
159+
- **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 `npx skills add clerk/skills` (and the version-pinned `clerk-cli` skill URL) manually, or have their agent do it.
160160

161-
The base skills `clerk` and `clerk-setup` are always included. The detected framework dependency adds a matching skill:
161+
Two install commands run, sharing one runner:
162+
163+
### 1. The version-pinned `clerk-cli` skill
164+
165+
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.
166+
167+
The source URL is constructed from `CLI_VERSION` (the compile-time global injected by `scripts/build.ts`):
168+
169+
```
170+
https://github.com/clerk/cli/tree/v<CLI_VERSION>/skills/clerk-cli
171+
```
172+
173+
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`.
174+
175+
### 2. The upstream framework-pattern skills
176+
177+
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:
162178

163179
| Framework dep | Added skill |
164180
| ----------------------- | ----------------------------- |
@@ -173,7 +189,13 @@ The base skills `clerk` and `clerk-setup` are always included. The detected fram
173189
| `express` | `clerk-backend-api` |
174190
| `fastify` | `clerk-backend-api` |
175191

176-
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.
192+
These skills version independently of the CLI, so no pin is applied.
193+
194+
### Failure handling
195+
196+
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.
197+
198+
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.
177199

178200
## API Endpoints
179201

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
@@ -23,10 +31,10 @@ import {
2331
import { isNonEmpty } from "../../lib/helpers/arrays.js";
2432
import type { ProjectContext } from "./frameworks/types.js";
2533

26-
/** Skills installed regardless of framework. */
34+
/** Upstream skills installed regardless of framework. */
2735
const BASE_SKILLS = ["clerk", "clerk-setup"];
2836

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

43-
const SKILLS_SOURCE = "clerk/skills";
51+
/** Source for upstream framework-pattern skills. */
52+
const UPSTREAM_SKILLS_SOURCE = "clerk/skills";
4453

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

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

70151
export async function installSkills(
@@ -73,8 +154,9 @@ export async function installSkills(
73154
packageManager: ProjectContext["packageManager"] | undefined,
74155
skipPrompt: boolean,
75156
): Promise<void> {
76-
const skills = resolveSkills(frameworkDep);
77-
const skillList = skills.join(", ");
157+
const upstreamSkills = resolveUpstreamSkills(frameworkDep);
158+
const skillRef = clerkCliSkillRef();
159+
const skillList = ["clerk-cli", ...upstreamSkills].join(", ");
78160

79161
if (isHuman() && !skipPrompt) {
80162
const install = await confirm({
@@ -90,7 +172,7 @@ export async function installSkills(
90172
console.log(
91173
yellow(
92174
"\nNo package runner found on PATH (looked for bunx, npx, pnpm, yarn). " +
93-
`Install one and run \`npx skills add ${SKILLS_SOURCE}\` manually.`,
175+
`Install one and run \`npx skills add ${UPSTREAM_SKILLS_SOURCE}\` manually.`,
94176
),
95177
);
96178
return;
@@ -112,31 +194,31 @@ export async function installSkills(
112194
}
113195

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

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

0 commit comments

Comments
 (0)