Reduce org ID friction: add chunk org list and auto-detect org ID in init - #462
Conversation
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 |
| // 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?
The getting-started guide walked new users through build-prompt and code review as its numbered path, with sidecars below it under a heading still marked '(preview)'. Someone following the steps in order never touched a sidecar. README framed the two as co-equal capabilities. Restructure so the sidecar loop is the primary path: - GETTING_STARTED: sidecar creation and the dev loop become Steps 3 and 4, skills become Step 5, and build-prompt/review move below the sidecar material as 'Team review context'. Drop the '(preview)' label. Group the detail sections under a 'Sidecar reference' heading. Lead the auth step with CircleCI, since that is what sidecars need. - README: drop the 'two main capabilities' framing, move context generation below the sidecar quick start. - AGENTS.md: describe sidecars as the primary capability. - SKILLS.md: lead the intro with sidecar setup and the dev loop, and point first-time readers at chunk-sidecar-setup. Also adds the chunk-sidecar-setup skill to the getting-started skills table, which listed only four of the five installed skills. Command-level details in 'Environment setup', 'Syncing', 'First-time sidecar setup', and 'Hook behavior' are left alone pending #438, #462, #491, and #501, which change those commands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…init `chunk sidecar` commands need an org ID, and previously the only way to get one was to find it in the CircleCI UI and set it by hand. This adds discovery and auto-detection so most users never have to. - `chunk org list` (with `--json`) lists the orgs the authenticated user belongs to, so the ID is discoverable from the CLI. - `chunk init` resolves the org ID as part of setup: auto-selects when the user belongs to exactly one org, shows a picker when interactive with several, and skips silently on auth failure, no TTY, or cancellation. `--skip-org-id` opts out. - Sidecar commands fall back to the same resolution path rather than failing with "no interactive terminal". - chunk-sidecar skill: org preflight now points at `chunk org list --json` instead of asking the user for an ID up front. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
a1f2caf to
e9a2f47
Compare
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