Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneesstack72

Relationships

#3951 swamp subscription start opens the Swamp Club billing page, or a collective's with --collective, and status points its call to action at it

Opened by stack72 · 10/26/2026· Shipped 10/26/2026

Summary

Add swamp subscription start, which opens the Swamp Club billing page in a browser, and point swamp subscription status's call to action at it. Builds on swamp-club#3950 (merged in #3372), which added the subscription group, status, and billingUrl() in src/libswamp/subscription/status.ts. Depends on nothing else: swamp-club#3949, which sends signed-out visitors on /o/<slug>/billing through login, has shipped.

Manual pages and swamp-uat tests for the whole swamp subscription set are tracked in one closing issue after this one, not here.

Behaviour

Repo-less and gated, like status. The command makes no network call of its own; the auth gate may verify the credential first, and blocks a revoked key before the command runs.

Invocation Opens Log output
swamp subscription start <serverUrl>/billing Opening <url> in your browser.
swamp subscription start --collective acme <serverUrl>/o/acme/billing same line with that URL
swamp subscription start --no-browser nothing Manage billing at <url>
browser launcher fails (headless Linux, SSH) nothing Could not open a browser. Open this URL to manage billing: then the URL indented; exit 0
  • --collective <slug> opens /o/<slug>/billing, not /billing?collective=. That route already exists on swamp-club and redirects a personal collective to /billing, so the CLI never needs to know which collectives are personal, and never reads organizations.
  • The slug goes through encodeURIComponent and is not checked against the user's memberships; the billing page decides what the user may manage (a plain member is redirected to the collective page).
  • --no-browser mirrors swamp start --no-browser, with the same help text: "Print the URL instead of opening a browser".
  • serverUrl: SWAMP_CLUB_URL, else the stored credential's server, else DEFAULT_SWAMP_CLUB_URL. A trailing / is stripped, so the URL never contains //.

JSON mode, one document on stdout and nothing on stderr:

{ "url": "https://swamp-club.com/o/acme/billing", "opened": true, "collective": "acme" }

opened is false for --no-browser and for a launcher failure; collective is null when not given. A launcher failure is not an error exit in either mode: the URL is the result.

status call to action

swamp subscription status currently ends with To upgrade: <billingUrl> or To manage billing: <billingUrl> (src/presentation/renderers/subscription_status.ts), because start did not exist. Switch the log line to swamp subscription start, keeping callToActionLabel() and its rule unchanged. status --json keeps its billingUrl key as is.

Design

  • src/libswamp/subscription/status.ts: extend billingUrl(serverUrl, collective?) so a collective gives /o/<encoded slug>/billing; existing callers are unchanged.
  • src/libswamp/subscription/start.ts: subscriptionStart(ctx, deps, input) builds the URL with billingUrl, then yields opening_browser and either opened or browser_open_failed; with --no-browser it yields url_ready and never calls the opener. Exports SubscriptionStartEvent, SubscriptionStartDeps ({ resolveServerUrl, openBrowser: (url) => Promise<boolean> }) and createSubscriptionStartDeps(), which wraps openBrowser from src/infrastructure/process/browser.ts the way src/libswamp/auth/login.ts does.
  • src/presentation/renderers/subscription_start.ts: createSubscriptionStartRenderer(mode).
  • src/cli/commands/subscription_start.ts: leaf with --collective <slug:string> and --no-browser, a description and at least one .example(); register it in src/cli/commands/subscription.ts.
  • src/libswamp/mod.ts: export the new names from their defining files.

Unit tests

In-process only; none spawns the CLI or opens a browser.

  • src/libswamp/subscription/start_test.ts: default URL; --collective URL, including a slug with spaces and &; trailing slash stripped; SWAMP_CLUB_URL wins (via withMockedEnv); DEFAULT_SWAMP_CLUB_URL with no credentials; --no-browser never calls the opener; an opener failure yields browser_open_failed, not an error
  • src/libswamp/subscription/status_test.ts: billingUrl with a collective
  • src/presentation/renderers/subscription_start_test.ts: log lines for opened, --no-browser and failure; JSON opened and collective values
  • src/presentation/renderers/subscription_status_test.ts: the call to action names swamp subscription start under each label; none for admin
  • src/cli/auth_gate_exemptions_test.ts: pin ["subscription", "start"] as gated
  • integration/json_mode_conformance_test.ts and integration/cli_help_wording_test.ts pass without edits; run all of integration/ once

Acceptance

  • swamp subscription group help lists status and start
  • start opens <serverUrl>/billing, or <serverUrl>/o/<slug>/billing with --collective
  • --no-browser and a launcher failure print the URL and exit 0
  • start --json prints exactly { url, opened, collective }
  • Works outside a swamp repo; gated without a credential
  • status's call to action names swamp subscription start; its label rule and JSON are unchanged
  • No other command's output or network behaviour changes

Out of scope

  • Manual pages and swamp-uat tests (one closing issue for the whole set)
  • Checkout, plan changes, cancellation or taking a Personal License from the terminal; payment stays on the web
  • Changes to the bundled swamp skill
  • The inbox (slice 2)
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 9 MOREPR_LINKED+ 2 MORESESSION_SUMMARIZED

Shipped

10/26/2026, 1:32:22 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/26/2026, 12:57:18 AM

Sign in to post a ripple.