Skip to content

feat(cli-core): add clerk orgs and clerk billing shortcut commands#219

Open
nicolas-angelo wants to merge 4 commits intomainfrom
feat/billing-orgs-shortcut-commands
Open

feat(cli-core): add clerk orgs and clerk billing shortcut commands#219
nicolas-angelo wants to merge 4 commits intomainfrom
feat/billing-orgs-shortcut-commands

Conversation

@nicolas-angelo
Copy link
Copy Markdown

Summary

Managing organizations and billing currently requires constructing raw JSON payloads with clerk config patch. This is error-prone, hard to remember, and not discoverable. These new shortcut commands provide an ergonomic CLI interface for the most common org and billing configuration tasks.

  • clerk orgs enable/disable — toggle organizations with optional configuration
  • clerk billing enable/disable — toggle billing for orgs or users
  • clerk billing plans create/list/update/remove — full plan lifecycle management

All commands are thin wrappers around the existing config patch API endpoint.

What changed

  • New clerk orgs command (commands/orgs/) — enable and disable subcommands that patch organization_settings. enable supports --force-selection, --auto-create, --max-members, and --domains. disable checks if org billing is active and warns before proceeding. Aliased as clerk organizations.
  • New clerk billing command (commands/billing/) — enable and disable subcommands that patch billing.organization_enabled or billing.user_enabled based on the required --for org|user flag.
  • New clerk billing plans subcommandcreate, list, update, and remove for managing subscription plans. create takes a positional slug and auto-derives the display name via title case (overridable with --name). remove uses ?destructive=true to delete the plan from the config.

Usage

Organizations

# Enable organizations
clerk orgs enable

# Enable with options
clerk orgs enable --force-selection --auto-create --max-members 10 --domains

# Disable organizations
clerk orgs disable

Billing

# Enable billing for organizations
clerk billing enable --for org

# Enable billing for users
clerk billing enable --for user

# Disable
clerk billing disable --for org

Plans

# Create a plan (slug is positional, display name auto-derived)
clerk billing plans create pro --amount 1999 --payer org
# Creates: slug "pro", name "Pro", $19.99/mo

# Override the display name
clerk billing plans create pro --name "Pro Plus" --amount 1999 --payer org

# Create with a free trial
clerk billing plans create enterprise --amount 9999 --payer org --trial-days 14

# Create a hidden plan
clerk billing plans create internal --amount 0 --payer org --hidden

# List all plans
clerk billing plans list
clerk billing plans list --json

# Update a plan
clerk billing plans update pro --amount 2999
clerk billing plans update pro --hidden

# Remove a plan
clerk billing plans remove pro

(App | Instance) Targeting

All commands resolve the linked app from the current project directory by default (via clerk link). Use --app and --instance to override:

clerk orgs enable --app app_abc123
clerk billing plans list --app app_abc123 --instance prod

Before / After

Before (manual JSON):

clerk config patch --json '{"organization_settings":{"enabled":true,"force_organization_selection":true}}' --yes
clerk config patch --json '{"billing":{"organization_enabled":true,"plans":{"pro":{"name":"Pro","amount":1999,"payer_type":"org","is_recurring":true,"publicly_visible":true}}}}' --yes

After (shortcut commands):

clerk orgs enable --force-selection
clerk billing enable --for org
clerk billing plans create pro --amount 1999 --payer org

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: a87ed32

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

This PR introduces CLI support for managing Clerk Organizations and Billing configuration. It adds two new top-level command groups—clerk orgs and clerk billing—with subcommands for enabling/disabling organizations, toggling billing for organizations or users, and performing full CRUD operations on billing subscription plans. The implementation includes command registration in the CLI program entry point, command handlers that resolve app/instance context and apply configuration patches via API calls, comprehensive test suites for both feature areas, accompanying documentation, and a changeset entry.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately and specifically describes the main change: adding new shortcut CLI commands for managing organizations and billing.
Description check ✅ Passed The description comprehensively explains the motivation, what changed, usage examples, and before/after comparisons directly related to the changeset.
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.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 2

🤖 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/billing/index.ts`:
- Around line 109-120: Validate numeric CLI flags before assigning to plan: for
options.amount, options.annualAmount, and options.trialDays parseInt results
must be checked for NaN and integer-ness (e.g., const amt =
parseInt(options.amount,10); if (Number.isNaN(amt) || !Number.isFinite(amt))
return error/exit), and only then set plan.amount, plan.annual_monthly_amount,
and plan.free_trial_days; for trialDays also ensure it's a non-negative integer
and set plan.free_trial_enabled true only when validation passes. Apply the same
checks for the other numeric flags referenced later (the block noted at lines
~201-210) so no invalid or partial numeric input (like "12abc" → 12 or NaN) is
sent to the API.

In `@packages/cli-core/src/commands/orgs/index.ts`:
- Around line 20-24: The patch payload construction uses
parseInt(options.maxMembers, 10) which allows invalid or non-positive values
(NaN or truncated numbers) to be sent to patchInstanceConfig; validate
options.maxMembers before adding to patch: ensure it's present, is an integer
string with no trailing chars, and is > 0 (e.g., via Number.isInteger(parsed)
and parsed > 0) and only then set patch.max_allowed_memberships to the parsed
integer; if validation fails, return/throw a clear user-facing error or CLI exit
indicating the --max-members value is invalid.
🪄 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: 9b25373c-146e-4858-b48e-3f4ebe70879f

📥 Commits

Reviewing files that changed from the base of the PR and between c586b39 and a87ed32.

📒 Files selected for processing (9)
  • .changeset/billing-orgs-shortcut-commands.md
  • README.md
  • packages/cli-core/src/cli-program.ts
  • packages/cli-core/src/commands/billing/README.md
  • packages/cli-core/src/commands/billing/index.test.ts
  • packages/cli-core/src/commands/billing/index.ts
  • packages/cli-core/src/commands/orgs/README.md
  • packages/cli-core/src/commands/orgs/index.test.ts
  • packages/cli-core/src/commands/orgs/index.ts

Comment on lines +109 to +120
amount: parseInt(options.amount, 10),
payer_type: options.payer,
is_recurring: true,
publicly_visible: !options.hidden,
};

if (options.currency) plan.currency = options.currency;
if (options.description) plan.description = options.description;
if (options.annualAmount) plan.annual_monthly_amount = parseInt(options.annualAmount, 10);
if (options.trialDays) {
plan.free_trial_enabled = true;
plan.free_trial_days = parseInt(options.trialDays, 10);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Demonstrates why parseInt is unsafe for CLI numeric flags.
bun -e 'console.log(parseInt("12abc", 10)); console.log(JSON.stringify({ amount: Number.NaN }))'

Repository: clerk/cli

Length of output: 96


🏁 Script executed:

# First, locate the file and check its structure
fd -t f "index.ts" | grep -E "billing|commands"

Repository: clerk/cli

Length of output: 759


🏁 Script executed:

# Read the file to see the actual code context
cat -n packages/cli-core/src/commands/billing/index.ts | head -250

Repository: clerk/cli

Length of output: 9504


🏁 Script executed:

# Check if there's any input validation or CLI option type definitions
rg -A 5 -B 5 "PlansCreateOptions|PlansUpdateOptions" packages/cli-core/src/commands/billing/

Repository: clerk/cli

Length of output: 3419


🏁 Script executed:

# Find where plansCreate and plansUpdate are called/registered
rg -B 5 -A 5 "plansCreate|plansUpdate" --type ts packages/cli-core/src

Repository: clerk/cli

Length of output: 13564


🏁 Script executed:

# Check for CLI argument parsing (commander, yargs, etc.)
find packages/cli-core/src -name "*.ts" | xargs grep -l "commander\|yargs\|option\|argument" | head -5

Repository: clerk/cli

Length of output: 278


🏁 Script executed:

# Check the cli-program.ts to see how the plans create/update options are defined
rg -B 10 -A 3 "plans\.command.*create" packages/cli-core/src/cli-program.ts

Repository: clerk/cli

Length of output: 35


🏁 Script executed:

# Look for the full option definitions for plans create and update
rg -B 20 "\.action\(plansCreate\)|\.action\(plansUpdate\)" packages/cli-core/src/cli-program.ts | head -80

Repository: clerk/cli

Length of output: 2080


🏁 Script executed:

# Check if there's any validation happening in the CLI framework itself
# Look at the Command class and createOption to see if they validate
rg -A 10 "createOption.*amount" packages/cli-core/src/cli-program.ts

Repository: clerk/cli

Length of output: 35


🏁 Script executed:

# Check if there are any tests that validate invalid numeric input
grep -r "parseInt\|NaN" packages/cli-core/src/commands/billing/

Repository: clerk/cli

Length of output: 754


🏁 Script executed:

# Verify that JSON.stringify converts NaN to null (standard JS behavior)
node -e "console.log(JSON.stringify({amount: NaN, value: parseInt('abc', 10)}))"

Repository: clerk/cli

Length of output: 81


🏁 Script executed:

# Check if there are any tests for invalid numeric input
cat packages/cli-core/src/commands/billing/index.test.ts | grep -A 10 -B 2 "invalid\|NaN\|abc\|parse"

Repository: clerk/cli

Length of output: 6224


🏁 Script executed:

# Check if there's any validation or error handling after the API call
rg -A 5 "patchInstanceConfig" packages/cli-core/src/commands/billing/index.ts | head -20

Repository: clerk/cli

Length of output: 670


🏁 Script executed:

# Look at the complete test file to see if there are edge cases tested
wc -l packages/cli-core/src/commands/billing/index.test.ts

Repository: clerk/cli

Length of output: 109


Add validation for numeric CLI flags before patching billing plans.

parseInt silently accepts partial input ("12abc" → 12) and converts invalid input to NaN, which serializes as null in JSON. This allows corrupted plan amounts and trial days to be sent to the API. The code needs explicit numeric validation.

Suggested fix
 function titleCase(slug: string): string {
   return slug
     .split(/[-_]/)
     .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
     .join(" ");
 }
+
+function parseNonNegativeInteger(value: string | undefined, flagName: string): number {
+  if (typeof value !== "string" || !/^\d+$/.test(value)) {
+    throwUsageError(`${flagName} must be a non-negative integer.`);
+  }
+
+  const parsed = Number(value);
+  if (!Number.isSafeInteger(parsed)) {
+    throwUsageError(`${flagName} must be a safe integer.`);
+  }
+
+  return parsed;
+}
 
 export async function plansCreate(slug: string, options: PlansCreateOptions): Promise<void> {
   const ctx = await resolveAppContext(options);
 
   const plan: Record<string, unknown> = {
     name: options.name || titleCase(slug),
-    amount: parseInt(options.amount, 10),
+    amount: parseNonNegativeInteger(options.amount, "--amount"),
     payer_type: options.payer,
     is_recurring: true,
     publicly_visible: !options.hidden,
   };
 
   if (options.currency) plan.currency = options.currency;
   if (options.description) plan.description = options.description;
-  if (options.annualAmount) plan.annual_monthly_amount = parseInt(options.annualAmount, 10);
-  if (options.trialDays) {
+  if (options.annualAmount !== undefined) {
+    plan.annual_monthly_amount = parseNonNegativeInteger(options.annualAmount, "--annual-amount");
+  }
+  if (options.trialDays !== undefined) {
     plan.free_trial_enabled = true;
-    plan.free_trial_days = parseInt(options.trialDays, 10);
+    plan.free_trial_days = parseNonNegativeInteger(options.trialDays, "--trial-days");
   }
   const plan: Record<string, unknown> = {};
   if (options.name) plan.name = options.name;
-  if (options.amount) plan.amount = parseInt(options.amount, 10);
+  if (options.amount !== undefined) plan.amount = parseNonNegativeInteger(options.amount, "--amount");
   if (options.currency) plan.currency = options.currency;
   if (options.description) plan.description = options.description;
-  if (options.annualAmount) plan.annual_monthly_amount = parseInt(options.annualAmount, 10);
+  if (options.annualAmount !== undefined) {
+    plan.annual_monthly_amount = parseNonNegativeInteger(options.annualAmount, "--annual-amount");
+  }
   if (options.hidden) plan.publicly_visible = false;
   if (options.visible) plan.publicly_visible = true;
-  if (options.trialDays) {
+  if (options.trialDays !== undefined) {
     plan.free_trial_enabled = true;
-    plan.free_trial_days = parseInt(options.trialDays, 10);
+    plan.free_trial_days = parseNonNegativeInteger(options.trialDays, "--trial-days");
   }

Also applies to: 201-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli-core/src/commands/billing/index.ts` around lines 109 - 120,
Validate numeric CLI flags before assigning to plan: for options.amount,
options.annualAmount, and options.trialDays parseInt results must be checked for
NaN and integer-ness (e.g., const amt = parseInt(options.amount,10); if
(Number.isNaN(amt) || !Number.isFinite(amt)) return error/exit), and only then
set plan.amount, plan.annual_monthly_amount, and plan.free_trial_days; for
trialDays also ensure it's a non-negative integer and set
plan.free_trial_enabled true only when validation passes. Apply the same checks
for the other numeric flags referenced later (the block noted at lines ~201-210)
so no invalid or partial numeric input (like "12abc" → 12 or NaN) is sent to the
API.

