Skip to content

feat(users): add users command scaffolding and clerk users create#240

Open
wyattjoh wants to merge 1 commit intomainfrom
revert-239-revert-236-wyattjoh/users-scaffolding-and-create
Open

feat(users): add users command scaffolding and clerk users create#240
wyattjoh wants to merge 1 commit intomainfrom
revert-239-revert-236-wyattjoh/users-scaffolding-and-create

Conversation

@wyattjoh
Copy link
Copy Markdown
Contributor

Summary

Adds the clerk users command family with the first subcommand, clerk users create. The top-level users command exposes shared targeting (--app, --secret-key, --instance, --dry-run, --yes, --json) that upcoming subcommands will reuse.

clerk users create accepts curated flags (--email, --phone, --username, --password, --first-name, --last-name, --external-id) for the common path, and -d, --data <json> / --file <path> for raw BAPI bodies when a field the curated flags don't expose (primary_email_address_id, skip_password_checks, web3_wallets) is needed. Program-level --input-json composes cleanly so agents can drive curated flags from a JSON object. BAPI enforces identifier and required-field rules server-side, so the command only needs a BAPI secret key, not the applications:manage Platform API scope.

In human mode, clerk users invoked with no subcommand opens an interactive menu that dispatches to registered actions, and clerk users create invoked without curated flags or -d/--file enters a guided wizard. The wizard reads the instance's Frontend API user_settings and prompts only for fields the instance accepts, marking required ones. When no project is linked the wizard falls back to a shared application picker (also used by clerk link), so users don't have to run clerk link first. Agent mode disables every interactive flow and exits with a structured usage error instead.

The application picker now lists "Create a new application" at the bottom and de-emphasizes it until highlighted, so it reads as a fallback rather than a primary choice. lib/listage.ts gained a per-choice style hook to support that without baking dim styling into the prompt theme.

Alongside create, this PR lands shared scaffolding upcoming subcommands will build on: lib/bapi-command.ts (secret-key resolution and error formatting), lib/users.ts (payload builders, JSON input parsing), commands/users/output.ts (mutation print helpers), and commands/users/lifecycle-runner.ts (shared runner for direct state transitions).

Test plan

  • bun run format:check
  • bun run lint
  • bun run typecheck
  • bun run test
  • bun run test:e2e:op (every framework fixture's test-user creation now runs through clerk users create -d)
  • Reviewer: spot-check clerk users create --help and the curated-flags + -d interaction
  • Reviewer: drive the interactive clerk users create wizard with no project linked, confirm picker → wizard → BAPI handoff

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: 231c3b7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
clerk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@wyattjoh
Copy link
Copy Markdown
Contributor Author

!snapshot

@wyattjoh wyattjoh marked this pull request as ready for review April 28, 2026 15:19
@github-actions
Copy link
Copy Markdown
Contributor

Snapshot published

npm install -g clerk@1.1.1-snapshot.231c3b7
Package Version
clerk 1.1.1-snapshot.231c3b7

Published from 231c3b7

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6020fc58-cd77-4b31-bad6-0734aa6fed12

📥 Commits

Reviewing files that changed from the base of the PR and between e4778c0 and 231c3b7.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (45)
  • .changeset/users-scaffolding-and-create.md
  • .gitignore
  • README.md
  • packages/cli-core/package.json
  • packages/cli-core/src/cli-program.test.ts
  • packages/cli-core/src/cli-program.ts
  • packages/cli-core/src/commands/api/bapi.ts
  • packages/cli-core/src/commands/api/index.test.ts
  • packages/cli-core/src/commands/api/index.ts
  • packages/cli-core/src/commands/link/index.test.ts
  • packages/cli-core/src/commands/link/index.ts
  • packages/cli-core/src/commands/users/README.md
  • packages/cli-core/src/commands/users/create-wizard.test.ts
  • packages/cli-core/src/commands/users/create-wizard.ts
  • packages/cli-core/src/commands/users/create.test.ts
  • packages/cli-core/src/commands/users/create.ts
  • packages/cli-core/src/commands/users/index.test.ts
  • packages/cli-core/src/commands/users/index.ts
  • packages/cli-core/src/commands/users/interactive/attributes.test.ts
  • packages/cli-core/src/commands/users/interactive/attributes.ts
  • packages/cli-core/src/commands/users/interactive/instance-context.test.ts
  • packages/cli-core/src/commands/users/interactive/instance-context.ts
  • packages/cli-core/src/commands/users/interactive/pick-user.test.ts
  • packages/cli-core/src/commands/users/interactive/pick-user.ts
  • packages/cli-core/src/commands/users/lifecycle-runner.ts
  • packages/cli-core/src/commands/users/menu.test.ts
  • packages/cli-core/src/commands/users/menu.ts
  • packages/cli-core/src/commands/users/output.ts
  • packages/cli-core/src/commands/users/registry.ts
  • packages/cli-core/src/commands/users/shared.ts
  • packages/cli-core/src/lib/app-picker.ts
  • packages/cli-core/src/lib/bapi-command.test.ts
  • packages/cli-core/src/lib/bapi-command.ts
  • packages/cli-core/src/lib/config.test.ts
  • packages/cli-core/src/lib/config.ts
  • packages/cli-core/src/lib/errors.ts
  • packages/cli-core/src/lib/fapi.test.ts
  • packages/cli-core/src/lib/fapi.ts
  • packages/cli-core/src/lib/listage.test.ts
  • packages/cli-core/src/lib/listage.ts
  • packages/cli-core/src/lib/users.test.ts
  • packages/cli-core/src/lib/users.ts
  • packages/cli-core/src/test/integration/users-commands.test.ts
  • skills/clerk/references/recipes.md
  • test/e2e/lib/test-user.ts

📝 Walkthrough

Walkthrough

This pull request introduces a new clerk users command family with interactive scaffolding and user creation functionality. The changes add a top-level clerk users command with a create subcommand that supports both interactive and flag-driven flows. The create wizard fetches instance user settings via the Frontend API to dynamically prompt only enabled fields and enforce required ones. New shared infrastructure includes FAPI client integration for user settings retrieval, BAPI command utilities for secret key resolution and error handling, and an enhanced application picker that repositions the "create new application" option as a fallback. The implementation includes comprehensive tests covering command registration, wizard flows, BAPI/FAPI integration, and integration scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding the users command family with the create subcommand as the initial implementation.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the feature, its implementation details, interaction modes, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

}) {
const payload: Record<string, unknown> = {};

if (options.email) payload.email_address = [options.email];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These truthiness checks (if (options.email)) will silently drop empty strings, which matters for buildUpdateUserPayload where "" could mean "clear this field". Using != null preserves the "omit if not provided" semantics while allowing explicit empty strings:

Suggested change
if (options.email) payload.email_address = [options.email];
if (options.email != null) payload.email_address = [options.email];
if (options.phone != null) payload.phone_number = [options.phone];
if (options.username != null) payload.username = options.username;
if (options.password != null) payload.password = options.password;
if (options.firstName != null) payload.first_name = options.firstName;
if (options.lastName != null) payload.last_name = options.lastName;
if (options.externalId != null) payload.external_id = options.externalId;

}

