Skip to content

feat: add reusable dynamic shell completion #323

Description

@tmeckel

Dynamic Shell Completion

Goal

Add reusable Azure-backed positional completion for Cobra commands. First production integration: azdo repo list / ls / l.

Expected behavior:

  • azdo repo ls org:<TAB> returns project candidates such as org:ProjectA.
  • azdo repo ls my-repo<TAB> returns project candidates from the configured default organization.
  • Completion returns names only, case-insensitive prefix matches, sorted deterministically.
  • Completion uses Cobra's existing Bash, Zsh, Fish, and PowerShell generators; no custom shell scripts.
  • Completion failures, missing auth, rate limits, and timeouts return no candidates without blocking or printing diagnostics into the completion protocol.

repo list still executes with [ORG:]PROJECT and lists repositories for the selected project. Initial completion does not list repositories across all projects or change command arguments.

Decisions

  • Register handlers per command in NewCmd; no root registry.
  • Add generic scope-aware completion helpers so future commands can supply resource-specific candidate fetchers without reimplementing partial ORG: parsing.
  • No cache in v1: Cobra starts a fresh completion process per request; in-memory cache has little value and disk cache adds invalidation/staleness/security concerns.
  • Add persistent config keys:
    • completion_timeout: Go duration, default 800ms.
    • completion_limit: positive integer, default 100.
  • Invalid or missing settings fall back to defaults and never make completion fail.
  • Apply timeout to the completion fetch path and use cobra.ShellCompDirectiveNoFileComp for every result, including suppressed failures.
  • Do not start progress indicators or write logs/output to stdout during completion.

Architecture

Shared completion utility

Add internal/cmd/util/completion.go with:

  • A request type containing completion context, already-consumed positional args, resolved organization, typed prefix, and whether the organization was explicitly supplied.
  • A handler type returning raw candidate names and an error. Handlers own resource/API lookup; shared utility owns scope parsing, filtering, formatting, limits, timeout, and failure suppression.
  • RegisterProjectScopeCompletion(cmd, ctx, handler) assigning cmd.ValidArgsFunction for the first positional argument.
  • A reusable project handler backed by ctx.ClientFactory().Core(...) and azdo.GetProjects(...), which already handles continuation tokens.
  • Partial scope parsing separate from ParseProjectScope: recognize at most one ORG: prefix, allow an empty suffix for org:<TAB>, reject malformed colons/slashes, never call the full parser on incomplete input.
  • Default organization resolution only for unprefixed input. Bare org remains a project prefix under default organization; never interpreted as an organization for project-scoped completion.
  • Candidate formatting: explicit org returns org:Name; implicit org returns Name.
  • Case-insensitive prefix filtering, deterministic case-insensitive sorting, deduplication, then candidate cap from completion_limit.
  • Empty prefix returns up to configured limit of project names.
  • len(args) >= 1 returns no candidates (repo list accepts one positional scope).
  • Bounded completion context and a hard callback return bound where necessary; fetcher/API errors convert to empty results. No ShellCompDirectiveError or file fallback.
  • Debug-only structured logging if useful; never log tokens, project data beyond candidate metadata, or write to stdout.

Future commands reuse the same registration helper with project, pipeline, team, repository, or other resource handlers. Nested resource completion can use request args and add a specialized parser without changing existing command registrations.

Configuration

Update internal/config/config_options.go with completion_timeout and completion_limit so azdo config set/list flows expose them. Add shared parsing/default logic in internal/config or the completion utility, with positive-duration/positive-integer validation and fallback to defaults for invalid persisted values. Keep configuration interface changes minimal; no new auth or Azure client interface is needed.

Document settings in command/config help or generated docs where the repository's config conventions require it. No environment-variable settings in v1.

Command wiring

Update internal/cmd/repo/list/list.go to register project-scope completion after command flags are created. Existing aliases automatically share the callback. Keep execution parsing and API behavior unchanged.

Existing Cobra completion initialization in ExecuteC already supplies completion and __complete; no root wiring or shell generator implementation is required.

Tests (hermetic)

Add internal/cmd/util/completion_test.go covering:

  • Partial input: empty, my, org:, org:my, bare org, whitespace, malformed multiple colons, empty organization, slash/target input.
  • Explicit and default organization resolution.
  • Case-insensitive filtering, sorting, formatting, deduplication, and configured cap.
  • Empty results, handler error, missing default organization, invalid config values, and timeout cancellation.
  • Project handler with mocked Core client and multi-page GetProjects continuation responses.
  • No progress/log/stdout contamination.
  • A minimal Cobra root invoking __complete to assert candidate lines plus final :4 (NoFileComp) protocol marker.

Extend internal/cmd/repo/list/list_test.go with:

  • Registration assertion that ValidArgsFunction is non-nil.
  • Default-org completion returning bare project names.
  • Explicit org: completion returning org:Project candidates.
  • Prefix filtering and candidate cap through mocked Core client.
  • Failure path returning no candidates without invoking Git repository listing.

Add configuration tests for the new option keys/defaults and invalid-value fallback.

All tests remain hermetic. Reuse generated Core/client/config mocks; do not call Azure REST APIs.

Verification

Run focused tests first:

go test ./internal/config/... ./internal/cmd/util/... -run Completion -v
go test ./internal/cmd/repo/list/... -v

Run full validation:

go test ./...
make lint
make docs

Review generated documentation diff. Smoke-test all shell generators and protocol paths:

go run cmd/azdo/azdo.go completion bash
go run cmd/azdo/azdo.go completion zsh
go run cmd/azdo/azdo.go completion fish
go run cmd/azdo/azdo.go completion powershell

Use hermetic __complete tests for candidate output; manual unauthenticated invocation may correctly produce only :4.

After source changes, run graphify update . and verify completion relationships remain indexed. Inspect git diff for only completion/config/tests/docs changes and preserve unrelated worktree changes.

Likely Files

  • internal/cmd/util/completion.go — shared completion API, partial scope parsing, fetch/filter/timeout behavior.
  • internal/config/config_options.go — persistent completion settings.
  • internal/cmd/repo/list/list.go — initial production registration.
  • internal/azdo/loader.go — paginated project retrieval reuse.
  • internal/cmd/util/scope.go — canonical scope semantics to mirror without invoking on partial input.
  • internal/cmd/util/completion_test.go — reusable utility and Cobra protocol tests.
  • internal/cmd/repo/list/list_test.go — command integration tests.
  • internal/cmd/root/root.go and vendored Cobra completion sources — protocol/generator verification only; modify only if tests prove wiring requires it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Priority 1

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions