Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/billing-orgs-shortcut-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk orgs enable/disable` and `clerk billing enable/disable/plans` shortcut commands for managing organizations, billing, and subscription plans from the CLI.
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Options:
-v, --version Output the version number
--mode <mode> Force interaction mode (human or agent). Defaults to
auto-detect based on TTY.
--verbose Show detailed error output
--verbose Show detailed output (enables debug messages)
-h, --help Display help for command

Commands:
Expand All @@ -48,6 +48,17 @@ Commands:
schema [options] Pull instance config schema from Clerk
patch [options] Partially update instance configuration (PATCH)
put [options] Replace entire instance configuration (PUT)
orgs|organizations Manage Clerk Organizations
enable [options] Enable organizations on the linked instance
disable [options] Disable organizations on the linked instance
billing Manage billing and subscription plans
enable [options] Enable billing on the linked instance
disable [options] Disable billing on the linked instance
plans Manage subscription plans
create [options] <slug> Create a subscription plan
list [options] List subscription plans
update [options] <slug> Update a subscription plan
remove [options] <slug> Remove a subscription plan
env Manage environment variables
pull [options] Pull environment variables from Clerk to .env.local
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
Expand Down Expand Up @@ -152,6 +163,71 @@ clerk config put
$ clerk config put --instance prod --file config.json Replace production config
$ clerk config put --file config.json --yes Skip confirmation prompt

clerk orgs enable
--force-selection Force organization selection on login
--auto-create Auto-create an organization for new users
--max-members <n> Maximum members per organization
--domains Enable verified domains
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)
Examples:
$ clerk orgs enable Enable organizations
$ clerk orgs enable --force-selection Enable and force org selection
$ clerk orgs enable --auto-create --max-members 10 Enable with auto-creation and member limit

clerk orgs disable
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)

clerk billing enable
--for <org|user> (required) Billing target type
--require-payment-method Require payment method for free trials
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)
Examples:
$ clerk billing enable --for org Enable org billing
$ clerk billing enable --for user Enable user billing

clerk billing disable
--for <org|user> (required) Billing target type
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)

clerk billing plans create <slug>
--name <name> Override display name (default: title-cased slug)
--amount <cents> (required) Monthly price in cents
--payer <org|user> Who pays
--currency <code> Currency code (default: usd)
--description <text> Plan description
--trial-days <n> Free trial length in days
--annual-amount <cents> Monthly equivalent when billed annually, in cents
--hidden Hide plan from end users
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)
Examples:
$ clerk billing plans create pro --amount 1999 --payer org
$ clerk billing plans create enterprise --name "Enterprise Plus" --amount 9999 --payer org --trial-days 14

clerk billing plans list
--json Output as JSON
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)

clerk billing plans update <slug>
--name <name> Update display name
--amount <cents> Update monthly price
--hidden Hide plan from end users
--visible Show plan to end users
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)
Examples:
$ clerk billing plans update pro --amount 2999
$ clerk billing plans update pro --hidden

clerk billing plans remove <slug>
--app <id> Application ID to target
--instance <id> Instance to target (dev, prod, or instance ID)

clerk env pull
--app <id> Application ID to target (works from any directory)
--instance <id> Instance to target (dev, prod, or a full instance ID)
Expand Down
178 changes: 178 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import {
billingEnable,
billingDisable,
plansCreate,
plansList,
plansUpdate,
plansRemove,
} from "./commands/billing/index.ts";

export function createProgram() {
const program = new Command()
Expand Down Expand Up @@ -414,6 +423,175 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(configPut);

// --- clerk orgs ---
const orgs = program
.command("orgs")
.alias("organizations")
.description("Manage Clerk Organizations")
.setExamples([
{ command: "clerk orgs enable", description: "Enable organizations" },
{
command: "clerk orgs enable --force-selection --max-members 10",
description: "Enable with options",
},
{ command: "clerk orgs disable", description: "Disable organizations" },
]);

orgs
.command("enable")
.description("Enable organizations on the linked instance")
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--force-selection", "Force organization selection on login")
.option("--auto-create", "Auto-create an organization for new users")
.option("--max-members <n>", "Maximum members per organization")
.option("--domains", "Enable verified domains")
.option("--yes", "Skip confirmation prompts")
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.

--yes is advertised on orgs enable/disable, billing enable/disable, and plans create/update/remove, but no implementation consults options.yes or calls confirm(). The commands always mutate silently. Users who see --yes will reasonably assume there is a prompt to skip.

Compare with config patch, which these shortcuts wrap: packages/cli-core/src/commands/config/push.ts:94-102 does if (!options.dryRun && isHuman() && !options.yes) { await confirm(...) }. Without an equivalent branch here, the shortcut is strictly more dangerous than the command it's supposed to replace.

Fix: either remove --yes and the yes?: boolean fields until a prompt exists, or add the real confirmation branch for every mutating command. Option two is the right call, these are production config edits.

.setExamples([
{ command: "clerk orgs enable", description: "Enable organizations" },
{
command: "clerk orgs enable --force-selection",
description: "Enable and force org selection",
},
{
command: "clerk orgs enable --auto-create --max-members 10",
description: "Enable with auto-creation and member limit",
},
])
.action(orgsEnable);

orgs
.command("disable")
.description("Disable organizations on the linked instance")
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.setExamples([{ command: "clerk orgs disable", description: "Disable organizations" }])
.action(orgsDisable);

// --- clerk billing ---
const billing = program
.command("billing")
.description("Manage billing and subscription plans")
.setExamples([
{ command: "clerk billing enable --for org", description: "Enable org billing" },
{
command: "clerk billing plans create pro --amount 1999 --payer org",
description: "Create a Pro plan",
},
{ command: "clerk billing plans list", description: "List all plans" },
]);

billing
.command("enable")
.description("Enable billing on the linked instance")
.addOption(createOption("--for <type>", "Billing target type").choices(["org", "user"]))
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--require-payment-method", "Require payment method for free trials")
.option("--yes", "Skip confirmation prompts")
.setExamples([
{ command: "clerk billing enable --for org", description: "Enable org billing" },
{ command: "clerk billing enable --for user", description: "Enable user billing" },
])
.action(billingEnable);

billing
.command("disable")
.description("Disable billing on the linked instance")
.addOption(createOption("--for <type>", "Billing target type").choices(["org", "user"]))
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.setExamples([
{ command: "clerk billing disable --for org", description: "Disable org billing" },
])
.action(billingDisable);

const plans = billing
.command("plans")
.description("Manage subscription plans")
.setExamples([
{ command: "clerk billing plans list", description: "List all plans" },
{
command: "clerk billing plans create pro --amount 1999 --payer org",
description: "Create a Pro plan at $19.99/mo",
},
]);

plans
.command("create")
.description("Create a subscription plan")
.argument("<slug>", "Plan slug (display name auto-derived via title case)")
.option("--name <name>", "Override display name")
.requiredOption("--amount <cents>", "Monthly price in cents")
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.

patchInstanceConfig() accepts dryRun (see lib/plapi.ts:156-184). clerk config patch exposes --dry-run at cli-program.ts:367. None of the new shortcuts plumb it through.

A maintainer who wants to preview billing enable --for org or plans remove pro on production has to fall back to the raw config patch command, defeating the point of the wrappers. These are the exact operations you most want to dry-run.

Fix: add --dry-run to every mutating subcommand and pass { dryRun: options.dryRun } into patchInstanceConfig, matching config/push.ts:107-115.

.addOption(createOption("--payer <type>", "Who pays").choices(["org", "user"]))
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.

Line 527 uses .requiredOption("--amount <cents>", …). Line 528 uses .addOption(createOption("--payer <type>", "Who pays").choices(["org", "user"])) with no .makeOptionMandatory(). packages/cli-core/src/commands/billing/README.md:18 claims --payer is (required).

When omitted, options.payer === undefined, so billing/index.ts:110 writes payer_type: undefined. JSON.stringify drops the field, sending a malformed plan to the API.

Fix:

.addOption(
  createOption("--payer <type>", "Who pays")
    .choices(["org", "user"])
    .makeOptionMandatory(),
)

Add a test covering the missing---payer rejection.

.option("--currency <code>", "Currency code (default: usd)")
.option("--description <text>", "Plan description")
.option("--trial-days <n>", "Free trial length in days")
.option("--annual-amount <cents>", "Monthly equivalent when billed annually, in cents")
.option("--hidden", "Hide plan from end users")
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.setExamples([
{
command: "clerk billing plans create pro --amount 1999 --payer org",
description: "Create a Pro plan at $19.99/mo for orgs",
},
{
command:
'clerk billing plans create enterprise --name "Enterprise Plus" --amount 9999 --payer org --trial-days 14',
description: "Create an Enterprise plan with a 14-day trial",
},
])
.action(plansCreate);

plans
.command("list")
.description("List subscription plans")
.option("--json", "Output as JSON")
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.setExamples([
{ command: "clerk billing plans list", description: "List all plans" },
{ command: "clerk billing plans list --json", description: "Output as JSON" },
])
.action(plansList);

plans
.command("update")
.description("Update a subscription plan")
.argument("<slug>", "Plan slug to update")
.option("--name <name>", "Update display name")
.option("--amount <cents>", "Update monthly price in cents")
.option("--currency <code>", "Update currency")
.option("--description <text>", "Update description")
.option("--trial-days <n>", "Update free trial days")
.option("--annual-amount <cents>", "Update annual amount")
.option("--hidden", "Hide plan from end users")
.option("--visible", "Show plan to end users")
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.setExamples([
{ command: "clerk billing plans update pro --amount 2999", description: "Update price" },
{ command: "clerk billing plans update pro --hidden", description: "Hide plan" },
])
.action(plansUpdate);

plans
.command("remove")
.description("Remove a subscription plan")
.argument("<slug>", "Plan slug to remove")
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.setExamples([
{ command: "clerk billing plans remove pro", description: "Remove the Pro plan" },
])
.action(plansRemove);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
80 changes: 80 additions & 0 deletions packages/cli-core/src/commands/billing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# clerk billing

Enable/disable billing and manage subscription plans on the linked instance.

## Usage

```
clerk billing enable --for <org|user> [options]
clerk billing disable --for <org|user> [options]
clerk billing plans create <slug> [options]
clerk billing plans list [options]
clerk billing plans update <slug> [options]
clerk billing plans remove <slug> [options]
```

## Options

### `enable` / `disable`

| Flag | Description |
| -------------------------- | ---------------------------------------------------- |
| `--for <org\|user>` | **(required)** Target billing type |
| `--require-payment-method` | Require payment method for free trials (enable only) |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |

### `plans create`

| Flag | Description |
| ------------------------- | ------------------------------------------------------------------- |
| `<slug>` | Plan slug (positional). Display name is auto-derived via title case |
| `--name <name>` | Override display name (default: title-cased slug) |
| `--amount <cents>` | **(required)** Monthly price in cents |
| `--payer <org\|user>` | **(required)** Who pays |
| `--currency <code>` | Currency code (default: usd) |
| `--description <text>` | Plan description |
| `--trial-days <n>` | Free trial length in days |
| `--annual-amount <cents>` | Monthly equivalent when billed annually, in cents |
| `--hidden` | Hide plan from end users |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |

### `plans list`

| Flag | Description |
| ----------------- | -------------------------------------- |
| `--json` | Output as JSON |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |

### `plans update`

| Flag | Description |
| ------------------------- | -------------------------------------- |
| `<slug>` | Plan slug to update (positional) |
| `--name <name>` | Update display name |
| `--amount <cents>` | Update monthly price |
| `--currency <code>` | Update currency |
| `--description <text>` | Update description |
| `--trial-days <n>` | Update free trial days |
| `--annual-amount <cents>` | Update annual amount |
| `--hidden` | Hide plan |
| `--visible` | Show plan |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |

### `plans remove`

| Flag | Description |
| ----------------- | -------------------------------------- |
| `<slug>` | Plan slug to remove (positional) |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | -------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config (for plans list/remove) |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch billing and plans config |
Loading
Loading