Skip to content

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

Merged
wyattjoh merged 29 commits intomainfrom
wyattjoh/users-scaffolding-and-create
Apr 27, 2026
Merged

feat(users): add users command scaffolding and clerk users create#236
wyattjoh merged 29 commits intomainfrom
wyattjoh/users-scaffolding-and-create

Conversation

@wyattjoh
Copy link
Copy Markdown
Contributor

@wyattjoh wyattjoh commented Apr 24, 2026

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

Introduce the top-level `clerk users` command with shared targeting and
mutation output helpers, plus `clerk users create` as the first
subcommand. Curated flags cover the common fields and `-d, --data` /
`--file` send raw BAPI request bodies for fields the flags don't expose.

- Wire `users` into the CLI program with `--app`, `--secret-key`,
  `--instance`, `--dry-run`, `--yes`, and `--json` behavior aligned
  across future users subcommands.
- Extract payload builders, input parsing, and JSON payload reading
  into `lib/users.ts` so future subcommands can share them.
- Add `lib/bapi-command.ts` to centralize BAPI secret-key resolution
  and error handling across users mutations.
- Add `lifecycle-runner.ts` as the shared runner for direct state
  transitions, ready for the upcoming lifecycle and specialized
  subcommands to import.
- Migrate the e2e test-user helper to exercise `clerk users create`
  end-to-end across every framework fixture.

BAPI enforces identifier and required-field rules server-side, so the
command only needs a BAPI secret key — no `applications:manage`
Platform API scope is required.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 24, 2026

🦋 Changeset detected

Latest commit: ecf78cf

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

Comment thread packages/cli-core/src/test/integration/users-commands.test.ts Fixed
wyattjoh added 25 commits April 24, 2026 15:35
…, validate hostname

- Add FapiError extends ApiError in errors.ts so HTTP failures carry
  status, body, and url instead of a stringified CliError message.
- Extend cli-program.ts verbose branch to print url for FapiError as
  well as PlapiError.
- Factor fapiFetch/fapiFetchJson private helpers in fapi.ts so both
  public functions share a single HTTP + JSON-parse path with safe
  error handling.
- Validate the decoded FAPI hostname against /^[a-zA-Z0-9.-]+$/ in
  decodePublishableKey to reject keys with embedded slashes, @, etc.
- Lift the magic literal "5" into CLERK_JS_API_VERSION constant.
- Update tests: existing 500/401 cases now assert FapiError; add
  missing-token and missing-user_settings cases; add hostname
  validation cases for "/" and "@" characters.
Add `isEnabled`, `isRequired`, and `enabledAttributes` pure helper functions over `UserSettingsJSON` in the new `interactive/` subdirectory.
Introduces a side-effect-driven action registry (registry.ts) with
registerUsersAction / listUsersActions / __resetUsersActionRegistryForTesting,
re-exported from index.ts. Registers the create action in create.ts.
A separate registry.ts breaks the circular dependency that would arise
from create.ts importing index.ts while index.ts imports create.ts.
The interactive menu (`clerk users` with no subcommand) needs to accept
targeting flags so it can open against a specific app or instance instead
of relying on a linked project. Without these options on the parent,
`clerk users --app foo` errored with 'unknown option --app'.
The 'About to POST /v1/users' confirmation used log.raw() for the JSON
dump, which bypasses the bar-prefix that intro() pushes. The output
visually escaped the wizard frame and showed dense JSON instead of
human-friendly field listings. Now each top-level field renders as a
single 'key: value' line through log.info(), so the sidebar wraps the
preview cleanly. Single-element string arrays (BAPI's email_address
shape) are unwrapped for display.
Create is non-destructive: a mistyped field is recoverable by deleting
and re-creating. The wizard already collected each value via prompts;
re-asking 'Proceed?' immediately after is decision fatigue. The flag
path goes straight to the spinner too, matching the wizard.

--yes is preserved on the CLI surface but is now a no-op for create
(scripts that pass it continue to work). Lifecycle commands (delete,
ban, unban, lock, unlock) keep their confirmation gate where it
genuinely earns its place.
When --app/--instance/--secret-key were declared on both the parent
'users' command and the 'create' subcommand, Commander's option-parser
attributed the value to the parent and the child action received undefined.
That broke 'clerk users create --app foo' for the wizard path.

Now the targeting flags live only on the parent. The create subcommand's
action wrapper uses cmd.optsWithGlobals() to merge parent + child options
before invoking the handler. Both invocation orders work: 'clerk users
--app foo create' and 'clerk users create --app foo'.
Pull the 'pick existing or create new' interactive flow out of
commands/link/index.ts into lib/app-picker.ts so other entry points can
use the same UX. No behavior change for clerk link itself; the picker
message and the auto-detect-from-keys handshake stay where they were.
…mode

