Relationships
#3950 swamp subscription status reports the plan and billing standing of the signed-in user and each collective, worded as auth whoami words it
Opened by stack72 · 10/26/2026· Shipped 10/26/2026
Summary
Add the swamp subscription command group and its first leaf, swamp subscription status, which reports the plan and billing standing of the signed-in user and each collective they belong to. It builds on swamp-club#3947 (shipped in #3370), which moved the standing helpers into src/presentation/renderers/collective_standing.ts and rollUpPlanName into src/libswamp/auth/whoami.ts. No swamp-club API change: it reads the GET /api/whoami response swamp auth whoami already reads.
swamp subscription start is a follow-up issue. Until it ships, the call to action in this command prints the billing URL rather than a command that does not exist yet.
Behaviour
Repo-less and gated like any other command (not added to UNGATED_COMMANDS). A bare swamp subscription prints the group's help via groupCommandAction once past the gate, as swamp issue does; signed out, the gate's standard block appears instead.
Log mode
stack72 ([REDACTED-EMAIL]) on https://swamp-club.com
Plan: Team
Collectives:
stack72 trial: 9 days left (ends 2026-11-03)
swamp-club Team active
To manage billing: https://swamp-club.com/billing- The identity line,
Plan:roll-up and collective lines are worded exactly asswamp auth whoamiwords them: reusecollectiveLines/standingfromcollective_standing.tsandrollUpPlanNamefrom libswamp. Never copy them. - A collective API key's first line is
Collective token: <slug> on <serverUrl>, as inauth whoami. - The final label follows swamp-club's standings (
lib/app/views/whoami-view.tsthere):To upgrade:when the strongest plan is absent,freeorpersonal. A Personal License is not a plan: no price, seats or subscription, and work use needs a paid plan.To manage billing:forteam,businessandenterprise.- No call to action when the strongest plan is
admin; the web hides the billing link for Admin standing too. - Unknown plan values are shown as sent and take
To manage billing:.
- When whoami reports no entitlements (an older or self-hosted swamp-club, or an empty list —
entitlementsOf()returnsundefinedfor both), exit 0 with the identity line thenThis Swamp Club server does not report subscriptions.
JSON mode
One document on stdout, nothing on stderr:
{
"serverUrl": "https://swamp-club.com",
"username": "stack72",
"plan": "team",
"planName": "Team",
"entitlementsReported": true,
"collectives": [
{ "slug": "stack72", "plan": "free", "planName": "Free",
"trial": { "state": "active", "daysRemaining": 9, "endsAt": "2026-11-03T00:00:00Z" } },
{ "slug": "swamp-club", "plan": "team", "planName": "Team", "subscriptionStatus": "active" }
],
"billingUrl": "https://swamp-club.com/billing"
}collectivescarries each entitlement with the fields whoami knows (theWhoamiCollectiveEntitlementshape), trial included for every plan, for the same billing-triage reasonauth whoami --jsongives. It is not the raw server payload: fieldsentitlementsOf()does not map, such asserveInstanceCount, are not included.- A collective token replaces
usernamewithcollectiveSlug. - No entitlements:
entitlementsReported: false,collectives: []. planandplanNamearenullwhen absent;planNamecomes fromrollUpPlanName.billingUrlis${serverUrl}/billingwith any trailing/stripped, so it never contains//billing.
Data and errors
serverUrluses the same precedence asauth whoami:SWAMP_CLUB_URL, else the stored credential.- Entitlement comes only from
collectiveEntitlements, never fromorganizations(AGENTS.md: that list is an authorization contract). - A revoked key is blocked by the auth gate before the command runs;
invalidApiKey()only surfaces if the key is revoked between the gate and the call.notAuthenticated()andcancelled()(Ctrl-C viactx.signal) pass through as inwhoami.
Design
src/libswamp/subscription/status.ts:subscriptionStatus(ctx, deps)iterateswhoami(ctx, deps), passesloading_credentials,contacting_serveranderrorthrough, and mapscompletedto{ kind: "completed", status: SubscriptionStatusView }. Delegating keeps theauth.jsonrefresh and verification-proof save identical toauth whoami. Also exportsSubscriptionStatusEvent,SubscriptionStatusViewandbillingUrl(serverUrl).src/presentation/renderers/subscription_status.ts:createSubscriptionStatusRenderer(mode).src/cli/commands/subscription.ts(group,.action(groupCommandAction)) andsrc/cli/commands/subscription_status.ts(leaf, description and at least one.example()), buildingcreateAuthDeps({ serverUrlOverride: Deno.env.get("SWAMP_CLUB_URL"), identity })asauth_whoami.tsdoes.src/cli/mod.ts: register.command("subscription", subscriptionCommand)besidequest; add"subscription"toNON_REPO_COMMANDS.src/libswamp/mod.ts: export the new names from their defining files.
Carried from swamp-club#3947
- A1. Add
src/presentation/renderers/collective_standing_test.tspinning the shared helpers directly: a single collective; every collective with no standing (bare slugs, no padding); a trial with 1 day left ("1 day left", singular); an active trial with nullendsAt(no "(ends …)" suffix); a paid plan with and withoutsubscriptionStatus; a free collective withprivateExtensions: "allowed"and an expired trial ("covered", not "trial ended"). - A2.
collectiveLines([])computesMath.max()over nothing, a width of-Infinity. Make it return[]for an empty list. The #3947 whoami tests must pass unedited. - A3.
rollUpPlanNamestays in libswamp:subscriptionStatususes it for the JSONplanName.
Tests
All in-process; none spawns the CLI or a browser. Deno.test("functionName: describes behavior", …) naming; reuse src/libswamp/auth/whoami_response.fixture.json where it fits.
-
src/libswamp/subscription/status_test.ts: fixture response maps to the view; no entitlements →entitlementsReported: false; collective token; Personal License and Admin standings;notAuthenticated,invalidApiKeyandcancelledpass through;serverUrlOverridewins;billingUrlstrips a trailing slash; the call carriesctx.signal -
src/presentation/renderers/subscription_status_test.ts: log identity,Plan:and aligned collective lines;To upgrade:for absent / free / personal,To manage billing:for paid and unknown, none for admin; the "does not report subscriptions" line; JSON exact document shape, trial kept for paid collectives,nullplan fields -
src/presentation/renderers/collective_standing_test.ts(A1, A2) -
src/cli/mod_test.ts:subscriptionin theisRepoScopedCommandrepo-less loop -
src/cli/auth_gate_exemptions_test.ts: pin["subscription"]and["subscription", "status"]as gated -
integration/json_mode_conformance_test.tsandintegration/cli_help_wording_test.tspass without edits; run all ofintegration/once
Acceptance
-
swamp subscriptionprints group help listingstatuswhen signed in -
swamp subscription statuswords every collective exactly asswamp auth whoamidoes - Call-to-action label follows the absent/free/personal · paid · admin rule
- An older server without entitlements exits 0 with the "does not report subscriptions" line
-
status --jsonprints exactly one document with the documented keys - Works outside a swamp repo; gated without a credential
-
swamp auth whoamioutput unchanged; no other command's output or network behaviour changes
Out of scope
swamp subscription start(follow-up issue; it switches the call to action toswamp subscription start)- The inbox (slice 2)
- Changes to the bundled swamp skill; agents find the command through
swamp help - Widening
entitlementsOf(),organizationsorgetCollectives()
Docs and UAT tests
Ship alongside this PR, in the sibling repos:
- Manual (swamp-club repo): add
content/manual/reference/subscription-commands.md, modelled oncontent/manual/reference/issue-commands.md. Documentswamp subscription status: what each standing means (plan, trial, covered, trial ended, Personal License, Admin), the call-to-action rule, the "does not report subscriptions" case,SWAMP_CLUB_URL, and the--jsondocument with every key. Link it fromcontent/manual/reference/swamp-account-requirement.md. It gains astartsection in the follow-up issue. - UAT (swamp-uat repo): add
tests/cli/subscription/status_test.ts, run against the compiled binary with thewithStubClubstub fromsrc/cli/helpers/stub_club.tsand the standing helpers insrc/cli/helpers/whoami_standing.ts, astests/cli/auth/whoami_test.tsdoes. Cover: a mixed account (paid, trial, covered, Personal License) in log and--json; the call-to-action label for free, Personal License, paid and Admin; an older server with no entitlements (exit 0, "does not report subscriptions",entitlementsReported: false); a collective API key;SWAMP_CLUB_URLpointing at the stub; outside a swamp repo; signed out (the gate's block); bareswamp subscriptionprinting group help;--jsonwriting one document to stdout and nothing to stderr.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.