Skip to content

feat: manage members and invitations (member, invitation) - #57

Merged
alukach merged 4 commits into
mainfrom
feat/memberships
Oct 19, 2026
Merged

alukach merged 4 commits into
mainfrom
feat/memberships

Conversation

@alukach

@alukach alukach commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Adds membership management to the CLI, modelled on product.

Commands

source-coop member list my-org [--state invited] [--json] [-i]
source-coop member view my-org alice [--json] [-i]
source-coop member invite my-org alice bob --role maintainers
source-coop member invite my-org/my-data -A people.txt --role read_data   # one account per line, # comments
source-coop member edit my-org alice --role owners
source-coop member revoke my-org alice          # also withdraws a pending invitation
source-coop member leave my-org
source-coop invitation list [--json] [-i]
source-coop invitation accept|reject my-org/my-data
  • Scope is ACCOUNT (an org) or ACCOUNT/PRODUCT. Members are named by account ID. The CLI finds the membership ID by listing, because the API has no GET for a single membership.
  • Bulk invites: the API takes one invitation per request, so invite sends one per account and carries on past refusals, reporting each. It exits with the last failure's code (e.g. 3 for "already a member"). --json prints the memberships it created as an array. When prompted, the accounts not yet invited are saved as a draft for the next run.
  • Prompts: who, then role, then a review, with ← going back. The default role is read_data, and owners is offered only to people who can grant it (mirrors grantors in the web app's authz). A 400 on role or account_id re-asks just that field.
  • Confirmation:
    • revoke asks y/n.
    • Revoking yourself, or leave, asks you to type the name.
    • Without a terminal, both need --yes.
  • -i: e (change role) and d (revoke) only from a member's screen. An invitation's screen offers only d, because the API won't change an invitation's role. invitation list -i offers a to accept and r to reject.
  • API: everything goes through /memberships (source.coop#679): GET with membership_account_id/product_id for a scope's members and account_id/state for your own, POST to invite, and PATCH /memberships/{id} for a role or a state (member accepts, revoked rejects, revokes or leaves). Production doesn't serve it yet; staging does.
  • Naming: invite and revoke keep create and delete as hidden aliases. AGENTS.md and the convention test now say so.
  • Shared code: main.rs puts the --api-url/--proxy-url flags into a shared Site struct with a connect() helper used by all three API commands. A few product.rs helpers are now pub(crate).

Demos

Each GIF links to the same session on asciinema.org, where it can be paused, rewound and its text copied. Recorded (GIFs with VHS, sessions with asciinema) against a stateful stub API with made-up accounts (signed in as dana, an owner of acme-lab). The tapes and stub are on the demos/pr-57 branch, which exists only to host these images.

`member list` — An org's members (its products' members are left out), a product's own, only invitations, and `--json`.

member-list

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`member view` — One member, and the whole record with `--json`.

member-view

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`member invite` — Bulk: account IDs from the command line plus `-A people.txt` (`#` comments skipped). It carries on past an existing member (409) and an unknown account (404), then exits with the last failure's code. After that, the prompted flow: who, then role, then review.

member-invite

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`member edit` — With `--role`, then prompted, starting from the current role.

member-edit

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`member revoke` — Withdrawing an invitation, removing a member, and revoking yourself, which needs the name typed back (typed wrong here, so nothing changes).

member-revoke

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`member leave` — Typed confirmation, then leaving again: not a member, exit 5.

member-leave

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`member list -i` — Open bob, `e` to change his role, back; open carol's invitation, `d` to withdraw it; `/` to filter.

member-list-i

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`invitation list` / `accept` / `reject`

invitation

▶ Pausable recording on asciinema.org: pause, scrub and copy text

`invitation list -i` — Open one and `a` to accept, open the next and `r` to reject.

invitation-list-i

▶ Pausable recording on asciinema.org: pause, scrub and copy text

Not in this PR

  • No pending-invitation hint in auth status: that command only reads the local cache, and the hint would mean a network call.
  • No read-only "via the org" section in a product's member list: the README says org members reach its products instead.
  • No n key to invite from member list -i: it would break the "changes only from a record's screen" rule. Easy to add if we want it.
  • No account search when inviting: the API has no REST endpoint for searching accounts, so account IDs are typed.

Server-side follow-ups (source.coop)

  • Last owner: there's no protection, so the only owner can revoke or demote themselves. The CLI makes you type the name to revoke yourself, but the rule belongs on the server.

Testing

  • cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test and cargo test --features staging pass: 140 tests, 29 of them new.
  • The new tests use the scripted prompter against wiremock, and check the full asked lists.
  • Checked against staging's OpenAPI spec, and with a signed-out GET /memberships?membership_account_id=… on staging, which returns the page the CLI reads. Not yet run end to end against staging while signed in: invites, role changes and accepting are covered by mocked tests only.
  • The demos were recorded against a stub of the old endpoints. The commands look and behave the same on the new ones.

🤖 Generated with Claude Code

@claude

claude Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Claude finished @alukach's task in 20s —— View job


✅ No blocking issues — safe to merge.

I read the diff for main.rs, product.rs, README.md and AGENTS.md, and the invite, edit, revoke, leave and role-grant paths in member.rs. I didn't build the project or run the tests.

  • Bulk invite carries on past refusals and exits with the last failure's code, as the PR describes. Only the accounts that failed are saved to the draft.
  • Revoking yourself asks for the name to be typed back, and leave always does. Without a terminal both need --yes.
  • Site::connect keeps the same ordering as the old product path, so behaviour there is unchanged.
  • grantable_roles always offers at least maintainers, so roles[...] in reask and edit can't index an empty list.
  • Neither revoke --yes nor leave --yes makes a sign-in check before the request. They go straight to the API, which refuses if you aren't signed in. That's acceptable.

Simplify (ponytail)

  • src/member.rs:~896-937: the nested loop with reask retry inside the bulk send is a lot of machinery for a case that is already rare. Reporting the 400 and letting the draft carry the account to the next run would drop reaskable and reask. The retry matches the rule in AGENTS.md that the API is the only judge and rejected fields are re-asked, so keep it if that rule applies here.
  • src/member.rs:731: split_accounts is only used for prompted input. Skip it if one account per prompt is acceptable.

💰 Estimated review cost: $0.19 · 0m20s · 8 turns

alukach and others added 3 commits October 19, 2026 12:40
`source-coop member list|view|invite|edit|revoke|leave` manages who
belongs to an organization or product; `source-coop invitation
list|accept|reject` answers your own invitations. `member invite` takes
several account IDs and `--accounts-file` (one per line) and sends one
invitation each, carrying on past refusals. `-i` pickers, `--json`,
prompts with back/review, and drafts follow the product commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vitations by where

Accepting, rejecting and leaving printed "Accepted dana of geo-commons";
the invitation screen was titled with your own account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased on main, whose convention test now wants every resource
positional to complete: SCOPE offers your accounts and their products,
a member the named scope's members, invite a product's org members not
on it, and accept/reject/leave your own invitations and memberships.

From the review on #57:
- a resumed invite draft is shown for review before sending
- a non-string account_id in -F is an error instead of being dropped
- a rejected role is re-asked from the roles you can grant
- member list -i honours --state
- one helper picks the member for edit -i and revoke -i, and a plain
  loop replaces the VecDeque in invite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad without copying the request

From the second review on #57.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach marked this pull request as ready for review October 19, 2026 19:49
@alukach
alukach merged commit fc59ce9 into main Oct 19, 2026
10 checks passed
@alukach
alukach deleted the feat/memberships branch October 19, 2026 19:50
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.

1 participant