function getLifecycleSuccessMessage(path: string): string {
const userId = getUserIdFromPath(path);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two parallel if-chains (getLifecycleSuccessMessage and getLifecycleErrorMessage) duplicate the same path.endsWith logic across ~60 lines. A lookup map would cut this down significantly:

Suggested change
const userId = getUserIdFromPath(path);
const LIFECYCLE_LABELS: Record<string, { success: string; error: string }> = {
"/ban": { success: "Banned user", error: "Failed to ban user" },
"/unban": { success: "Unbanned user", error: "Failed to unban user" },
"/lock": { success: "Locked user", error: "Failed to lock user" },
"/unlock": { success: "Unlocked user", error: "Failed to unlock user" },
"/profile_image": { success: "Removed profile image for user", error: "Failed to remove profile image for user" },
"/mfa": { success: "Disabled MFA for user", error: "Failed to disable MFA for user" },
"/totp": { success: "Removed TOTP for user", error: "Failed to remove TOTP for user" },
"/backup_code": { success: "Removed backup codes for user", error: "Failed to remove backup codes for user" },
};
function getLifecycleLabel(path: string, type: "success" | "error"): string {
const userId = getUserIdFromPath(path);
const suffix = "/" + (path.split("/").pop() ?? "");
const labels = LIFECYCLE_LABELS[suffix];
const fallback = type === "success" ? "Updated user" : "Failed to update user";
return `${labels?.[type] ?? fallback} ${userId}`;
}

export async function runCreateWizard(options: WizardOptions): Promise<CreateWizardResult> {
const ctx = await resolveUsersInstanceContext(options);
const settings =
ctx.fapiHost && ctx.publishableKey
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If FAPI is down or the request fails, loadSettings throws FapiError and the entire wizard crashes. This contrasts with fetchAppsTolerantly in app-picker.ts which gracefully degrades on 5xx. Wrapping this in a try-catch would let the wizard fall back to showing all fields as optional (which it already does when fapiHost is undefined):

Suggested change
ctx.fapiHost && ctx.publishableKey
let settings: UserSettingsJSON | undefined;
if (ctx.fapiHost && ctx.publishableKey) {
try {
settings = await loadSettings(ctx.fapiHost, decodePublishableKey(ctx.publishableKey).instanceType);
} catch {
// Fall back to prompting the full curated set when FAPI is unreachable.
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants