Developer: # Repository Guidelines
- Source Code: Located in
internal/(core packages:cmd/*,azdo,config,git,iostreams,docs, etc.). - CLI Entrypoints: Main binary at
cmd/azdo/azdo.go, documentation generator atcmd/gen-docs/. - Documentation: Generated Markdown in
docs/(output from Cobra command hierarchy). - Tests: Placed alongside production code as
*_test.go. - Vendoring: Dependencies locked in
vendor/.
- Build CLI:
make build(producesazdobinary). - Lint:
make lint(runs golangci-lint based on.golangci.yml). - Testing:
go test ./...(useTIMEOUT=...for custom timeout). - Documentation:
make docs(rebuildsdocs/viacmd/gen-docs). - Housekeeping:
make tidy(go mod tidy),make clean(removes binaries/distribution files). - Run Locally:
go run cmd/azdo/azdo.go --version(no installation required).
This project relies on the samber/cc-skills-golang skill packs for general Go conventions. The full skill catalog, error-rate impact per skill, and the recommended ⭐️ list are documented in the samber README. Install with the vercel-labs/skills CLI:
# Inspect which Go skills are currently installed
npx skills list | grep golang-
# Install a single skill
npx skills add samber/cc-skills-golang --skill golang-code-style -yWorkflow:
- Before writing or modifying Go code, run
npx skills list | grep golang-to see which skills are installed in this project or globally. The agent loads them automatically based on description matching; ifgolang-how-tois installed it also force-loads relevant secondary skills (e.g. Cobra review →golang-spf13-cobra+golang-cli+golang-error-handling). - Treat the installed samber skills as the source of truth for general Go rules (style, naming, error wrapping, nil safety, testing patterns, concurrency, context propagation, etc.).
- Apply the project-specific rules in the sections below only where they deviate from samber. Every such section declares
> Supersedes samber/cc-skills-golang@<skill> for this project.at the top — samber's ⚙️ override mechanism is honored automatically. - If a section below does not declare a supersession, the samber skills win on that topic.
When in doubt: trust the samber skill that triggers. Project-specific sections exist to add or restrict, not to re-teach.
Supersedes
samber/cc-skills-golang@golang-code-style,@golang-naming, and@golang-error-handlingfor this project.
For the general rules (gofmt, goimports, MixedCaps, Get-prefix, errors.Is/As, %w wrapping), defer to the installed samber skills. Project-specific deviations:
- Linting:
make lintrunsgolangci-lintagainst.golangci.yml. Always run before submission. - Logging: Use
zap.L()with structured messages. Do not introducelogorslogwithout approval. - Error wrapping: Always propagate with
fmt.Errorf("...: %w", err). - Variables: Variable names must never collide with any import or the name of a Go package.
- CLI Flags: Use kebab-case (e.g.,
--organization-url). - Multi-value flag sentinel: When supporting "remove all" semantics on list flags, reserve
*as the exclusive sentinel. Commands must reject combinations like--remove-label foo,*and treat a lone*as "remove every existing entry." - Editing tools: Modify files using git-aware patches (e.g.,
apply_patch). Do not use ad-hoc scripts (Python, sed) to edit tracked files — diffs must stay reviewable. - Indentation during drafts: Cosmetic indentation mismatches are acceptable while implementing changes. Final formatting is applied with
gofumptafter coding is complete, so focus on correctness first.
Supersedes
samber/cc-skills-golang@golang-testingand@golang-stretchr-testifyfor this project.
For the general patterns (table-driven, parallel, fuzzing, coverage, assert vs require), defer to the installed samber skills. Project-specific deviations:
- Frameworks:
testing+testify(requirefor fatal preconditions,assertfor non-fatal). - Hermetic: All tests must be hermetic and use mocks under
internal/mocks. Never call the Azure DevOps REST API directly in tests — generate or extend a mock instead. - REST API commands: For commands that touch Azure DevOps REST API endpoints (
internal/cmd), always add black-box tests informed by Azure DevOps REST API 7.1 documentation. - Imports: Review and understand structs and functions in
vendor/to maintain correct imports. - Coverage: Add tests for new features and edge cases (e.g., authentication, URL parsing, remote operations).
For a complete guide, refer to TESTING.md.
- Commits: Adhere to Conventional Commits specification (e.g.,
feat: ...,fix: ...,chore: ...,refactor: ...). - Pull Requests: Clearly describe changes, rationale, and how to reproduce/verify; reference linked issues.
- CI Checks: Run
make lintandgo test ./...before submission; updatedocs/if CLI changes.
- Secrets Management: Never commit secrets. Use
AZDO_TOKENfor headless runs;azdo auth loginuses OS keyring for token storage by default. - Default Organization: Set via
AZDO_ORGANIZATIONor config files under${AZDO_CONFIG_DIR:-~/.config/azdo}.
- If you propose a plan and the user approves it, you must follow it; any material deviation (approach, dependencies, SDK-vs-REST, output shape) requires you to stop and ask for approval before implementing.
- If a planned step is blocked by sandbox/network/tooling constraints, do not “work around” by changing the approach; pause and ask for approval (or for the user to perform required external steps) instead.
- If you need to create directories or delete/move files, do not do it yourself; stop and ask the user to perform the action, then continue once confirmed.
- Do not introduce placeholder/stub implementations (e.g., TODOs, empty helpers, or temporary
return nil) to “get unstuck”; if information is missing, stop and ask the user for guidance/approval. - Prefer MCP tools (GitHub MCP / msdocs / context7) over direct network fetches (
curl, etc.); if MCP cannot provide what’s needed, try network fetches or internet search. If this does not work either, ask the user to fetch/provide the information instead.
- Location & Structure: Place new command code in an appropriate subdirectory under
internal/cmd/<category>/<command>/, matching the CLI hierarchy. For example,azdo repo create→internal/cmd/repo/create/create.go. - Factory Function: Define a single factory function named
NewCmd(ctx util.CmdContext) *cobra.Command— do not prefix with category names (oldNewCmdRepoXstyle is deprecated). - Options Handling: Use an unexported
optsstruct to bind CLI flags. Keep parsing and validation inRunEminimal, delegating logic to a separaterunXfunction. - CmdContext Usage: Always use the injected
util.CmdContextto retrieveIOStreams, configuration (ctx.Config()), connection (ctx.ConnectionFactory()), and typed API clients viactx.ClientFactory(). - Vendored API: Access Azure DevOps endpoints via the vendored
azuredevops/v7client packages instead of raw HTTP calls. Build the appropriateArgsstructs and call the client method (e.g.,git.Client.CreateRepository).- CRITICAL: Verify Data Types: Before using API response data, always inspect the struct definitions in the
vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/directory. Mismatched types (e.g.,int64vsuint64) between the API struct and your command's structs will cause compilation errors.
- CRITICAL: Verify Data Types: Before using API response data, always inspect the struct definitions in the
- Output: Use
ctx.Printer(format)and the standard printer helpers to format table or JSON output. When not emitting JSON, preferctx.Printer("list")for clear tables and pick the most relevant columns for the scenario.
- Start the progress indicator immediately after successfully obtaining
IOStreamsso users see activity while you parse inputs or build clients. - Immediately
defer ios.StopProgressIndicator()to guarantee cleanup on every return path. - If you need to print to stdout/stderr before the deferred stop executes, call
ios.StopProgressIndicator()first to keep progress text from interleaving with user-visible output. - Testing: Create mocks for the relevant client interface methods under
internal/mocksand write hermetic, table-driven tests alongside the command.
When a required Azure DevOps client is not available in the vendored Go SDK:
- Confirm the absence by searching the upstream SDK using the GitHub MCP server (
https://github.com/microsoft/azure-devops-go-api/blob/dev/azuredevops/v7). - Extend
type ClientFactory interfaceininternal/azdo/connection.gowith the new client method signature. - Ask the user to run
go mod tidyfollowed bygo mod vendorafter the interface additions (the sandbox cannot do this automatically). - Add a matching mock generation entry to
scripts/generate_mocks.sh. - Let the user run
bash ./scripts/generate_mocks.sh - Implement the factory method in
internal/azdo/factory.goso the new client can be constructed via existing connection plumbing.
Do not hand-roll HTTP calls or add new internal/azdo/extensions methods as a shortcut when an SDK client can be introduced through this process; if the user-approved plan is to add an SDK client, follow the steps above or stop and ask for approval to change approach.
-
JSON and Table/Plain output are handled via separate code paths.
-
Use
util.AddJSONFlags(cmd, &opts.exporter, ...)inNewCmdto register JSON-related flags (--json,--jq,--template). This populates theopts.exporterfield when a user specifies one of those flags. The string slice you pass must list every JSON field you expose (matching the struct tag names) so users can filter output predictably. -
JSON Output Logic:
- In the command's
run...function, check ifopts.exporter != nil. - If true, this indicates the user wants JSON output.
- Define a dedicated view struct (or slice of structs) that represents the JSON surface you intend to support. Field names must match the strings you register with
util.AddJSONFlagsand every optional field should use a pointer type withjson:"...,omitempty"so unset values disappear from the payload. - Populate that view struct from the SDK model (write small helper functions when mapping requires normalization—e.g., formatting
azuredevops.Time, collapsing identities, adding derived counts). Avoid returning the raw SDK types directly; surface only the columns you are committed to supporting. - Call
opts.exporter.Write(ios, result)to serialize the struct and print it.
- In the command's
-
Table/Plain Output Logic:
- This is the default path, executed when
opts.exporter == nil. - For creating horizontal tables:
- Get a printer:
tp, err := ctx.Printer("list"). - Define all column headers:
tp.AddColumns("Header1", "Header2", ...). - Finalize the header row:
tp.EndRow(). - For each data row, add cell values in order:
tp.AddField(value1),tp.AddField(value2), etc. - Finalize the data row:
tp.EndRow(). - Render the table:
tp.Render().
- CRITICAL:
AddFieldpopulates a cell in a horizontal row corresponding to a column defined byAddColumns. It is not for creating vertical key-value lists (e.g.,Label: Value). Do not pass a label toAddField.
- Get a printer:
- For simple text output (e.g., a success message),
fmt.Fprintf(ios.Out, "...")is acceptable.
- This is the default path, executed when
-
Documentation:
- Add CLI help examples for using
--jsonand--format table. - Run
make docsto regenerate markdown so output options appear in generated documentation.
- Add CLI help examples for using
-
Parameter Derivation: Before defining CLI flags/args for a new command, analyze the corresponding method in the vendored Azure DevOps Go API under
vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/<service>/client.go(or related file). Review its function signature and the associatedArgsstruct:- Map required struct fields or parameters to required CLI flags or positional arguments.
- Map optional fields to optional CLI flags, providing sensible defaults from environment variables (
AZDO_*) or configuration where applicable. - Use repository conventions for flag naming (kebab-case) and argument positioning.
- Ensure every CLI parameter has clear help text and appears in regenerated documentation (
make docs).
-
Organization/Project Positional Argument Parsing:
- For commands that operate within a project, accept the target project and optional organization as the first positional argument in the form
[ORGANIZATION/]PROJECT. - Parse by splitting on
/:- One segment → project only; retrieve organization from default config.
- Two segments → first is organization, second is project.
- Any other segment count → return a flag/argument error.
- If organization is omitted and no default is configured, return an error stating that no organization was specified or configured.
- Follow patterns established in existing commands (e.g.,
internal/cmd/repo/list/list.go) for parsing and validation.
- For commands that operate within a project, accept the target project and optional organization as the first positional argument in the form
-
Pagination for List Operations:
- When using any Azure DevOps SDK
List*method that supports continuation tokens, loop until theContinuationTokenin the response is nil or empty. - Append results across all pages before applying filters or selections.
- Break the loop only after exhausting all pages, or if the command supports
--max-items [int>0]and the user specified a value stop after the max value
- When using any Azure DevOps SDK
-
Confirmation for Destructive Operations:
- Destructive commands must prompt the user for confirmation unless a
--yesflag is provided. - On cancellation, return
util.ErrCancel.
- Destructive commands must prompt the user for confirmation unless a
-
Nil Handling for API Fields:
- Explicitly check for nil pointer fields returned from API calls before use.
- Use
types.GetValuefor safe dereferencing or return an error if the value is required and missing.
-
Debug Logging:
- Use
zap.L().Debugto log critical decision points (e.g., parsing format detected, scope descriptor resolutions, API call stages).
- Use
-
Aliases:
- For common destructive commands, provide short command aliases such as
d,del,rmto improve ergonomics. - For list commands, provide aliases such as
l,ls - For commands that create objects provide aliases like
c,cr
- For common destructive commands, provide short command aliases such as
-
Working with Scope Descriptors:
- For project-scoped operations, you must first fetch the project object using the
core.Client. - Then, you must use the
graph.Client'sGetDescriptormethod with the project'sStorageKey(ID) to get the correct scope descriptor. - The
graph.Client'sGetDescriptorcan be used to get the storage descriptor for various objects like projects, users, groups etc.
- For project-scoped operations, you must first fetch the project object using the
-
Single-Level Addition (Leaf to Existing Group):
- Implement the new leaf command in the correct parent group’s subdirectory, using
NewCmd(ctx). - Import and register it in the parent’s
NewCmdviaparentCmd.AddCommand(child.NewCmd(ctx)).
- Implement the new leaf command in the correct parent group’s subdirectory, using
-
New Subgroup Under Existing Group:
- Create a new directory for the subgroup under the group’s folder.
- Implement
NewCmd(ctx)for the subgroup (acts as a parent for its own children). - Register the subgroup in the immediate parent’s
NewCmdwithAddCommand. - Add leaf commands under the subgroup and register them similarly.
-
New Top-Level Group:
- Implement the group’s
NewCmd(ctx)ininternal/cmd/<group>/<group>.go. - Add it to the root command in
internal/cmd/root/root.goviarootCmd.AddCommand(group.NewCmd(ctx)).
- Implement the group’s
-
Multi-Level Nesting:
- For deeper hierarchies (e.g.,
root → graph → viz → generate), ensure each parent in the chain callsAddCommand()for its direct children. - Create folders reflecting the hierarchy (
internal/cmd/graph/viz/generate/). - Provide a
NewCmdfunction for every level that initializes its Cobra command and wires its children.
- For deeper hierarchies (e.g.,
-
Documentation Update: After adding any new command at any level, run
make docsto regenerate CLI documentation so all new commands appear indocs/. -
Checklist: Before making changes, begin with a concise checklist (3-7 conceptual bullets) outlining intended actions. Skip if the change is trivial
-
Change Scope: Keep changes focused and consistent with existing structure and naming.
-
Patch Size: Favor small, isolated patches and update/add nearby tests where relevant.
-
Patch Size: Favor small, isolated patches and update/add nearby tests where relevant. If a planned step becomes blocked, stop and ask instead of substituting a different implementation strategy.
-
Validation: After code edits, validate results in 1-2 lines and proceed or self-correct if validation fails.
-
Code Edits: Explicitly state assumptions before edits, create or run minimal tests when possible, and produce ready-to-review diffs following the repository style.
-
Code Scope: When you create, change or fix tests you only work on tests. You don't change any other code. When required prompt the user to deviate from that instruction.
-
Context7: Always use context7 when I need code generation, setup or configuration steps, or library/API documentation. This means you should automatically use the Context7 MCP tools to resolve library id and get library docs without me having to explicitly ask.
-
JSON and Table/Plain output are handled via separate code paths.
-
Use
util.AddJSONFlags(cmd, &opts.exporter, ...)inNewCmdto register JSON-related flags (--json,--jq,--template). This populates theopts.exporterfield when a user specifies one of those flags, and the provided slice must enumerate every JSON field (matching the struct tags) that the command supports so consumers can filter reliably. -
JSON Output Logic:
- In the command's
run...function, check ifopts.exporter != nil. - If true, this indicates the user wants JSON output.
- Define a dedicated view struct (or slice of structs) with explicit
json:"..."tags and register the matching field names withutil.AddJSONFlags. Use pointer types plusomitemptyfor optional fields so unset data is omitted, and map values from the SDK into this view (formatting times, flattening identities, computing derived helpers). Do not expose raw SDK structs in the JSON response. - Call
opts.exporter.Write(ios, result)to serialize the struct and print it.
- In the command's
-
Table/Plain Output Logic:
- This is the default path, executed when
opts.exporter == nil. - For tabular data, get a printer with
tp, err := ctx.Printer("list"). Usetp.AddColumns(),tp.AddField(), andtp.EndRow()to build the table, then calltp.Render().
- This is the default path, executed when
Supersedes
samber/cc-skills-golang@golang-code-style,@golang-error-handling,@golang-safety, and@golang-structs-interfacesfor this project.
For the general Go rules (gofmt, goimports, error wrapping, nil safety, struct/interface design, embedding, pointer-vs-value receivers), defer to the installed samber skills. The following project-specific helpers and imports must be used in addition:
types.GetValue[T](internal/types): Safe pointer dereference. Required for Azure DevOps API fields that may be nil — do not use*ptrdirectly.types.MapSlice/types.MapSlicePtr(internal/types): Generic slice transforms that handle nil pointers. Use instead of manual mapping loops.util.AddJSONFlags(cmd, &opts.exporter, ...): Register--json/--jq/--templateon every command that emits structured output. The string slice you pass must list every JSON field you expose.util.ErrCancel: Return this — notutil.SilentExit— when the user cancels a confirmation prompt.util.CmdContext: Always retrieveIOStreams,Prompter,Config,ConnectionFactory, andClientFactoryvia this. Never reach for globals.
- Standard library:
fmt,strings,context - Third-party:
github.com/spf13/cobra,github.com/MakeNowJust/heredoc,go.uber.org/zap - Internal:
github.com/tmeckel/azdo-cli/internal/cmd/util,github.com/tmeckel/azdo-cli/internal/azdo,github.com/tmeckel/azdo-cli/internal/types