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 readsorganizations.- The slug goes through
encodeURIComponentand 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-browsermirrorsswamp 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, elseDEFAULT_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: extendbillingUrl(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 withbillingUrl, then yieldsopening_browserand eitheropenedorbrowser_open_failed; with--no-browserit yieldsurl_readyand never calls the opener. ExportsSubscriptionStartEvent,SubscriptionStartDeps({ resolveServerUrl, openBrowser: (url) => Promise<boolean> }) andcreateSubscriptionStartDeps(), which wrapsopenBrowserfromsrc/infrastructure/process/browser.tsthe waysrc/libswamp/auth/login.tsdoes.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 insrc/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;--collectiveURL, including a slug with spaces and&; trailing slash stripped;SWAMP_CLUB_URLwins (viawithMockedEnv);DEFAULT_SWAMP_CLUB_URLwith no credentials;--no-browsernever calls the opener; an opener failure yieldsbrowser_open_failed, not an error -
src/libswamp/subscription/status_test.ts:billingUrlwith a collective -
src/presentation/renderers/subscription_start_test.ts: log lines for opened,--no-browserand failure; JSONopenedandcollectivevalues -
src/presentation/renderers/subscription_status_test.ts: the call to action namesswamp subscription startunder each label; none for admin -
src/cli/auth_gate_exemptions_test.ts: pin["subscription", "start"]as gated -
integration/json_mode_conformance_test.tsandintegration/cli_help_wording_test.tspass without edits; run all ofintegration/once
Acceptance
-
swamp subscriptiongroup help listsstatusandstart -
startopens<serverUrl>/billing, or<serverUrl>/o/<slug>/billingwith--collective -
--no-browserand a launcher failure print the URL and exit 0 -
start --jsonprints exactly{ url, opened, collective } - Works outside a swamp repo; gated without a credential
-
status's call to action namesswamp 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)
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.