Comment on lines +20 to +24
const patch: Record<string, unknown> = { enabled: true };
if (options.forceSelection) patch.force_organization_selection = true;
if (options.domains) patch.domains_enabled = true;
if (options.maxMembers) patch.max_allowed_memberships = parseInt(options.maxMembers, 10);
if (options.autoCreate) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate --max-members before patching config.

Line 23 turns invalid input into unsafe payloads: parseInt("abc", 10) becomes NaN, and JSON.stringify sends that as null; parseInt("10abc", 10) silently sends 10. Reject non-positive/non-integer values before calling patchInstanceConfig.

Proposed fix
-import { withApiContext } from "../../lib/errors.ts";
+import { throwUsageError, withApiContext } from "../../lib/errors.ts";
@@
-  if (options.maxMembers) patch.max_allowed_memberships = parseInt(options.maxMembers, 10);
+  if (options.maxMembers !== undefined) {
+    const maxMembers = Number(options.maxMembers);
+    if (!Number.isInteger(maxMembers) || maxMembers < 1) {
+      throwUsageError("--max-members must be a positive integer.");
+    }
+    patch.max_allowed_memberships = maxMembers;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli-core/src/commands/orgs/index.ts` around lines 20 - 24, The patch
payload construction uses parseInt(options.maxMembers, 10) which allows invalid
or non-positive values (NaN or truncated numbers) to be sent to
patchInstanceConfig; validate options.maxMembers before adding to patch: ensure
it's present, is an integer string with no trailing chars, and is > 0 (e.g., via
Number.isInteger(parsed) and parsed > 0) and only then set
patch.max_allowed_memberships to the parsed integer; if validation fails,
return/throw a clear user-facing error or CLI exit indicating the --max-members
value is invalid.

.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.

`Removing plan ${cyan(slug)} on ${ctx.appLabel} (${ctx.instanceLabel})...`,
() =>
withApiContext(
patchInstanceConfig(ctx.appId, ctx.instanceId, config, { destructive: true }),
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.

plansRemove fetches the current config, deletes the slug in memory, and PATCHes with { destructive: true }. No prompt, no diff, no dry-run. A typo in <slug> or the active instance silently wipes a live subscription plan.

The equivalent clerk config patch path would print a diff, warn, and ask "Proceed?". The shortcut regresses this safety.

Fix: gate the mutation behind isHuman() && !options.yes confirmation, support --dry-run, and show the plan (name, price, payer) being removed before confirming.

.argument("<slug>", "Plan slug (display name auto-derived via title case)")
.option("--name <name>", "Override display name")
.requiredOption("--amount <cents>", "Monthly price in cents")
.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.

.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.


const plan: Record<string, unknown> = {
name: options.name || titleCase(slug),
amount: parseInt(options.amount, 10),
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.

Every numeric option (--amount, --max-members, --trial-days, --annual-amount) goes through raw parseInt(value, 10) with no validation, here and at billing/index.ts:117, 120, 201, 204, 209, plus orgs/index.ts:23.

parseInt("abc", 10) returns NaN, which JSON.stringify emits as null. So clerk billing plans create pro --amount abc --payer org sends amount: null to the API, and --max-members abc sends max_allowed_memberships: null. Neither failure mode is user-friendly.

Fix: use Commander's option argParser at the registration site:

.option("--amount <cents>", "Monthly price in cents", (v) => {
  const n = Number.parseInt(v, 10);
  if (!Number.isFinite(n) || n < 0) {
    throwUsageError(`Invalid --amount: "${v}". Must be a non-negative integer.`);
  }
  return n;
})

Or extract a shared parseNonNegativeInt helper.


const billing = current.billing as Record<string, unknown> | undefined;
if (billing?.organization_enabled) {
log.warn(
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.

orgsDisable fetches billing config and calls log.warn("…Disabling organizations will also disable org billing."), then unconditionally issues the PATCH.

Emitting a warning and then doing the thing the warning describes is worse than not warning at all, in a CI log it looks like the warning was heeded. If the dependency is worth detecting (it is), it's worth gating behind confirmation.

Fix: when billing.organization_enabled is true, require explicit confirmation in human mode or a --force / --yes flag to proceed. In non-human mode, consider failing with a usage error directing the user to pass --force.


const config = { billing: patch };

const result = await withSpinner(
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.

config push.ts:75-92 fetches the current config, computes a diff with hasConfigChanges / printDiff, prints it, then prompts. The shortcut commands send blind.

If confirmation is added per the critical-priority findings, pair it with the diff, otherwise the prompt just says "Proceed?" with no context about what's about to change.

Fix: reuse hasConfigChanges / printDiff from config/push.ts. They already handle partial-payload patch mode, which is what these wrappers generate.


log.data(JSON.stringify(result, null, 2));
log.success(`Billing enabled for ${target === "org" ? "organizations" : "users"}`);
}
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.

log.data(JSON.stringify(result, null, 2)) emits the full server response on stdout after every mutation, here and at billing/index.ts:80, 128, 229, 267, plus orgs/index.ts:41, 73.

Per .claude/rules/logging.md, log.data is stdout for pipeable output. config patch does this because the user opted into a low-level config operation. For clerk orgs enable the user asked a single-intent question, piping a ~200-line config blob out every time is noise and will trip up shell pipelines.

Fix: demote to log.debug(...) for routine success, or emit only when --json / --verbose is set. Keep the log.success(...) line as the human-facing signal.

const patch: Record<string, unknown> = { enabled: true };
if (options.forceSelection) patch.force_organization_selection = true;
if (options.domains) patch.domains_enabled = true;
if (options.maxMembers) patch.max_allowed_memberships = parseInt(options.maxMembers, 10);
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 (options.forceSelection) patch.force_organization_selection = true;
if (options.domains) patch.domains_enabled = true;
if (options.autoCreate) {  enabled: true }

There's no --no-force-selection or --force-selection=false. To turn a flag off while keeping orgs enabled, the user has to fall back to clerk config patch, partially defeating the wrapper.

Fix: either (a) document in the README that these flags are one-way and point users to config patch for the inverse, or (b) accept Commander's boolean negation (--no-force-selection) and write false when it's set.

const currency = (plan.currency as string) || "usd";
const price = amount === 0 ? "Free" : `${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`;
const payer = plan.payer_type as string;
const visible = plan.publicly_visible !== false;
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.

cyan(plan.name as string) casts without checking. If the config contains a plan with no name field (legacy or malformed), output reads undefined (slug) — $X.XX/mo ….

Fix: cyan((plan.name as string) ?? slug).

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