feat(org): add commands to discover and manage organizations#297
Conversation
Connector commands accept `--organization <name>` and read an `identity.organization` default, but there was no way to discover which organizations the active account can use. Agents could see the flag yet had to already know the valid names, and `oo auth status` never listed organizations. Add an `oo org` command group backed by the existing org-control service: - `oo org list` fetches `GET /v1/me/organizations`, which already returns the full membership set (organizations you created appear with `role: "creator"`), so one request answers "what can I pass to `--organization`". It marks the entry matching the configured default. - `oo org current` reports the default identity offline. - `oo org use <name>` validates the name against the account's memberships before persisting `identity.organization`. - `oo org clear` returns connector commands to the personal identity. The group resolves an OOMOL account only and is rejected when just a self-hosted connector is configured. Telemetry records bucketed counts and a configured-default flag, never organization names. Closes #296 Signed-off-by: Kevin Cui <bh@bugs.cc>
Summary by CodeRabbit
WalkthroughThis PR adds a new Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as oo org use
participant Shared as listMemberOrganizations
participant API as org-control API
participant Settings as settingsStore
User->>CLI: oo org use <name>
CLI->>CLI: trim/validate name
CLI->>Shared: fetch accessible organizations
Shared->>API: GET /v1/me/organizations
API-->>Shared: organizations list
Shared-->>CLI: OrganizationView[]
alt name matches accessible organization
CLI->>Settings: setIdentityOrganization(name)
CLI-->>User: success message
else name not accessible
CLI-->>User: CliUserError (not accessible)
end
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/application/commands/org/shared.test.ts (1)
15-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing coverage for the
requestErrorpath.All three
CliUserErrorbranches inlistMemberOrganizationsare covered excepterrors.org.requestError(thrown when the fetcher itself throws, e.g. network failure). Consider adding a test wherefetcherrejects/throws to assert this branch and its message construction viagetUnexpectedRequestErrorMessage.As per coding guidelines, "Any modification must include sufficient tests."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/application/commands/org/shared.test.ts` around lines 15 - 108, `listMemberOrganizations` is missing test coverage for the `errors.org.requestError` branch when the fetcher throws before a response is returned. Add a new test in `shared.test.ts` that uses `createRequestContext` with a `fetcher` which rejects/throws, then assert `expectCliUserError` returns `errors.org.requestError` and that the message comes from `getUnexpectedRequestErrorMessage`. Keep the existing success and invalid-response tests unchanged while covering this unique error path.Source: Coding guidelines
src/application/commands/org/shared.ts (1)
72-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSwallowed parse error loses debugging context.
The bare
catchdiscards the originalJSON.parse/schema-validation error entirely before throwing the genericerrors.org.invalidResponse. Unlike therequestErrorpath (line 58-62), which forwardserror.messageviagetUnexpectedRequestErrorMessage, this path gives no way to diagnose malformed API responses in production logs.♻️ Suggested fix: log the underlying cause before rethrowing
try { return organizationsResponseSchema .parse(JSON.parse(rawResponse) as unknown) .organizations .map(toOrganizationView); } - catch { + catch (error) { + context.logger.warn({ error }, "Failed to parse organization membership response"); throw new CliUserError("errors.org.invalidResponse", 1); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/application/commands/org/shared.ts` around lines 72 - 80, The parsing branch in shared.ts swallows the underlying JSON.parse or organizationsResponseSchema validation error, so add handling in the try/catch around organizationsResponseSchema.parse and toOrganizationView to preserve debugging context. Catch the error as a symbol, log or surface its message/details before throwing CliUserError("errors.org.invalidResponse", 1), and keep the existing generic user-facing error while ensuring the original cause is available in logs similar to the requestError path.src/application/commands/org/list.ts (1)
90-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffExtract the ANSI-aware table renderer shared by list commands.
src/application/commands/org/list.tsandsrc/application/commands/connector/apps.tsboth build dimmed headers, measure cells withBun.stringWidth, and pad rows the same way. A small shared helper would remove the duplication and keep future list commands consistent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/application/commands/org/list.ts` around lines 90 - 156, The ANSI-aware table rendering logic in formatOrganizationsAsText is duplicated with the same header dimming, width calculation via visibleWidth/Bun.stringWidth, and row padding used by the connector apps list command. Extract this into a shared helper used by formatOrganizationsAsText and the corresponding renderer in connector/apps.ts, keeping createOrgListColumns and joinOrgListRow as the local entry points or adapting them to the shared formatter. Ensure the shared helper preserves ANSI-safe width handling and two-space gutter alignment so all list commands stay consistent.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/application/commands/org/list.ts`:
- Around line 90-156: The ANSI-aware table rendering logic in
formatOrganizationsAsText is duplicated with the same header dimming, width
calculation via visibleWidth/Bun.stringWidth, and row padding used by the
connector apps list command. Extract this into a shared helper used by
formatOrganizationsAsText and the corresponding renderer in connector/apps.ts,
keeping createOrgListColumns and joinOrgListRow as the local entry points or
adapting them to the shared formatter. Ensure the shared helper preserves
ANSI-safe width handling and two-space gutter alignment so all list commands
stay consistent.
In `@src/application/commands/org/shared.test.ts`:
- Around line 15-108: `listMemberOrganizations` is missing test coverage for the
`errors.org.requestError` branch when the fetcher throws before a response is
returned. Add a new test in `shared.test.ts` that uses `createRequestContext`
with a `fetcher` which rejects/throws, then assert `expectCliUserError` returns
`errors.org.requestError` and that the message comes from
`getUnexpectedRequestErrorMessage`. Keep the existing success and
invalid-response tests unchanged while covering this unique error path.
In `@src/application/commands/org/shared.ts`:
- Around line 72-80: The parsing branch in shared.ts swallows the underlying
JSON.parse or organizationsResponseSchema validation error, so add handling in
the try/catch around organizationsResponseSchema.parse and toOrganizationView to
preserve debugging context. Catch the error as a symbol, log or surface its
message/details before throwing CliUserError("errors.org.invalidResponse", 1),
and keep the existing generic user-facing error while ensuring the original
cause is available in logs similar to the requestError path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 652018b5-1cfd-4989-833e-1992f4ca4c78
⛔ Files ignored due to path filters (2)
src/application/bootstrap/__snapshots__/run-cli.test.ts.snapis excluded by!**/*.snapsrc/application/commands/config/__snapshots__/index.cli.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (14)
docs/commands.mddocs/commands.zh-CN.mdsrc/application/commands/catalog.tssrc/application/commands/org/clear.tssrc/application/commands/org/current.tssrc/application/commands/org/index.cli.test.tssrc/application/commands/org/index.tssrc/application/commands/org/list.test.tssrc/application/commands/org/list.tssrc/application/commands/org/shared.test.tssrc/application/commands/org/shared.tssrc/application/commands/org/use.tssrc/application/commands/telemetry-decisions.test.tssrc/i18n/catalog.ts
Connector commands (
oo connector run,oo connector proxy,oo connector apps) accept--organization <name>and read anidentity.organizationdefault, but nothing let you discover which organizations the active account can actually use — agents saw the flag yet had to already know the valid names, andoo auth statusnever listed organizations (#296).This adds an
oo orgcommand group backed by the existing org-control service.oo org listcallsGET /v1/me/organizations, which already returns the account's full membership set — organizations you created come back withrole: "creator"— so a single request answers "what can I pass to--organization", with no backend change required. The listing marks the entry matching the configured default, so you can see which identity connector commands use by default.oo org list [--json]— list accessible organizations (name,id,role,current); text output is a color-coded aligned table.oo org current [--json]— show the default organization identity, offline.oo org use <name>— validate the name against the account's memberships, then persistidentity.organization.oo org clear— return connector commands to the personal identity, offline.The group resolves an OOMOL account only (honoring the
OO_API_KEYoverride) and is rejected when just a self-hosted connector is configured, consistent with other non-connector commands. Telemetry records bucketed counts and a configured-default flag, never organization names. Covered by unit tests for the request and formatting layers plus CLI tests for each subcommand, including the no-account and inaccessible-name paths.Closes #296