Skip to content

Add dynamic shell completions for flags and positional arguments #158

@tiulpin

Description

@tiulpin

Area

All command groups — cross-cutting UX improvement

Problem or use case

Cobra gives us basic command-name completion for free, but users still have to remember and type exact values for flags and positional arguments — project IDs, job IDs, agent names, pool names, status enums, etc. Major CLIs like gh and glab complete these dynamically, which dramatically reduces friction:

# Today: user has to know the exact job ID
teamcity run start Falcon_B<TAB>   # nothing happens

# Goal: complete from the server
teamcity run start Falcon_B<TAB>
Falcon_Build        Falcon_BuildDocker   Falcon_BuildNative

# Same for flags
teamcity run list --status <TAB>
success   failure   running   error   unknown

teamcity agent list --pool <TAB>
Default   Linux-Agents   Windows-Cloud

Without completions, users fall back to running teamcity project list or teamcity agent list in a separate terminal just to copy-paste an ID — that's the exact workflow completions eliminate.

Proposed solution

Implementation approach

  1. Create a shared completion helper (e.g. internal/completion/) that wraps API calls with a short timeout and returns []string + cobra.ShellCompDirective.
  2. Register completions via cobra.RegisterFlagCompletionFunc() for flags and ValidArgsFunction for positional args in each command's setup.
  3. Graceful degradation: if the server is unreachable or the token is missing, return cobra.ShellCompDirectiveNoFileComp (no error, just no suggestions).

Tier 1 — Static enums (no API call, works offline)

Command Flag / Arg Values
run list --status success, failure, running, error, unknown
job tree --only dependents, dependencies

These are the quickest wins — just cobra.FixedCompletions(...) or a static slice.

Tier 2 — Single API call (most common, highest value)

Command(s) Flag / Arg API source
run start <job-id> positional GetBuildTypes() → ID list
run list, queue list --job / -j GetBuildTypes() → ID list
run list --project / -p GetProjects() → ID list
run watch, run log, run download, run artifacts, run pin/unpin/tag/untag <run-id> positional GetBuilds() → recent IDs
queue remove, queue top, queue approve <run-id> positional GetBuildQueue() → queued IDs
agent view, agent enable/disable/authorize/reboot, agent jobs, agent move <agent> positional GetAgents() → ID + name
agent list --pool / -p GetAgentPools() → pool names
agent move <pool-id> positional GetAgentPools() → pool IDs
pool view, pool link/unlink <pool-id> positional GetAgentPools() → pool IDs
pool link/unlink <project-id> positional GetProjects() → project IDs
project view, project param * <project-id> positional GetProjects() → project IDs
job view, job pause/resume, job tree, job param * <job-id> positional GetBuildTypes() → job IDs

A small set of reusable completion functions covers all of these:

// Sketch — not prescriptive, just to illustrate scope
func CompleteJobIDs(f *cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
    return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
        client, err := f.Client()
        if err != nil {
            return nil, cobra.ShellCompDirectiveNoFileComp
        }
        ctx, cancel := context.WithTimeout(cmd.Context(), 2*time.Second)
        defer cancel()
        types, err := client.GetBuildTypes(ctx, api.BuildTypesOptions{})
        if err != nil {
            return nil, cobra.ShellCompDirectiveNoFileComp
        }
        var completions []string
        for _, bt := range types {
            completions = append(completions, fmt.Sprintf("%s\t%s", bt.ID, bt.Name))
        }
        return completions, cobra.ShellCompDirectiveNoFileComp
    }
}

Tier 3 — Context-dependent (harder, lower priority)

Command(s) Flag / Arg Depends on API source
project param get/set/delete <name> positional resolved <project-id> GetProjectParameters(projectID)
job param get/set/delete <name> positional resolved <job-id> GetBuildTypeParameters(jobID)
run download --path / -p resolved <run-id> GetArtifacts(buildID, "")

These require reading a previously-resolved positional arg (args[0]) before querying the API. Cobra supports this — the args slice in ValidArgsFunction contains already-completed arguments.

Scope explicitly excluded

  • Branch names: requires VCS API integration not currently exposed
  • Parameter values: too context-dependent, no universal source
  • --since / --until dates: free-form input, shell completion not helpful

Concrete examples of the end result

# Complete project IDs (with description after tab)
$ teamcity project view <TAB>
MyProject          My Project Name
Falcon             Falcon CI
InternalTools      Internal Tooling

# Complete agent by name or ID
$ teamcity agent enable <TAB>
1    Agent-Linux-01
2    Agent-Linux-02  
3    Agent-Windows-Cloud

# Complete pool for agent list filter
$ teamcity agent list --pool <TAB>
Default          Linux-Agents     Windows-Cloud

# Complete queued build IDs
$ teamcity queue remove <TAB>
98701    98702    98715

# Context-dependent: parameter names after project ID is resolved
$ teamcity project param get MyProject <TAB>
env.JAVA_HOME     env.BUILD_NUMBER    system.teamcity.version

# Static enum — works offline
$ teamcity run list --status <TAB>
success   failure   running   error   unknown

Implementation notes

  • Timeout: API calls during completion must be fast (2s hard cap). Users will not tolerate a laggy TAB.
  • Caching: Optional, but a short TTL cache (30-60s) for project/job lists would help for repeated completions in the same session.
  • \t descriptions: Cobra supports value\tdescription format — use it to show both ID and human-readable name (e.g. Falcon_Build\tFalcon CI Build).
  • No file completion: Most flags don't accept file paths. Return cobra.ShellCompDirectiveNoFileComp by default to avoid noise.
  • Testing: cobra.GenBashCompletionV2 / cobra.GenZshCompletionV2 can be tested via __complete hidden command — add unit tests that mock the API client and verify completion output.

Alternatives considered

  • Static completion scripts shipped with the binary: Would handle enums but not dynamic values. Not viable for IDs that vary per server.
  • Completion via alias expansion: Too fragile, no standard across shells.

Contribution

Happy to break this into smaller PRs per tier if that helps review. Tier 1 (static enums) could land independently as a quick win.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions