Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
42979ca
feat(users): add `clerk users list` subcommand
wyattjoh Apr 24, 2026
520f4e0
feat(users): register list in users menu with app-picker fallback
wyattjoh Apr 27, 2026
0a1e52b
refactor(users): trim --order-by choices to displayed and timestamp f…
wyattjoh Apr 27, 2026
66bed58
docs(skill): point users recipes at `clerk users list`
wyattjoh Apr 27, 2026
ec79778
fix(users): thread parent options into list and menu actions
wyattjoh Apr 27, 2026
aae6046
fix(users): keep --app/--instance from picker fallback on missing key
wyattjoh Apr 27, 2026
dbf4af0
feat(users): add open command skeleton with linked-profile path
wyattjoh Apr 30, 2026
32979fe
feat(users): support --print on users open
wyattjoh Apr 30, 2026
6a1d1b1
feat(users): emit structured JSON in agent mode for users open
wyattjoh Apr 30, 2026
8589fda
test(users): lock in --secret-key-alone rejection on users open
wyattjoh Apr 30, 2026
2e2d70c
feat(users): pick user interactively when no id provided
wyattjoh Apr 30, 2026
a64964a
test(users): cover targeting forwarding and NOT_LINKED propagation
wyattjoh Apr 30, 2026
95e7671
feat(users): register open action in users menu
wyattjoh Apr 30, 2026
2e25012
feat(users): wire users open into the CLI
wyattjoh Apr 30, 2026
b130bb1
docs(users): document users open command
wyattjoh Apr 30, 2026
54fab8f
docs: refresh CLI help output for users open
wyattjoh Apr 30, 2026
87ec755
docs(changeset): cover users list and users open
wyattjoh Apr 30, 2026
e96906d
docs(users): add users open consistency design
wyattjoh Apr 30, 2026
abd7070
refactor(users): surface labels in instance context
wyattjoh Apr 30, 2026
48d973d
fix(users): align open auth with list and create
wyattjoh Apr 30, 2026
1c88d04
fix(users): support paginated responses in user picker
wyattjoh Apr 30, 2026
1c8cfa9
test(users): cover open consistency contract
wyattjoh Apr 30, 2026
296dcc7
fix(users): align list and picker with actual BAPI /v1/users shape
wyattjoh Apr 30, 2026
f79c23f
fix(users): validate user ID format on users open
wyattjoh Apr 30, 2026
d1c3402
refactor(users): reword unreachable dashboard-target check as invariant
wyattjoh Apr 30, 2026
5a9aca1
fix(cli): accept negative integers in option lexer for clearer range …
wyattjoh Apr 30, 2026
3efe059
refactor(test/e2e): widen createTestUser to accept appId or secretKey
wyattjoh May 1, 2026
6ba895b
docs(skill): audit clerk-cli skill against current CLI source
wyattjoh May 1, 2026
23baf29
fix(users): restore auth-free dashboard targeting
wyattjoh May 1, 2026
1e62894
feat(users list): paginate JSON output with hasMore envelope
wyattjoh May 1, 2026
4482122
fix(users open): tighten agent-mode contract per PR review
wyattjoh May 1, 2026
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
10 changes: 10 additions & 0 deletions .changeset/users-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"clerk": minor
---

Add direct user-management commands to `clerk users`:

- `clerk users list` with pagination, query search, repeatable identifier filters (`--email-address`, `--phone-number`, `--username`, `--user-id`, `--external-id`), `--order-by` over Clerk's common user ordering fields, and an application picker when invoked without a linked project, env var, or targeting flag. `--limit` defaults to 100 and accepts 1-250. `--json` (and agent mode) emits `{ data, hasMore }` so callers can paginate without a separate count call; the human-mode table footer surfaces the next `--offset` when more pages are available. The interactive user picker (used by `clerk users open` and other update flows) shows a "More results, refine your search" hint when matches overflow its window.
- `clerk users open [user-id]` for opening a user's Clerk dashboard page in the browser, with interactive pickers for the application and the user, plus `--print` for emitting the URL.

