Skip to content

feat(cli): offer a pinned profile after login on a terminal - #75

Merged
thebrandonlucas merged 1 commit into
feat/cli-wallets-all-orgsfrom
feat/cli-login-pinned-profile
Oct 21, 2026
Merged

thebrandonlucas merged 1 commit into
feat/cli-wallets-all-orgsfrom
feat/cli-login-pinned-profile

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Stack

One stack, merged bottom up:

  1. feat(cli): name the wallets that could pay a BOLT11 invoice #73: wallets that could pay a BOLT11 invoice (base master)
  2. fix(cli): name each candidate's environment when --env is not given #77: tee8z's review fixes for feat(cli): name the wallets that could pay a BOLT11 invoice #73 (base feat(cli): name the wallets that could pay a BOLT11 invoice #73)
  3. feat(cli): list wallets in every organization with --all-orgs #74: wallets list --all-orgs (base fix(cli): name each candidate's environment when --env is not given #77)
  4. This PR: offer a pinned profile after login (base feat(cli): list wallets in every organization with --all-orgs #74)

Design

This changes login and picker behavior, so here is the design before the code.

Current behavior (checked on master + #74):

  • voltage login saves the login and prints account, email, authenticated, replaced. It pins no organization or environment: a login's Account has organization_id: None.
  • A profile is used only with --profile NAME. There is no default profile, and voltage profiles create is the only way to make one.
  • The terminal picker always asks, even with exactly one organization, environment, or wallet. A person presses Enter on a one-item list.

New behavior (terminal only):

Step Behavior
Login finished, no profile uses this login Save a profile for this login, so later commands need only --profile NAME. Press Esc to skip.
Organization The only one is taken: Organization: Acme ID (the only one). With several, the picker asks.
Environment Same as for the organization.
Profile name: Text prompt, default on Enter.
Saved Saved profile NAME. Use it with --profile NAME. The login result shows profile NAME.
Esc, no environments, a name already used, a blank name, or a listing error No profile was saved[: why]. Create one later with voltage profiles create NAME --account ACCOUNT. Login still exits 0.
Ctrl-C At a prompt or during the offer's requests (the organization token exchange, the environment listing), exits 130 with Interrupted while saving a profile; the login for NAME is saved and the voltage profiles create hint. SubmissionState marks the offer as under way, apart from the session change that the offer's own token refresh sets and settles.

The picker change applies to every command: the only organization or environment is taken without asking. A wallet is always asked for, even when there is only one, because a payment with --execute spends from it, and a keypress is the confirmation.

Saving: the profile is saved through Settings::create_profile, which voltage profiles create now uses too. It takes the credential lock, re-reads config.toml, checks the name again, then inserts and saves. A change another command made while the prompt was open is kept, and a name it took is refused with Profile NAME already exists. Both paths now use that message, where profiles create used to say Profile already exists.

What stays the same:

  • Off a terminal, with --json or --output json|ndjson, with stdout piped, or with --no-input: no prompt, no extra request. voltage login --json output is unchanged (the profile field is omitted when None).
  • When a profile already uses the login, which includes signing in again, nothing is offered and nothing more is requested.
  • Scope resolution doesn't change. A saved profile still applies only with --profile. I did not add a default profile: it would change which credential every command uses, and it could shadow VOLTAGE_API_KEY in CI. That is a separate product decision.

Checks

On head 7c1a78a: the gate passes in nix develop (130 unit, 119 integration), and CRAP on this tip shows 0 of 605 measured functions at or above 20 (highest wait_for_payment at 19.60). The new SIGINT test fails without the fix, with Interrupted before any resource change was submitted.

  • voltage-pr-review self-review against feat(cli): list wallets in every organization with --all-orgs #74's head, with the Rust style checklist.
  • Every new path is tested on a pseudo-terminal: a profile saved for the only organization and environment, a pick between two organizations then Esc at the name, an existing name, a blank name, Ctrl-C at the name (130, asserting the message and hint), SIGINT during a delayed environment listing (130, same message, login saved, no profile), a profile saved by another command while the prompt is open (both kept), a name taken by another command while the prompt is open (refused, theirs kept), profiles create with a duplicate name, no environments, a profile that already uses the login (no offer, one /users/current request), and the only wallet still asked for. The existing picker tests now assert the "(the only one)" notices, and the Esc test uses two organizations and two environments so that each picker really asks.
  • Gate in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked, and python3 scripts/check-coverage.py all pass.
  • CRAP (outside nix) on ff8b488: 0 of 602 measured functions at or above 20. The highest score is 19.60 for wait_for_payment, unchanged.

After `voltage login` on a terminal, the CLI offers to save a profile
pinned to an organization and environment, so later commands need only
`--profile NAME`. It reuses the scope picker, then asks for the name.
Esc skips it, and a failure leaves the login saved and names
`voltage profiles create`. Ctrl-C exits 130 and says that the login is
saved. Nothing is offered when a profile already uses the login, off a
terminal, with `--json`, or with `--no-input`.

The profile is saved through `Settings::create_profile`, which
`profiles create` now uses too: it takes the credential lock and reads
the configuration again before adding the profile, so a change another
command made while the prompt was open is kept, and a name it took is
refused with the same message.

The picker now takes the only organization or environment without
asking, and says so. A wallet is still always asked for, because a
payment spends from it.
tee8z
tee8z previously approved these changes Oct 21, 2026

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved 7c1a78a against feat/cli-wallets-all-orgs (#74 24cd775). The changes requested on #73 still block this stack.

  • The offer runs only when can_ask is true and no profile already uses the login, so --json, --no-input, a pipe and a re-login are unchanged, and send no extra request.
  • The login is saved before the offer starts. Esc, no environments, a blank or taken name, or a listing failure each leave it saved with a profiles create notice, and the login exits 0.
  • Ctrl-C at a prompt or during the offer's requests reports profile_offer_interrupted. profile_offer is kept apart from session, so the offer's own refresh cannot clear it. The SIGINT test covers that.
  • Settings::create_profile takes the lock, re-reads the configuration and checks the name again, and profiles create now uses it too. A concurrent change is kept, and a concurrent name is refused. Both cases are tested.
  • Taking the only organization or environment says so on stderr. A wallet is still always asked for, which keeps a keypress before a payment.

Nonblocking: in profiles(), the if let early returns followed by a _ => arm hide future ProfileCommand variants. One match with an explicit arm per variant, each taking the lock where it needs it, would let the compiler flag a new variant.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (130 unit, 119 integration) and python3 scripts/check-coverage.py passed. CRAP: 605 measured functions, none at or above 20, highest wait_for_payment at 19.60 and endpoint at 18.00 (from #74's flag); 173 functions were unmeasured. CI is green.

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.

2 participants