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
2 changes: 1 addition & 1 deletion .claude/rules/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ paths:
alwaysApply: false
---

All output goes through the `log` object from `src/lib/log.ts`. **Never use `console.log`, `console.error`, `console.warn`, `console.info`, or `process.stderr.write` directly** — use `log.*` methods so output respects log levels, throttling, and test capture.
All output goes through the `log` object from `src/lib/log.ts`. **Never use `console.log`, `console.error`, `console.warn`, `console.info`, or `process.stderr.write` directly** — use `log.*` methods so output respects log levels, throttling, and test capture. The `no-console` oxlint rule enforces this in production source; test files (`*.test.ts`, `src/test/**`) and `scripts/**` are exempt.

```ts
import { log } from "<relative-path>/lib/log.ts";
Expand Down
13 changes: 12 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"rules": {
"unicorn/no-process-exit": "error"
"unicorn/no-process-exit": "error",
"no-console": "error"
},
"overrides": [
{
Expand All @@ -12,6 +13,16 @@
"rules": {
"unicorn/no-process-exit": "off"
}
},
{
"files": [
"scripts/**",
"packages/cli-core/src/**/*.test.ts",
"packages/cli-core/src/test/**"
],
"rules": {
"no-console": "off"
}
}
]
}
96 changes: 1 addition & 95 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,6 @@ Default to using Bun instead of Node.js.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.

## Testing

Use `bun test` to run tests.

```ts#index.test.ts
import { test, expect } from "bun:test";

test("hello world", () => {
expect(1).toBe(1);
});
```

## CI Checks

After modifying files, run these commands to match what CI enforces on pull requests:
Expand All @@ -56,7 +44,7 @@ bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun run test # Run unit tests
bun run test:e2e:op # Run E2E tests with secrets resolved from 1Password (preferred locally)
bun run test:e2e # Run E2E tests with env vars already set (used by CI; see .claude/rules/e2e.md)
bun run test:e2e # Run E2E tests with env vars already set (used by CI)
```

Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-memory and never written to disk. `bun run test:e2e` is for CI or for cases where the required env vars are already exported.
Expand All @@ -66,85 +54,3 @@ CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test
## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.

## Commands

Every CLI command lives in its own directory under `packages/cli-core/src/commands/<name>/`. Each directory must contain a `README.md` that documents:

- What the command does
- Usage and options
- Clerk API endpoints the command calls (method, path, description)
- Whether the command (or parts of it) is mocked/stubbed — call this out prominently with a blockquote at the top of the README if so

When adding a new command, create its directory and README. When modifying a command's behavior, options, or API calls, update its README to match.

When creating or modifying a command, evaluate whether it needs an agent mode. Commands with interactive prompts (menus, wizards, multi-step flows) should check `isAgent()` from `packages/cli-core/src/mode.ts` and, when in agent mode, output a structured prompt that an AI agent can follow instead of running the interactive flow. Commands that are already non-interactive (e.g., single API calls, browser-based OAuth) typically don't need agent mode.

### Root README

`README.md` at the project root contains the CLI help output. When commands are added, removed, or their options change, update the help output in `README.md` to stay in sync. You can regenerate it by running `bun run dev -- --help`.

## Logging

All output goes through the `log` object from `packages/cli-core/src/lib/log.ts`. **Never use `console.log`, `console.error`, `console.warn`, `console.info`, or `process.stderr.write` directly** — use `log.*` methods so output respects log levels, throttling, and test capture.

See [.claude/rules/logging.md](.claude/rules/logging.md) for the full method reference, debug logging, tagged loggers, inline highlighting, and testing patterns.

## Error Handling

All error classes and helpers live in `packages/cli-core/src/lib/errors.ts`. The global error handler in `packages/cli-core/src/cli.ts` catches thrown errors and formats them for the user. **Never call `console.error` + `process.exit` directly in commands** — throw an error instead and let the global handler deal with output and exit codes.

### Known failures — `CliError`

For user-facing errors (missing config, invalid input, resource not found), throw a `CliError`:

```ts
import { CliError } from "../../lib/errors.ts";

throw new CliError("No Clerk project linked. Run `clerk link` first.");

// With a docs URL (automatically gets .md appended in agent mode for Clerk URLs):
throw new CliError("Not authenticated.", {
docsUrl: "https://clerk.com/docs/guides/development/clerk-environment-variables",
});
```

### Usage/validation errors — `throwUsageError`

For invalid arguments or options, use `throwUsageError` (exits with code 2):

```ts
import { throwUsageError } from "../../lib/errors.ts";

if (!secretKey) {
throwUsageError("No secret key found. Set CLERK_SECRET_KEY or use --secret-key.");
}
```

