poc: add schema command to fetch Clerk API specs#37
Conversation
Adds a new `clerk openapi` command that fetches OpenAPI specifications from the clerk/openapi-specs repository. Supports four public API names with options for version selection, format (YAML/JSON), and file output. Changes: - New command with public names: backend, frontend, platform, webhooks - Aliases for internal names: bapi→backend, fapi→frontend - 24-hour local caching to reduce network requests - Comprehensive test suite with 15 test cases covering aliases, versions, formats, error handling, and file output - Updated CLAUDE.md to require tests for new functionality Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Rename the CLI command from `clerk openapi` to `clerk schema`. Use public-facing names (backend, frontend, platform, webhooks) as primary identifiers while keeping internal aliases (bapi, fapi) working. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support drilling into specific endpoints (e.g. `clerk schema backend /users`) or schema types (e.g. `clerk schema backend User`) instead of dumping the full spec. Add --resolve-refs flag to inline $ref references for self-contained output with circular reference detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
schema command to fetch Clerk API specs
| .argument("[api]", "API name: backend, frontend, platform, or webhooks") | ||
| .argument("[path]", "Endpoint path (e.g. /users) or schema type (e.g. User)") | ||
| .option("--spec-version <version>", "Spec version (default: latest)") | ||
| .option("--format <format>", "Output format: yaml (default) or json") |
There was a problem hiding this comment.
Nice to have: you could use .choices(["yaml", "json"]) here instead of the manual validation in schema(). Commander would reject invalid values automatically and display the allowed values in --help output. Same idea for the [api] argument -- .choices(["backend", "frontend", "platform", "webhooks", "bapi", "fapi"]) would give you free validation and help text.
.argument("[api]", "API name").choices(["backend", "frontend", "platform", "webhooks", "bapi", "fapi"])
// ...
.option("--format <format>", "Output format").choices(["yaml", "json"])| if (outputPath) { | ||
| await Bun.write(outputPath, content + "\n"); | ||
| console.error(`Spec written to ${outputPath}`); | ||
| } else { |
There was a problem hiding this comment.
Consider adding a --no-cache or --refresh flag. Right now there's no escape hatch if the cached file gets corrupted or the user needs a fresh copy within the 24h TTL window. Something like:
// in SchemaOptions
noCache?: boolean;
// in fetchSpec
if (!options.noCache) {
const cached = await readCache(api, version);
if (cached) return cached;
}| latest: string; | ||
| versions: string[]; | ||
| } | ||
|
|
There was a problem hiding this comment.
This version list will go stale whenever a new API version is released. You might want to add a comment noting that, or consider fetching the directory listing from the GitHub API at some point (e.g. GET repos/clerk/openapi-specs/contents/bapi) so the CLI can discover versions dynamically. For now, a TODO and maybe a simple test that fetches the repo to flag drift would be a low-cost safety net.
// TODO: consider discovering versions dynamically from the clerk/openapi-specs repo|
|
||
| // ── Ref resolution ─────────────────────────────────────────────────────────── | ||
|
|
||
| export function resolveAllRefs(node: unknown, root: unknown, seen?: Set<string>): unknown { |
There was a problem hiding this comment.
The visited.delete(refPath) pattern here is correct -- it tracks the current ancestor chain so sibling refs to the same schema both get resolved. Just wanted to confirm this is intentional since it's the kind of thing that looks like it might be a bug at first glance. Maybe a short comment would help future readers:
// Remove from visited so sibling references to the same schema
// are resolved (only circular *ancestor* chains are blocked).
visited.delete(refPath);
rafa-thayto
left a comment
There was a problem hiding this comment.
Hey! Nice feature, this would be really useful for agents and developers introspecting the API. A few things to address:
1. File paths are under src/ instead of packages/cli-core/src/
This branch looks like it predates the monorepo restructuring. Current main has all CLI source under packages/cli-core/src/. A rebase + move would be needed before this can merge.
2. Uses console.log/console.error directly
The project has a strict no-console oxlint rule and uses log.* methods everywhere (see .claude/rules/logging.md). This will fail the lint check.
import { log } from "../../lib/log.ts";
// For pipeable data output:
log.data(content);
// For status messages:
log.info(`Spec written to ${outputPath}`);
3. Tests delete the real CLERK_CACHE_DIR
beforeEach does await rm(CLERK_CACHE_DIR, { recursive: true, force: true }) which resolves to the developer's actual cache directory. Running tests locally would wipe your real CLI cache.
// Use a temp directory instead:
const testCacheDir = await mkdtemp(join(tmpdir(), "clerk-schema-test-"));
process.env.CLERK_CONFIG_DIR = testCacheDir;
4. Cache TTL mismatch
The README says "cached locally for 24 hours" but the code uses CACHE_TTL_MS which is 1 hour in constants.ts. Either update the docs or define a separate SCHEMA_CACHE_TTL_MS = 24 * 60 * 60 * 1000.
5. Tests should use captureLog() instead of spyOn(console)
The project has a captureLog() test utility that integrates with the log.* system. Once you switch from console.log to log.*, the tests should use captureLog() too.
6. Missing changeset
The Enforce Changeset workflow will block merge. This needs a minor changeset since it adds a new user-facing command.
7. resolveAllRefs cycle detection could be stricter
The visited set is shared across sibling properties with add/delete. For diamond-shaped ref patterns, cloning the set when entering a new ref would be safer:
visited.add(refPath);
const resolved = resolveAllRefs(target, root, new Set(visited));
visited.delete(refPath);
The core implementation is really well done though. The path introspection, type lookup, "did you mean?" suggestions, and circular ref handling are all solid. Just needs the structural updates to land on current main.
Summary
clerk schemacommand that fetches OpenAPI specs from the clerk/openapi-specs repo--resolve-refsto inline all$refreferences for self-contained outputUsage
Changes
clerk schemacommand with public API names (backend, frontend, platform, webhooks) and internal aliases (bapi, fapi)/users) or types (User) with prefix-aware matching and "did you mean?" suggestions--resolve-refsflag: recursively inlines$refreferences with circular reference detectionTest plan
bun test— all 398 tests passbun run lint/bun run format— cleanclerk schema backend /users,clerk schema backend User, and--resolve-refsagainst live specs🤖 Generated with Claude Code