When the create wizard runs without --app and no project is linked,
previously we threw NOT_LINKED with a 'Run clerk link or pass --app'
message. Now we open the same picker (existing apps + 'create new')
that clerk link uses, then resolve the chosen app's instance context.

Agent mode keeps the original error: agents must pass --app explicitly.
When no project is linked, the create wizard's
resolveUsersInstanceContext falls back to the app picker. The picked
appId and the secret key fetched from plapi were consumed only inside
the wizard; resolveBapiSecretKey then ran again with no targeting and
threw "No secret key found".

Split the wizard return into fields (user input) and targeting (resolved
app/instance/secretKey). The create command merges targeting onto its
options before the BAPI call, so resolveBapiSecretKey short-circuits on
the already-resolved key.

The "no input provided" check now inspects only fields, since targeting
is always populated when the picker fallback fires.
The "+ Create a new application" entry sat at the top of the list,
above the user's apps. For users with existing apps it makes the
common case (pick an existing app) require an extra arrow-down.
Place create at the bottom so it's the explicit fallback after
scanning the list.
…iption

The create option used dim styling, which made it nearly invisible at
the bottom of the list. Drop the dim, add a Separator above it so the
boundary between existing apps and the create action is unambiguous,
and attach a short description that surfaces under the prompt when the
row is highlighted. When no apps match the search, the list is just
the create option (no leading separator).

Refresh the branch changeset to mention the picker UX change since it
also affects clerk link.
…tive

Add an optional per-choice style hook to the search prompt's
SearchChoice (and forward it through normalizeChoices) so a single row
can override the default highlight behavior. Use it in the application
picker: the "+ Create a new application" row renders dim by default and
cyan when the cursor lands on it, so it reads as a de-emphasized fallback
rather than competing with the user's existing apps.

Drop the description added in the previous commit; the muted-by-default
treatment plus the separator are enough signal without a second line of
helper text.
The dim-by-default styling already differentiates the create row from
the app list. The Separator added an extra blank line that contributed
to the picker feeling cluttered without adding signal beyond what the
muted color already provides.
resolveBasePayload mutated the caller's options object (`Object.assign(
options, targeting, fields)`) so subsequent reads of options.app,
options.secretKey, etc. would reflect the wizard's resolved targeting.
That works today but quietly couples ordering: any future change that
runs resolveBapiSecretKey before resolveCreatePayload would break
without surfacing as a type error.

Refactor so resolveBasePayload returns { basePayload, resolved } and
resolveCreate returns { payload, resolved }. The create() function then
reads from resolved instead of options for downstream targeting,
payload printing, and BAPI error handling. The Commander-owned options
object is no longer mutated.

No behavior change. Existing unit tests and integration tests pass
unchanged.
Add integration tests that drive the full CLI program for paths the
unit suite covers only with mocks:

- raw -d/--data payload merging into BAPI body
- --file payload reading from disk
- --dry-run redacting password fields and skipping the BAPI call
- --secret-key targeting BAPI directly with no platform API call
- --app resolving the instance key without a linked project
- wizard picker fallback when no project is linked. This exercises
  the resolveUsersInstanceContext picker-fallback path and asserts
  the secret key resolved by the wizard reaches the BAPI request.
  This is the regression test for 5eed076, where the wizard
  resolved the key but dropped it before resolveBapiSecretKey ran.
- agent mode without input returns a structured usage error and
  never prompts.

The existing test.each leaves the harness's mocked _mode at "agent",
so each new test pins --mode human (or agent) explicitly.
Extract the search prompt's per-row render logic into an exported
renderSearchItem helper so it can be tested directly. The function is
the load-bearing piece behind the app-picker UX (mute-when-idle /
highlight-when-active); a future refactor that drops the
`if (item.style) return ...` branch would silently regress styling
across clerk link and the clerk users wizard fallback.

Also export normalizeChoices and add a round-trip test confirming the
style hook is preserved from input choice to normalized item.

Tests cover: default highlight, plain inactive rendering, style hook
overrides on both states, the cursor + name shape passed to the hook,
separators, and disabled choices ignoring the hook.
The previous test confirmed only that the wizard was not invoked in
agent mode. A regression that strips the curated-flag examples from
noInputMessage, or that throws an unstructured Error instead of
USAGE_ERROR, would slip through.

Tighten the assertion to confirm:
- error is a CliError with code USAGE_ERROR and exitCode EXIT_CODE.USAGE
- the message contains each curated-flag example
- neither the secret-key resolver nor BAPI are called
Comment thread packages/cli-core/src/test/integration/users-commands.test.ts Dismissed
@wyattjoh
Copy link
Copy Markdown
Contributor Author

!snapshot

@github-actions
Copy link
Copy Markdown
Contributor

Snapshot published

npm install -g clerk@1.1.1-snapshot.e33c494
Package Version
clerk 1.1.1-snapshot.e33c494

Published from e33c494

@wyattjoh wyattjoh marked this pull request as ready for review April 27, 2026 19:26
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 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: d619ef25-00ea-49c6-abb5-d632da8cbfc8

📥 Commits

Reviewing files that changed from the base of the PR and between 48d2d60 and ecf78cf.

📒 Files selected for processing (1)
  • skills/clerk/references/recipes.md

📝 Walkthrough

Walkthrough

Adds a new "clerk users" command family with a top-level interactive menu and a users create subcommand supporting curated flags, inline/file JSON payloads, dry-run, and an interactive creation wizard driven by Frontend API user settings. Implements users action registry, payload builders/redaction, interactive user picker, lifecycle runner, and users-specific output/error handling. Introduces centralized BAPI utilities (path normalization, secret-key resolution, error handling), Frontend API client (publishable-key decoding, dev-browser bootstrap, user-settings fetch), an app-picker utility, many unit/integration tests, README updates, and .gitignore addition.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.43% 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 PR title clearly and concisely describes the main change: adding the clerk users command family with the create subcommand.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, detailing the clerk users command implementation, features, and test plan.
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.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli-core/src/commands/users/create-wizard.ts`:
- Around line 94-97: The wizard trims password input which mutates user-entered
credentials; in the branch handling field.isPassword (inside create-wizard.ts
where password({ message, validate }) is called) stop calling .trim() on the
returned value so the exact string returned by password() is preserved and
forwarded to the API; keep trimming for non-password fields only (remove or
conditionally avoid value.trim() for the password path).

