Reduce org ID friction: add chunk org list and auto-detect org ID in init - #462
Reduce org ID friction: add chunk org list and auto-detect org ID in init#462schurchleycci wants to merge 4 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `chunk org list` command (unhides the org group, adds list subcommand with --json flag) - Auto-select org in orgPicker when user belongs to exactly one org - Improve ErrNoTTY message to suggest `chunk org list` + `chunk config set` - Update docs: chunk init now captures org ID; manual path uses `chunk org list` - Update chunk-sidecar skill to use `chunk org list` for org discovery instead of stopping to ask the user Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hanabel1
left a comment
There was a problem hiding this comment.
i think this could use some automated tests tho for (detectOrgID and orgPicker)
Good call, I'll add these before merging |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // interactive with multiple orgs. Skipped gracefully on auth failure, no TTY, | ||
| // or cancellation — none of these are fatal for chunk init. | ||
| func detectOrgID(ctx context.Context, rc config.ResolvedConfig, streams iostream.Streams, cfg *config.ProjectConfig) { | ||
| client, err := authprompt.ResolveCircleCIClient(rc, nil) |
There was a problem hiding this comment.
ResolveCircleCIClient returns ErrNeedsAuth for missing creds, but a genuine failure out of circleci.NewClient (bad base URL, etc.) gets swallowed here too. Gate the silent return on errors.Is(err, authprompt.ErrNeedsAuth) and warn otherwise?
| } | ||
| } | ||
|
|
||
| // Step 3: CircleCI org ID |
There was a problem hiding this comment.
Step numbering collides — there's already a // Step 3: Write hook config files further down.
|
|
||
| // Step 3: CircleCI org ID | ||
| if !skipOrgID && cfg.OrgID == "" { | ||
| rc, err := config.Resolve("", "", insecureStorage) |
There was a problem hiding this comment.
Step 2 does rc, _ := config.Resolve(...) and ignores the error; the identical failure here prints a warning. Resolve once above both steps and handle the error consistently?
| var jsonOut bool | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "list", |
There was a problem hiding this comment.
No coverage for org list — table output, --json, and the empty case are all untested. org_test.go only exercises create.
|
|
||
| client, err := ensureCircleCIClient(cmd.Context(), cmd, rc, io, tui.PromptHidden) | ||
| if err != nil { | ||
| return &userError{ |
There was a problem hiding this comment.
This throws away the userError ensureCircleCIClient already built (code auth.circleci_token_required, ExitAuthError) and relabels oauth/network failures as auth failures. Returning err unwrapped keeps the exit code — matters more now the skill branches on "error means not authenticated".
| } | ||
|
|
||
| if jsonOut { | ||
| enc := json.NewEncoder(io.Out) |
There was a problem hiding this comment.
Every other JSON path goes through iostream.PrintJSON (sidecar.go:180, config.go:72) — any reason to hand-roll the encoder here? Also a nil collabs encodes as null rather than [], which is awkward for anything parsing it.
| } | ||
|
|
||
| func TestResolveOrgID_ConfigOrgID(t *testing.T) { | ||
| t.Setenv(config.EnvCircleCIOrgID, "env-org") |
There was a problem hiding this comment.
Name says ConfigOrgID but this sets the env var — same path as TestResolveOrgID_FallsBackToPickOrg's sibling. The .chunk/config.json branch of config.ResolveOrgID is still uncovered.
| 3. If **both** are unset, run `chunk org list` to discover available orgs: | ||
| - **Single org returned** — use that ID automatically: `chunk config set orgID <id>`, then continue to Step 2. | ||
| - **Multiple orgs returned** — show the list to the user and ask which org to use **exactly once**. After they reply, run `chunk config set orgID <id>`, then continue to Step 2. | ||
| - **Error or empty list** — the user is not authenticated or has no orgs. Stop and ask them to run `chunk auth set circleci` first. |
There was a problem hiding this comment.
Empty list exits 0 with the warning on stderr, so the agent sees success with no rows rather than an error. Point this step at chunk org list --json so there's something deterministic to parse?
| if err != nil { | ||
| return | ||
| } | ||
| orgID, err := orgPicker(ctx, client)() |
There was a problem hiding this comment.
Why go via the collaborations picker at all here? Step 1 has already detected the VCS org/repo from the git remote, and GET /api/v3/orgs?filter[slug]=<provider>/<org> resolves a slug straight to the org UUID (public-api-service v3/api.go:118). circleci-cli does exactly this in internal/apiclient/org.go:54 (ResolveOrgID), as does circleci/mcp (client/pas/org.go GetOrgBySlug).
That would be deterministic rather than a guess: no picker, no TTY dependency, correct for multi-org users, and it picks the org that actually owns this repo instead of the first collaboration in the list. The empty-list case maps to "not on CircleCI" and can stay a soft skip.
internal/circleci/projects.go:59 GetProjectBySlug also already returns org_id and is currently unused — either route gets there without a picker.
There was a problem hiding this comment.
On the plumbing cost: v3 responses are JSON:API-enveloped ({"data": [...], "page": {...}}), and chunk-cli's only v3 usage today is the sidecar surface, which isn't. circleci-cli already has the generic for it — internal/apiclient/client.go:154:
type v3List[T any] struct {
Data []T `json:"data"`
Page struct {
Next *string `json:"next"`
Prev *string `json:"prev"`
} `json:"page"`
}plus v3Entity[T] for single-resource responses (client.go:150), and filterParam(key, val) → filter[key]=val (client.go:179). ResolveOrgID is then three lines on top of that.
Worth copying the same two generics + filterParam into internal/circleci rather than hand-unwrapping data at the call site — every future v3 endpoint needs them, and internal/httpcl already has QueryParam (request.go:66) so there's nothing else to build.
There was a problem hiding this comment.
Why go via the collaborations picker at all here? Step 1 has already detected the VCS org/repo from the git remote, and
GET /api/v3/orgs?filter[slug]=<provider>/<org>resolves a slug straight to the org UUID (public-api-servicev3/api.go:118).circleci-clidoes exactly this ininternal/apiclient/org.go:54(ResolveOrgID), as doescircleci/mcp(client/pas/org.goGetOrgBySlug).That would be deterministic rather than a guess: no picker, no TTY dependency, correct for multi-org users, and it picks the org that actually owns this repo instead of the first collaboration in the list. The empty-list case maps to "not on CircleCI" and can stay a soft skip.
internal/circleci/projects.go:59GetProjectBySlugalso already returnsorg_idand is currently unused — either route gets there without a picker.
Does that work for standalone orgs or just classic ones?
- detectOrgID: warn on unexpected errors from ResolveCircleCIClient, not just ErrNeedsAuth - init: resolve config.Resolve once above steps 2+3, handle error consistently; fix step numbering collision (Step 3 → Step 4 for hooks) - org list: return err unwrapped from ensureCircleCIClient to preserve ExitAuthError exit code; use iostream.PrintJSON; normalize nil collabs to [] before JSON encoding - auth: use providerCircleCI/providerAnthropic/providerGitHub constants in ValidArgs instead of repeating string literals (goconst lint fix) - tests: add org list table, --json, empty, and auth coverage; rename TestResolveOrgID_ConfigOrgID → EnvVar and add ProjectConfig branch test - SKILL.md: point org preflight at chunk org list --json for deterministic parsing; clarify empty-array condition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Sidecar commands need a CircleCI org ID, but there was no way to discover or capture it through the CLI — users either knew it already or got blocked. This PR closes that gap.
chunk initnow auto-detects the org ID after writing the project config. Single-org users get it picked silently; multi-org users see a picker. The ID is saved to.chunk/config.jsonso sidecar commands just work from that point on.For cases where init isn't an option,
chunk org listprovides a discovery command. TheorgPickererror message now points to it, and the sidecar skill uses it instead of stopping to ask the user.Test plan
chunk initwhile authenticated to one org — org ID auto-selected and written to.chunk/config.jsonchunk initwhile authenticated to multiple orgs — picker appears, selected ID writtenchunk initwhile not authenticated — completes without error, no org ID writtenchunk initin a non-interactive environment (no TTY) — completes without error or spurious warningchunk init --skip-org-id— org ID detection step skippedchunk org list— orgs printed in table form;--jsonreturns JSONchunk sidecar createwith single-org account and no orgID in config — org auto-selected, no picker🤖 Generated with Claude Code