Both commands appear in the interactive `clerk users` menu.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Commands:
whoami Show the current logged-in user
open Open Clerk resources in your browser
apps Manage your Clerk applications
users Manage Clerk users
users [options] Manage Clerk users
env Manage environment variables
config Manage instance configuration
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
Expand All @@ -49,4 +49,7 @@ Commands:
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
$ clerk skill install
```
162 changes: 162 additions & 0 deletions docs/superpowers/specs/2026-04-30-users-open-consistency-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Clerk Users Open Consistency Design

Date: 2026-04-30
Status: Ready for review
Owner: CLI

## Summary

Make `clerk users open` consistent with `clerk users list` and `clerk users create` by aligning its targeting and auth semantics with the existing Backend API command family.

This change fixes the current `users open` regressions without broadening the scope into a new interactive pagination system.

## Goals

- Make `users open` accept targeting flags with the same semantics as `users list` and `users create`.
- Fix the broken interactive picker so human-mode `clerk users open` can populate user choices again.
- Report the actual resolved instance label in human and agent output instead of hardcoding `development`.
- Keep the implementation narrow and local to `users open` and the existing picker.

## Non-Goals

- Adding lazy next-page or previous-page loading to the interactive picker.
- Making `clerk users list` auto-fetch all API pages.
- Extending `listage` with a generic remote-pagination abstraction.
- Redesigning the overall `users` command family beyond `open` consistency fixes.

## Problem Statement

`clerk users open` currently diverges from the rest of the `users` family in three ways:

1. It uses a custom instance-context resolver that rejects `--secret-key` combined with `--app` or `--instance`, unlike `users list` and `users create`.
2. Its interactive picker still expects a raw array response from `/v1/users`, so it fails when Clerk returns `{ data, totalCount }`.
3. It hardcodes `development` in agent output and human logging even when the command opens a user in another instance.

These inconsistencies create both functional regressions and confusing command semantics.

## Decision Summary

The approved contract is:

- `users open` should follow the same auth-resolution rule as `users list` and `users create`.
- `--secret-key` remains supported for Backend API user lookup.
- `--app` and `--instance` remain the source of dashboard URL targeting.
- `--secret-key` alone remains insufficient for `users open`, because opening the dashboard also requires a resolvable app target.
- The interactive picker should be fixed to parse Clerk's paginated `/users` response shape, but should remain a single-request result set per search term for now.

## Command Contract

### Supported Invocations

`clerk users open` should accept the same option set already used by `users list` and `users create`, with one command-specific constraint for dashboard URL construction.

Supported resolved invocations:

- linked project only
- `--app <app-id>`
- `--app <app-id> --instance <instance>`
- `--secret-key <key> --app <app-id>`
- `--secret-key <key> --app <app-id> --instance <instance>`

### Resolution Rules

For `users open`, resolution is split into two concerns:

- Backend API auth: resolved through the existing `resolveBapiSecretKey()` helper, exactly like `users list` and `users create`
- Dashboard targeting: resolved from app context so the command can build a URL from `appId`, `instanceId`, and `instanceLabel`

This yields the following behavior:

- If `--secret-key` is present, it is used for Backend API requests.
- If `--secret-key` is absent, Backend API auth is derived the same way as the other commands: `--app`, `CLERK_SECRET_KEY`, or linked-project resolution.
- Dashboard targeting comes from explicit `--app` and `--instance` when provided, otherwise from linked-project or interactive app selection in human mode.

### Unsupported Invocation

`clerk users open --secret-key <key>` without any resolvable app context remains invalid.

Reason:

- a secret key is enough to talk to `/v1/users`
- it is not enough to build the dashboard URL, which requires an application target and instance target

The usage error should explain that `users open` needs an app target to construct the dashboard URL.

## Interactive Picker Behavior

The picker used by `clerk users open` should remain search-based and should continue to fetch one result page per search term.

Approved behavior:

- keep the current `limit=20` request size
- keep one API request per search term
- parse both raw-array responses and `{ data, totalCount }` responses
- render the returned `data` rows as choices

Explicitly deferred:

- loading the next API page when the user arrows past the last loaded result
- loading the previous API page when the user arrows above the first loaded result
- aggregating all pages in memory

## Output Rules

`users open` should report the actual resolved target instance everywhere it surfaces command context.

Requirements:

- agent-mode JSON must emit the resolved `instanceLabel`
- human-mode status logging must print the resolved `instanceLabel`
- URL construction continues to use the resolved `appId` and `instanceId`

This fixes the current mismatch where the command opens the correct URL while claiming the target was `development`.

## Error Handling

### Missing Dashboard Context

When no app context can be resolved, `users open` should fail with the same class of linked-project or targeting errors already used elsewhere in the CLI.

If `--secret-key` is present but no app can be resolved, the message should make the missing requirement explicit: the command still needs an app target to build the dashboard URL.

### Picker Data Shape

If `/v1/users` returns an unexpected body shape, the picker should degrade to no choices rather than crashing. This preserves current prompt behavior while fixing the normal `{ data, totalCount }` case.

## Internal Architecture

Keep the implementation local and narrow.

Changes:

- update `users open` to resolve Backend API auth through `resolveBapiSecretKey()`
- resolve dashboard targeting through app-context resolution instead of the current all-in-one users resolver path
- update the picker mapper to read paginated `/users` responses
- remove the hardcoded `development` label in `users open`

Avoid:

- changing `users list` behavior
- changing `listage` APIs
- adding a new generic pagination abstraction in this pass

## Testing

Required coverage:

- `users open` accepts `--secret-key` combined with `--app` and optional `--instance`
- `users open --secret-key` alone still fails with an app-targeting usage error
- `users open` emits the resolved `instanceLabel` in agent mode
- `users open` logs the resolved `instanceLabel` in human mode
- `pickUser()` accepts `{ data, totalCount }` from `/v1/users`

Tests that should remain unchanged:

- `users list` manual pagination behavior
- any `listage` prompt behavior unrelated to the picker response shape

## Rollout Notes

This change intentionally fixes consistency and correctness without claiming support for interactive cross-page scrolling.

Future pagination work, if needed, should be treated as a separate design and implementation effort because the current prompt stack does not support remote boundary pagination by configuration alone.
70 changes: 68 additions & 2 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,78 @@ test("registers users as a top-level command", () => {
expect(users).toBeDefined();
});

test("registers users create as a subcommand", () => {
test("registers users create and list as subcommands", () => {
const program = createProgram();
const users = program.commands.find((command) => command.name() === "users")!;
const names = users.commands.map((command) => command.name());

expect(names).toContain("create");
expect(names).toEqual(expect.arrayContaining(["create", "list"]));
});

test("users list exposes common filters and pagination options", () => {
const program = createProgram();
const users = program.commands.find((command) => command.name() === "users")!;
const list = users.commands.find((command) => command.name() === "list")!;
const optionNames = list.options.map((option) => option.long);

expect(optionNames).toEqual(
expect.arrayContaining([
"--json",
"--limit",
"--offset",
"--query",
"--email-address",
"--phone-number",
"--username",
"--user-id",
"--external-id",
"--order-by",
"--secret-key",
"--app",
"--instance",
]),
);
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
}

test.each([
{
label: "--limit 0",
args: ["--limit", "0"],
expected: /Must be 1-250/,
},
{
label: "--limit 251",
args: ["--limit", "251"],
expected: /Must be 1-250/,
},
{
label: "--limit -5 (post-fix surfaces range message)",
args: ["--limit", "-5"],
expected: /Must be 1-250/,
},
{
label: "--limit abc",
args: ["--limit", "abc"],
expected: /Must be an integer/,
},
{
label: "--limit 1.5",
args: ["--limit", "1.5"],
expected: /Must be an integer/,
},
{
label: "--offset -1",
args: ["--offset", "-1"],
expected: /Must be >= 0/,
},
])("rejects $label", async ({ args, expected }) => {
await expect(parseUsersList(args)).rejects.toThrow(expected);
});
});

test("users create exposes --json output, curated flags, and -d/--data for inline request bodies", () => {
Expand Down
Loading