feat(install): show the whole team in the environment picker - #232
Conversation
Greptile SummaryThe PR expands the install environment picker with the authenticated team's environment catalog and disabled rows for environments lacking local API keys. It also moves selection into installer startup, adds bounded discovery, and extends the dashboard request and UI abstractions with cancellation and disabled-option support.
Confidence Score: 4/5The PR is not yet safe to merge because an already-aborted discovery signal can still start a dashboard request that keeps the CLI alive until its transport timeout. The new cancellation bridge listens only for future abort events, while team discovery performs asynchronous imports before request initialization; if the three-second deadline expires during those imports, the later dashboard request misses cancellation and can retain its socket and timeout. Files Needing Attention: src/lib/dashboard-graphql.ts and src/lib/resolve-install-credentials.ts Important Files Changed
|
|
0d4f5aa repositions the picker per review: it now renders after the AuthKit brand mark and before the machine starts (branch prompt, detection), instead of pre-banner. Flow reads: banner → pick environment → install steps. Bonus: the machine's auth check now runs after the pick, so choosing a profile that needs a login triggers the in-flow device auth instead of relying on pre-flight state. handleInstall also learned to rethrow CliExit so the picker's cancel stays a clean exit 2. |
Two review findings on the team-wide picker: - The 'Your team has N environments' note totaled candidates + unavailable, counting keyed profiles from another account or team as team members. The team total now comes from the fetched catalog itself; foreign profiles stay selectable but are no longer counted. - Best-effort discovery awaited the refresh client's 30s timeout (and the team fetch after it) before showing even the local-only picker or the single-profile silent path. The whole enrichment is now bounded at 3s and degrades to the local-only picker past that. Addresses PR #232 review.
0d4f5aa to
d30e80b
Compare
757047f to
c9cc399
Compare
Two review findings on the team-wide picker: - The 'Your team has N environments' note totaled candidates + unavailable, counting keyed profiles from another account or team as team members. The team total now comes from the fetched catalog itself; foreign profiles stay selectable but are no longer counted. - Best-effort discovery awaited the refresh client's 30s timeout (and the team fetch after it) before showing even the local-only picker or the single-profile silent path. The whole enrichment is now bounded at 3s and degrades to the local-only picker past that. Addresses PR #232 review.
d30e80b to
3f4442c
Compare
Two review findings on the team-wide picker: - The 'Your team has N environments' note totaled candidates + unavailable, counting keyed profiles from another account or team as team members. The team total now comes from the fetched catalog itself; foreign profiles stay selectable but are no longer counted. - Best-effort discovery awaited the refresh client's 30s timeout (and the team fetch after it) before showing even the local-only picker or the single-profile silent path. The whole enrichment is now bounded at 3s and degrades to the local-only picker past that. Addresses PR #232 review.
2290bf7 to
fbf0d9e
Compare
3f4442c to
cc68ded
Compare
The 3s discovery bound abandoned the losing promise without cancelling it, and a refresh outliving the picker can answer invalid_grant after the installer's auth flow writes a new session -- and invalid_grant clears the credential store. The picker now only uses a currently-valid stored token; an expired session degrades to the local-only picker and the machine's own auth check re-authenticates later. The bounded race now wraps only the read-only team fetch. Addresses PR #232 review.
Two review findings on the team-wide picker: - The 'Your team has N environments' note totaled candidates + unavailable, counting keyed profiles from another account or team as team members. The team total now comes from the fetched catalog itself; foreign profiles stay selectable but are no longer counted. - Best-effort discovery awaited the refresh client's 30s timeout (and the team fetch after it) before showing even the local-only picker or the single-profile silent path. The whole enrichment is now bounded at 3s and degrades to the local-only picker past that. Addresses PR #232 review.
The 3s discovery bound abandoned the losing promise without cancelling it, and a refresh outliving the picker can answer invalid_grant after the installer's auth flow writes a new session -- and invalid_grant clears the credential store. The picker now only uses a currently-valid stored token; an expired session degrades to the local-only picker and the machine's own auth check re-authenticates later. The bounded race now wraps only the read-only team fetch. Addresses PR #232 review.
fbf0d9e to
83ef2d1
Compare
39dc78c to
1e481c2
Compare
Two review findings on the team-wide picker: - The 'Your team has N environments' note totaled candidates + unavailable, counting keyed profiles from another account or team as team members. The team total now comes from the fetched catalog itself; foreign profiles stay selectable but are no longer counted. - Best-effort discovery awaited the refresh client's 30s timeout (and the team fetch after it) before showing even the local-only picker or the single-profile silent path. The whole enrichment is now bounded at 3s and degrades to the local-only picker past that. Addresses PR #232 review.
The 3s discovery bound abandoned the losing promise without cancelling it, and a refresh outliving the picker can answer invalid_grant after the installer's auth flow writes a new session -- and invalid_grant clears the credential store. The picker now only uses a currently-valid stored token; an expired session degrades to the local-only picker and the machine's own auth check re-authenticates later. The bounded race now wraps only the read-only team fetch. Addresses PR #232 review.
83ef2d1 to
aba6f16
Compare
1e481c2 to
913d2cc
Compare
A lost race returned the local-only picker but left the dashboard request running: its socket and 30s abort timer keep the event loop alive, so a quickly finished (or cancelled) install lingered until the transport timeout. The timeout now aborts an AbortController threaded through fetchTeamEnvironments into the dashboard transport, which merges caller signals into its own controller. The hung-fetch test asserts the signal fires. Addresses PR #232 review.
The picker offered only profiles this machine knows about, while the
dashboard shows the team's full environment list — installs felt like
they were choosing from a different world than the dashboard.
The picker now merges in the team's environments (best-effort, via the
existing session; offline/logged-out degrades to the local-only list).
Environments without a local API key render as disabled rows naming the
gap, and the intro note carries the recipe:
Your team has 21 environments; 5 are ready on this machine — pick ...
○ rows need an API key here first: workos profile add <name> <key> --client-id <id>
❯ cli-branding-smoke > Staging staging-3 · Sandbox ● active
Nick's Team's Project > test12 staging · Sandbox
Nick's Team's Project > Production ○ no API key on this machine
They stay disabled because they must: the dashboard catalog has no
operation that creates or reveals an sk_ secret, so the CLI cannot make
an arbitrary environment installable on its own. Also prompts now when a
single keyed profile coexists with other visible team environments
(previously silent), and ui.select learns a disabled option flag.
The picker ran in bin.ts pre-flight, so it rendered before the AuthKit brand mark — the flow read: question, THEN banner. It now runs in runWithCore between adapter.start() (banner) and actor.start() (machine), so the flow reads: banner → choose environment → install steps (branch prompt et al). Side benefits of pre-machine placement: - the machine's own auth check runs AFTER the pick, so switching to a profile that still needs a login is handled by the in-flow device auth - explicit credentials (--api-key / WORKOS_API_KEY) and headless runs skip the picker at the call site; the helper keeps its own guards handleInstall now rethrows CliExit so the picker's cancel (exit 2) surfaces as a clean cancel instead of being masked as installer_error. Picker specs retarget the exported helper directly.
Two review findings on the team-wide picker: - The 'Your team has N environments' note totaled candidates + unavailable, counting keyed profiles from another account or team as team members. The team total now comes from the fetched catalog itself; foreign profiles stay selectable but are no longer counted. - Best-effort discovery awaited the refresh client's 30s timeout (and the team fetch after it) before showing even the local-only picker or the single-profile silent path. The whole enrichment is now bounded at 3s and degrades to the local-only picker past that. Addresses PR #232 review.
The 3s discovery bound abandoned the losing promise without cancelling it, and a refresh outliving the picker can answer invalid_grant after the installer's auth flow writes a new session -- and invalid_grant clears the credential store. The picker now only uses a currently-valid stored token; an expired session degrades to the local-only picker and the machine's own auth check re-authenticates later. The bounded race now wraps only the read-only team fetch. Addresses PR #232 review.
A lost race returned the local-only picker but left the dashboard request running: its socket and 30s abort timer keep the event loop alive, so a quickly finished (or cancelled) install lingered until the transport timeout. The timeout now aborts an AbortController threaded through fetchTeamEnvironments into the dashboard transport, which merges caller signals into its own controller. The hung-fetch test asserts the signal fires. Addresses PR #232 review.
a52bbf4 to
22db109
Compare
| const url = `${getWorkOSApiUrl()}/graphql`; | ||
| const controller = new AbortController(); | ||
| const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); | ||
| options.signal?.addEventListener('abort', () => controller.abort(), { once: true }); |
There was a problem hiding this comment.
Already-aborted signal is ignored
When the discovery deadline expires while the preceding dynamic imports are still pending, sendDashboardRequest receives an already-aborted signal but subscribes only to future abort events. The dashboard fetch therefore starts with a live controller and can retain its socket and timeout for thirty seconds, causing a cancelled or quickly completed install to linger.
| options.signal?.addEventListener('abort', () => controller.abort(), { once: true }); | |
| if (options.signal?.aborted) { | |
| controller.abort(); | |
| } else { | |
| options.signal?.addEventListener('abort', () => controller.abort(), { once: true }); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/dashboard-graphql.ts
Line: 105
Comment:
**Already-aborted signal is ignored**
When the discovery deadline expires while the preceding dynamic imports are still pending, `sendDashboardRequest` receives an already-aborted signal but subscribes only to future abort events. The dashboard fetch therefore starts with a live controller and can retain its socket and timeout for thirty seconds, causing a cancelled or quickly completed install to linger.
```suggestion
if (options.signal?.aborted) {
controller.abort();
} else {
options.signal?.addEventListener('abort', () => controller.abort(), { once: true });
}
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Stack: #229 → #230 → #231 → this. Retarget as bases merge.
Why
The install picker listed only the profiles this machine already knows about, while the dashboard shows the team's full environment list. Choosing from a visibly different list than the dashboard is exactly the disorientation this stack set out to fix.
What
The picker now merges in the team's environments (fetched best-effort with the existing session via
fetchTeamEnvironments; offline or logged-out degrades to the local-only picker):sk_secret (verified exhaustively — onlyapiKeysmetadata,deleteApiKey,expireApiKey), so the CLI cannot make them installable on its own. Showing them with the enable recipe is the honest middleui.selectgains adisabledoption flag (plumbed to inquirer)Verification
2720 tests passing (3 new: disabled-row shape + note recipe, single-profile-plus-team prompting, discovery-failure degradation), typecheck + oxfmt clean.