In `@packages/cli-core/src/commands/users/interactive/instance-context.ts`:
- Around line 25-27: If a user supplies an explicit secret key, do not allow the
wizard to also resolve app/instance context: change the branching around
options.secretKey so that if options.secretKey is present and options.app or
options.instance is also provided you throw a clear error (or exit) instead of
continuing, and if only options.secretKey is present call
validateKeyPrefix(options.secretKey, "sk_") and return { secretKey:
options.secretKey } immediately; ensure this logic in instance-context.ts
surrounds the existing validateKeyPrefix usage so the wizard code that resolves
app/instance/publishable-key never runs when a secret key is supplied.

In `@packages/cli-core/src/lib/config.ts`:
- Around line 325-331: resolveAppContext currently uses
resolveFetchedApplicationInstance(options.app, app, options.instance) and
returns its instanceId/instanceLabel even when resolveFetchedApplicationInstance
indicates found: false; update resolveAppContext to check the returned object's
found flag (from resolveFetchedApplicationInstance) and when found is false
throw or return the proper INSTANCE_NOT_FOUND error (or raise the same error
type used elsewhere) instead of proceeding; reference the
resolveFetchedApplicationInstance call and the place in resolveAppContext that
returns instanceId/instanceLabel to implement the guard so invalid --instance
values are rejected early.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f9dece1-f8a0-497a-8790-d95bfff54044

📥 Commits

Reviewing files that changed from the base of the PR and between 303af7e and e33c494.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • .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
  • test/e2e/lib/test-user.ts

Comment thread packages/cli-core/src/commands/users/create-wizard.ts
Comment thread packages/cli-core/src/commands/users/interactive/instance-context.ts Outdated
Comment thread packages/cli-core/src/lib/config.ts
@wyattjoh
Copy link
Copy Markdown
Contributor Author

Stack: users-command-family

Part of a stacked PR chain. Do not merge manually.

- preserve whitespace in wizard password input (drop .trim())
- reject --secret-key combined with --app or --instance via usage error
- throw INSTANCE_NOT_FOUND in resolveAppContext when --instance does
  not match any fetched application instance
Copy link
Copy Markdown
Contributor

@Railly Railly left a comment

Choose a reason for hiding this comment

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

Awesome work! Just one thing: skill wasn't updated. recipes.md still points agents at clerk api /users -d '{...}' for user creation, so anyone running clerk skill install after this merges will keep defaulting to the raw API and never reach for users create. Worth blocking on a skill bump. Otherwise LGTM.

Mirror the examples block on `clerk users list` so `--help` for
create shows the three distinct invocation modes: curated flags,
inline -d body, and --file with --dry-run.
The bundled clerk skill's `recipes.md` defaulted to raw `clerk api /users -d '{...}'` for user creation, so agents installing the skill after this stack lands would never be steered toward the new dedicated command. Switch the basic create recipe to curated flags and the test-user recipes to `clerk users create -d '{...}'` (the same body shape, but routed through the user-management surface). The raw BAPI call is kept as a labeled fallback for fields the curated flags don't expose.
@wyattjoh wyattjoh merged commit 5784df6 into main Apr 27, 2026
10 checks passed
@wyattjoh wyattjoh deleted the wyattjoh/users-scaffolding-and-create branch April 27, 2026 21:44
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.

3 participants