| index dfa3b4c78..bab46d065 100644 |
| --- a/docs/superpowers/plans/2026-08-12-account-default-resolution.md |
| +++ b/docs/superpowers/plans/2026-08-12-account-default-resolution.md |
| @@ -2,7 +2,7 @@ |
| |
| > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| |
| -**Goal:** When `-account` is omitted in API mode, resolve it from config or the caller's memberships (one → use it; several → interactive chooser with a save-to-project prompt), and expose memberships via `GET /api/cli/accounts`, `carlos accounts list`, and `carlos auth whoami`. Closes #157. |
| +**Goal:** When `-account` is omitted in API mode, resolve it from config or the caller's memberships (one → use it; several → interactive chooser with a save-to-project prompt), and expose memberships via `GET /api/cli/apps` (already live), `carlos accounts list`, and `carlos auth whoami`. Closes #157. |
| |
| **Architecture:** One new token-gated console endpoint returning `Store.AccountsFor(addr)`. CLI-side, a single resolution seam on `apiSession` (`defaultAccount`) called through a `resolvedAccount(sess, raw, rawID)` wrapper that replaces `apiAccount(...)` at every API-mode callsite; flag defaults flip `"ops"` → `""` (direct mode already reads `""` as ops, so boxes are untouched). |
| |
| @@ -18,46 +18,14 @@ |
| |
| --- |
| |
| -### Task 1: Console endpoint `GET /api/cli/accounts` |
| +### Task 1: ~~Console endpoint~~ SUPERSEDED — endpoint already exists |
| |
| -**Files:** |
| -- Modify: `internal/console/authcli.go` (route next to `GET /auth/cli/whoami` at :213; handler next to `handleCLIWhoami` at :787) |
| -- Test: `internal/console/authcli_test.go` |
| - |
| -**Interfaces:** |
| -- Consumes: `s.cliUser(w, r)` gate, `s.store.AccountsFor(addr)` (`[]Account` with `.Name`/`.Sqid`, deleted excluded, creation order), `writeJSON`, `jsonError`. |
| -- Produces: `200 {"accounts":[{"name":…,"sqid":…}]}` — always an array, never null; `401` bare/wrong token (from `cliUser`). |
| - |
| -- [ ] **Step 1: failing test** — in `authcli_test.go`, mimic the whoami test at :152 (fixture + `getJSON`): signed-up user with a personal account gets `200` and their account's name+sqid; a second minted account (use the fixture's account-create path) appears in creation order; no/wrong token → 401. Assert the JSON shape exactly (`accounts` key, array of `{name,sqid}`). |
| -- [ ] **Step 2: run** — `go test ./internal/console/ -run CLIAccounts -count=1` → FAIL (404). |
| -- [ ] **Step 3: implement** |
| - |
| -```go |
| -s.mux.HandleFunc("GET /api/cli/accounts", s.handleCLIAccounts) |
| - |
| -// handleCLIAccounts answers `carlos accounts list` (signed-in mode) and the |
| -// omitted--account default: the caller's OWN live memberships, nothing else |
| -// — resolved from the token's address, so there is no enumeration surface. |
| -func (s *Server) handleCLIAccounts(w http.ResponseWriter, r *http.Request) { |
| - addr, ok := s.cliUser(w, r) |
| - if !ok { |
| - return |
| - } |
| - accts, err := s.store.AccountsFor(addr) |
| - if err != nil { |
| - log.Printf("console: cli accounts: %v", err) |
| - jsonError(w, http.StatusInternalServerError, "could not list accounts") |
| - return |
| - } |
| - out := make([]map[string]string, 0, len(accts)) |
| - for _, a := range accts { |
| - out = append(out, map[string]string{"name": a.Name, "sqid": a.Sqid}) |
| - } |
| - writeJSON(w, http.StatusOK, map[string]any{"accounts": out}) |
| -} |
| -``` |
| - |
| -- [ ] **Step 4: test passes**; **Step 5: commit** `console: GET /api/cli/accounts — the caller's own memberships (#157)`. |
| +`GET /api/cli/apps` (handleAPIApps, internal/console/authcli.go:219, live |
| +since main 5856dc5 2026-08-03, deployed both consoles) already returns |
| +`{"accounts":[{"sqid","name","role","apps"}]}` from `Store.AccountsFor`, |
| +leak-tested by TestAPIAppsListsMemberships. No console change, no console |
| +deploy. Task 4's `listAccounts` decodes this shape; the 404 fallback covers |
| +only pre-2026-08-03 consoles. |
| |
| ### Task 2: Config `account` key + lossless rewrites |
| |
| @@ -115,7 +83,7 @@ func resolvedAccount(sess *apiSession, raw, rawID *string) apiAcct |
| // it (verbs split modes before touching it). |
| ``` |
| |
| -- [ ] **Step 1: failing tests** — httptest console serving `/api/cli/accounts`: one account → Seg=sqid+Exact; several (non-TTY test env) → `severalAccountsErr` text; 404 → `{Seg:"ops"}`; explicit `-account`/`-account-id` never call the endpoint (assert via handler hit-counter); config account + matching console → its name, endpoint untouched; config console mismatch → falls through to memberships; expired-token 401 surfaces as an error, not a fallback. |
| +- [ ] **Step 1: failing tests** — httptest console serving `/api/cli/apps`: one account → Seg=sqid+Exact; several (non-TTY test env) → `severalAccountsErr` text; 404 → `{Seg:"ops"}`; explicit `-account`/`-account-id` never call the endpoint (assert via handler hit-counter); config account + matching console → its name, endpoint untouched; config console mismatch → falls through to memberships; expired-token 401 surfaces as an error, not a fallback. |
| - [ ] **Step 2: run** → FAIL. **Step 3: implement.** **Step 4: pass.** **Step 5: commit** `cli: resolve omitted -account from config or memberships`. |
| |
| ### Task 5: Flag-default flip + callsite sweep |
| @@ -136,7 +104,7 @@ func resolvedAccount(sess *apiSession, raw, rawID *string) apiAcct |
| **Files:** modify `auth.go` (`authWhoami` :702), test in `auth_e2e_test.go` or the httptest pattern used by whoami today. |
| |
| - [ ] **Step 1: failing test** — console serving both endpoints: output contains `accounts: tom@keymail.dev (bdk), acme (bce)`; console without the endpoint → whoami output unchanged, exit 0. |
| -- [ ] **Step 2:** after the identity print, `newConsoleClient(c.Console, c.Token).do(ctx, GET, "/api/cli/accounts", nil, &struct{ Accounts []apiAccountEntry }{…})`; on ANY error print nothing; else one muted line `accounts: n1 (s1), n2 (s2)`. |
| +- [ ] **Step 2:** after the identity print, `newConsoleClient(c.Console, c.Token).do(ctx, GET, "/api/cli/apps", nil, &struct{ Accounts []apiAccountEntry }{…})`; on ANY error print nothing; else one muted line `accounts: n1 (s1), n2 (s2)`. |
| - [ ] **Step 3: pass; commit** `cli: whoami prints account memberships`. |
| |
| ### Task 7: accounts list signed-in mode |
| @@ -144,7 +112,7 @@ func resolvedAccount(sess *apiSession, raw, rawID *string) apiAcct |
| **Files:** modify `accounts.go` (`runAccountsList` :204), test in `accounts` CLI test file (follow existing patterns; shell-outs use `carlosTestEnv` + scratch `cmd.Dir`). |
| |
| - [ ] **Step 1: failing test** — with no registry file at `-registry` and a credential for an httptest console: NAME/SQID table of the caller's accounts; with a registry file present: today's registry table (existing behavior test stays green); old console (404) → error naming the console as predating account listing. |
| -- [ ] **Step 2:** add `-console` flag; if `*consoleFlag == ""` and `os.Stat(*regPath)` succeeds → existing registry path verbatim; else `loggedInCredential(*consoleFlag)` (fatal on err), GET `/api/cli/accounts`, on 404 fatal `"%s does not support account listing — it predates this CLI"`, else `newTable` NAME/SQID. |
| +- [ ] **Step 2:** add `-console` flag; if `*consoleFlag == ""` and `os.Stat(*regPath)` succeeds → existing registry path verbatim; else `loggedInCredential(*consoleFlag)` (fatal on err), GET `/api/cli/apps`, on 404 fatal `"%s does not support account listing — it predates this CLI"`, else `newTable` NAME/SQID. |
| - [ ] **Step 3: pass; commit** `cli: accounts list works signed-in (#157)`. |
| |
| ### Task 8: Gate + PR |