### User cancellation — `throwUserAbort`

When the user cancels a prompt or confirmation, call `throwUserAbort()`. The global handler exits cleanly with no error output:

```ts
import { throwUserAbort } from "../../lib/errors.ts";

const confirmed = await confirm({ message: "Proceed?" });
if (!confirmed) throwUserAbort();
```

### API errors — `withApiContext`

Wrap API calls with `withApiContext` to attach a human-readable context string. The global handler extracts the first error message from the response body and prints it with the context prefix:

```ts
import { withApiContext } from "../../lib/errors.ts";

const config = await withApiContext(
fetchInstanceConfig(appId, instanceId),
"Failed to fetch config",
);
```

### API error classes

`BapiError` and `PlapiError` (both extend `ApiError`) are thrown by the API helpers in `packages/cli-core/src/commands/api/bapi.ts` and `packages/cli-core/src/lib/plapi.ts` respectively. Don't construct these in commands — they're thrown automatically by the fetch wrappers. Use `withApiContext` to add context when calling those helpers.
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ When running in agent mode (`--mode agent` or non-TTY), outputs a framework-spec
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 `npx skills add` (see [Agent skills install](#agent-skills-install))
17. Optionally installs framework-specific Clerk agent skills via the project's package runner (see [Agent skills install](#agent-skills-install))

## Framework Detection

Expand Down Expand Up @@ -165,10 +165,10 @@ If no entry file is found, a post-instruction is printed pointing to the Clerk J

## Agent skills install

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.
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.

- **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.
- **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.
- **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 / `--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.

The base skills `clerk` and `clerk-setup` are always included. The detected framework dependency adds a matching skill:

Expand Down
19 changes: 11 additions & 8 deletions packages/cli-core/src/commands/init/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { join } from "node:path";
import { search, confirm, input } from "@inquirer/prompts";
import { cyan, yellow } from "../../lib/color.js";
import { throwUserAbort, CliError } from "../../lib/errors.js";
import { log } from "../../lib/log.js";
import type { FrameworkInfo } from "../../lib/framework.js";
import { hasPackageJson } from "./context.js";
import {
Expand Down Expand Up @@ -84,7 +84,9 @@ async function askProjectName(entry: BootstrapEntry): Promise<string> {
}

async function generateProject(label: string, command: string[], cwd: string): Promise<void> {
console.log(`\nCreating ${cyan(label)} project...\n`);
log.blank();
log.info(`Creating \`${label}\` project...`);
log.blank();

const exitCode = await spawnInherited(command, cwd);
if (exitCode !== 0) {
Expand All @@ -93,14 +95,15 @@ async function generateProject(label: string, command: string[], cwd: string): P
}

async function installDependencies(pm: PackageManager, cwd: string): Promise<void> {
console.log(`\nInstalling dependencies...\n`);
log.blank();
log.info("Installing dependencies...");
log.blank();

const exitCode = await spawnInherited(PM_INSTALL_COMMANDS[pm], cwd);
if (exitCode !== 0) {
console.log(
yellow(
`\nDependency installation failed. Run manually: ${PM_INSTALL_COMMANDS[pm].join(" ")}`,
),
log.blank();
log.warn(
`Dependency installation failed. Run manually: \`${PM_INSTALL_COMMANDS[pm].join(" ")}\``,
);
}
}
Expand Down Expand Up @@ -163,6 +166,6 @@ export async function promptAndBootstrap(

await installDependencies(pm, projectDir);

console.log();
log.blank();
return { projectDir, projectName, packageManager: pm };
}
7 changes: 6 additions & 1 deletion packages/cli-core/src/commands/init/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,12 @@ describe("init", () => {

await init({ yes: true });

expect(skillsMod.installSkills).toHaveBeenCalledWith(FAKE_BOOTSTRAP.projectDir, "react", true);
expect(skillsMod.installSkills).toHaveBeenCalledWith(
FAKE_BOOTSTRAP.projectDir,
"react",
"npm",
true,
);
});

test("--starter in agent mode prints guidance without bootstrap", async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-core/src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function init(options: InitOptions = {}) {

if (options.skills !== false) {
bar();
await installSkills(ctx.cwd, ctx?.framework.dep, options.yes ?? false);
await installSkills(ctx.cwd, ctx?.framework.dep, ctx?.packageManager, options.yes ?? false);
}

outro("Done");
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/init/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ describe("buildSkillsArgs", () => {
test("interactive mode: no -y or -g, lets skills CLI take over", () => {
const args = buildSkillsArgs(skills, true);
expect(args).toEqual([
"npx",
"skills",
"add",
"clerk/skills",
Expand Down
Loading