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

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 as swamp auth whoami words them: reuse collectiveLines / standing from collective_standing.ts and rollUpPlanName from libswamp. Never copy them.
  • A collective API key's first line is Collective token: <slug> on <serverUrl>, as in auth whoami.
  • The final label follows swamp-club's standings (lib/app/views/whoami-view.ts there):
    • To upgrade: when the strongest plan is absent, free or personal. A Personal License is not a plan: no price, seats or subscription, and work use needs a paid plan.
    • To manage billing: for team, business and enterprise.
    • 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() returns undefined for both), exit 0 with the identity line then This 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"
}
  • collectives carries each entitlement with the fields whoami knows (the WhoamiCollectiveEntitlement shape), trial included for every plan, for the same billing-triage reason auth whoami --json gives. It is not the raw server payload: fields entitlementsOf() does not map, such as serveInstanceCount, are not included.
  • A collective token replaces username with collectiveSlug.
  • No entitlements: entitlementsReported: false, collectives: [].
  • plan and planName are null when absent; planName comes from rollUpPlanName.
  • billingUrl is ${serverUrl}/billing with any trailing / stripped, so it never contains //billing.

Data and errors

  • serverUrl uses the same precedence as auth whoami: SWAMP_CLUB_URL, else the stored credential.
  • Entitlement comes only from collectiveEntitlements, never from organizations (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() and cancelled() (Ctrl-C via ctx.signal) pass through as in whoami.

Design

  • src/libswamp/subscription/status.ts: subscriptionStatus(ctx, deps) iterates whoami(ctx, deps), passes loading_credentials, contacting_server and error through, and maps completed to { kind: "completed", status: SubscriptionStatusView }. Delegating keeps the auth.json refresh and verification-proof save identical to auth whoami. Also exports SubscriptionStatusEvent, SubscriptionStatusView and billingUrl(serverUrl).
  • src/presentation/renderers/subscription_status.ts: createSubscriptionStatusRenderer(mode).
  • src/cli/commands/subscription.ts (group, .action(groupCommandAction)) and src/cli/commands/subscription_status.ts (leaf, description and at least one .example()), building createAuthDeps({ serverUrlOverride: Deno.env.get("SWAMP_CLUB_URL"), identity }) as auth_whoami.ts does.
  • src/cli/mod.ts: register .command("subscription", subscriptionCommand) beside quest; add "subscription" to NON_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.ts pinning 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 null endsAt (no "(ends …)" suffix); a paid plan with and without subscriptionStatus; a free collective with privateExtensions: "allowed" and an expired trial ("covered", not "trial ended").
  • A2. collectiveLines([]) computes Math.max() over nothing, a width of -Infinity. Make it return [] for an empty list. The #3947 whoami tests must pass unedited.
  • A3. rollUpPlanName stays in libswamp: subscriptionStatus uses it for the JSON planName.

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, invalidApiKey and cancelled pass through; serverUrlOverride wins; billingUrl strips a trailing slash; the call carries ctx.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, null plan fields
  • src/presentation/renderers/collective_standing_test.ts (A1, A2)
  • src/cli/mod_test.ts: subscription in the isRepoScopedCommand repo-less loop
  • src/cli/auth_gate_exemptions_test.ts: pin ["subscription"] and ["subscription", "status"] 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 prints group help listing status when signed in
  • swamp subscription status words every collective exactly as swamp auth whoami does
  • 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 --json prints exactly one document with the documented keys
  • Works outside a swamp repo; gated without a credential
  • swamp auth whoami output 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 to swamp subscription start)
  • The inbox (slice 2)
  • Changes to the bundled swamp skill; agents find the command through swamp help
  • Widening entitlementsOf(), organizations or getCollectives()

Docs and UAT tests

Ship alongside this PR, in the sibling repos:

  • Manual (swamp-club repo): add content/manual/reference/subscription-commands.md, modelled on content/manual/reference/issue-commands.md. Document swamp 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 --json document with every key. Link it from content/manual/reference/swamp-account-requirement.md. It gains a start section in the follow-up issue.
  • UAT (swamp-uat repo): add tests/cli/subscription/status_test.ts, run against the compiled binary with the withStubClub stub from src/cli/helpers/stub_club.ts and the standing helpers in src/cli/helpers/whoami_standing.ts, as tests/cli/auth/whoami_test.ts does. 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_URL pointing at the stub; outside a swamp repo; signed out (the gate's block); bare swamp subscription printing group help; --json writing one document to stdout and nothing to stderr.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 6 MOREPR_LINKED+ 2 MORESESSION_SUMMARIZED

Shipped

10/26/2026, 12:55:03 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/26/2026, 12:25:04 AM

Sign in to post a ripple.