feat(users): add users command scaffolding and clerk users create#236
feat(users): add users command scaffolding and clerk users create#236
clerk users create#236Conversation
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 detectedLatest commit: ecf78cf The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
…, 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
|
!snapshot |
Snapshot publishednpm install -g clerk@1.1.1-snapshot.e33c494
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a new "clerk users" command family with a top-level interactive menu and a Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (44)
.changeset/users-scaffolding-and-create.md.gitignoreREADME.mdpackages/cli-core/package.jsonpackages/cli-core/src/cli-program.test.tspackages/cli-core/src/cli-program.tspackages/cli-core/src/commands/api/bapi.tspackages/cli-core/src/commands/api/index.test.tspackages/cli-core/src/commands/api/index.tspackages/cli-core/src/commands/link/index.test.tspackages/cli-core/src/commands/link/index.tspackages/cli-core/src/commands/users/README.mdpackages/cli-core/src/commands/users/create-wizard.test.tspackages/cli-core/src/commands/users/create-wizard.tspackages/cli-core/src/commands/users/create.test.tspackages/cli-core/src/commands/users/create.tspackages/cli-core/src/commands/users/index.test.tspackages/cli-core/src/commands/users/index.tspackages/cli-core/src/commands/users/interactive/attributes.test.tspackages/cli-core/src/commands/users/interactive/attributes.tspackages/cli-core/src/commands/users/interactive/instance-context.test.tspackages/cli-core/src/commands/users/interactive/instance-context.tspackages/cli-core/src/commands/users/interactive/pick-user.test.tspackages/cli-core/src/commands/users/interactive/pick-user.tspackages/cli-core/src/commands/users/lifecycle-runner.tspackages/cli-core/src/commands/users/menu.test.tspackages/cli-core/src/commands/users/menu.tspackages/cli-core/src/commands/users/output.tspackages/cli-core/src/commands/users/registry.tspackages/cli-core/src/commands/users/shared.tspackages/cli-core/src/lib/app-picker.tspackages/cli-core/src/lib/bapi-command.test.tspackages/cli-core/src/lib/bapi-command.tspackages/cli-core/src/lib/config.test.tspackages/cli-core/src/lib/config.tspackages/cli-core/src/lib/errors.tspackages/cli-core/src/lib/fapi.test.tspackages/cli-core/src/lib/fapi.tspackages/cli-core/src/lib/listage.test.tspackages/cli-core/src/lib/listage.tspackages/cli-core/src/lib/users.test.tspackages/cli-core/src/lib/users.tspackages/cli-core/src/test/integration/users-commands.test.tstest/e2e/lib/test-user.ts
|
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
Railly
left a comment
There was a problem hiding this comment.
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.
Summary
Adds the
clerk userscommand family with the first subcommand,clerk users create. The top-leveluserscommand exposes shared targeting (--app,--secret-key,--instance,--dry-run,--yes,--json) that upcoming subcommands will reuse.clerk users createaccepts 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-jsoncomposes 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 theapplications:managePlatform API scope.In human mode,
clerk usersinvoked with no subcommand opens an interactive menu that dispatches to registered actions, andclerk users createinvoked without curated flags or-d/--fileenters 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 byclerk link), so users don't have to runclerk linkfirst. 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.tsgained a per-choicestylehook 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), andcommands/users/lifecycle-runner.ts(shared runner for direct state transitions).Test plan
bun run format:checkbun run lintbun run typecheckbun run testbun run test:e2e:op(every framework fixture's test-user creation now runs throughclerk users create -d)clerk users create --helpand the curated-flags +-dinteractionclerk users createwizard with no project linked, confirm picker → wizard → BAPI handoff