This file is for Claude Code and any other assistant working in this repository. CLAUDE.md
imports it.
ldo-go: the Go port of the Python ldo (github.com/libre-devops/python-helpers), a fast,
read-only CLI for Microsoft (Entra ID, Defender XDR, Intune, Azure, Graph, PIM, Logic Apps,
Automation), ServiceNow and Atlassian (Jira, Confluence), with helpers for Terraform modules.
Its users are people signing in as themselves; automation is second. It is an experiment in
how far a port gets: the Python ldo is the reference, and a difference from it is a bug
unless docs/differences.md says why.
Everything reads, apart from ldo-go az use, which switches the Azure CLI's account,
planner add-news --write and add-rollup --write, which raise and update Planner tasks,
terraform sort and docs, which change a module's own files (--check only reads), and
logicapp export, which writes files. Never add a command that changes a tenant or an
instance without being asked.
- Go 1.27 or later, at
~/.local/go/binon this machine; the justfile puts it on PATH. - Run everything through
just:just checkbefore calling anything done (gofmt, go vet, the tests with a coverage floor of 85%),just cifor staticcheck and govulncheck too,just fmtto format,just run <args>to try the CLI,just record-jsonafter a meant change to a command's JSON. - Dependencies: cobra, BurntSushi/toml, azidentity and azcore, MSAL for Go, x/term, x/net, x/sys and pkg/browser. Do not add one without asking; everything else is the standard library.
cmd/ldo-go/ main: signals, then cli.Execute
internal/core/ vendor-neutral: errs, config, brand, httpx, network and probe,
tokenstore, poll, inputs and sheets (CSV and Excel), rowfilters,
logging, yamltext and jsontext (Python's writers, byte for byte),
markdown, render, colour, sorting, textfiles, webbrowser
internal/microsoft/ the shared Microsoft layer: clouds, profiles, tokens, API clients;
identity and azcli are its helpers
internal/microsoft/*/ one package per API: entra, xdr, graph, azure, pim, ...
internal/microsoft/devices/ the one composite, using entra, xdr and intune
internal/servicenow/ the ServiceNow vendor, with instance/ on it
internal/atlassian/ the Atlassian vendor, with jira/ and confluence/ on it
internal/terraform/ modules as files: the HCL splitter and the tools, with sort/ and docs/
internal/cli/ the ldo-go command: parses, calls a client, renders. No logic here.
internal/fakes/ test fakes, one package per concern
internal/project/layering_test.go enforces it: core imports nothing of ours but core, a
feature imports only core and its vendor layer, cli sits on top, and fakes are for tests.
It also fails for a package with no tests. A new vendor goes in its vendors map.
- Every package has tests beside it. Nothing touches the network, a real
azor a real clock: HTTP goes throughfakes/httpfake,azthroughfakes/azfake, time throughfakes/clockfake, and each service has a fake of its own (graphfake,snowfake,atlassianfake,terraformfake, ...). A loopback server is fine where a real handshake is the point (core/probe). - CLI tests use the harness in
internal/cli/harness_test.go:newHarness, thenh.ok,h.fails(code, ...),contains, andusageErrorfor a usage message. It runs in UTC. internal/cli/json_output_test.gorecords every command's JSON shape intestdata/json-output.json; a change that is meant is recorded withjust record-json. A new command gets a case there.- Where the Python gives a golden output (KQL, YAML, detections, analyzer reports), the Go
test compares with it byte for byte, from
testdata/.
Never hard-code ldo-go, LDO_ or the repository: use internal/core/brand (brand.Command,
brand.EnvVar("X"), brand.Suggest("config init"), brand.Docs("page")). The config file is
the Python ldo's own (brand.ConfigDir); sign-ins are kept in a file of this tool's own.
- Write for the next person to read it: small functions, split into named steps; a doc comment on everything exported saying what it gives; a comment says why, where the code cannot. Prefer a plain loop to a clever expression. Match the surrounding code.
- A model reads its JSON through
core/fields(Text,Map,Objects,Strings,When,Number); an id that goes into a path throughutil.RequireGUID. Anything a person types that goes into a URL path or a query is validated first, with an anchored pattern. - Return the specific error from
core/errs:Inputffor something given that cannot be used,Configffor a profile that cannot do this,NotFoundf,Ambiguousf,Authf,Reauthf,APIf, withWithHintsaying what to do. Library code never exits or prints; only the CLI turns errors into messages and exit codes (0 fine, 1 error, 2 usage, 3 needs attention, 130 interrupted). - Data goes to stdout; notes, warnings and progress to stderr, through
render.Console(Note,Warn,Emit). Every data command takes-oand-pthroughCommon; a list also takes--sortand--unique. - All HTTP goes through
core/httpx, which followscore/network(the proxy,no_proxyand the certificates);azruns with the same environment. Never build anhttp.Clientof your own. - A token or secret is never printed (only with an explicit
--raw), never logged, and never accepted on the command line. A type holding one has aStringandGoStringthat leave it out. A proxy address can hold a password: show one only throughnetwork.Redact. - Go maps lose order: where a file is written back, or order is part of the output, use
yamltext.Ordered. - Python counts characters, Go bytes: a limit that Python applies to a string's length is a rune count here.
README.mdis a short front page: the command table, install, quickstart and links. Detail goes indocs/, one page per area, indexed indocs/README.md;docs/development.mdcovers building, the tests, CI and releasing.CHANGELOG.mdgets anUnreleasedentry for every change a user would notice.- The command pages follow the Python
ldo's, since the commands are the same: a change to a command there, taken here, changes its page here too.docs/differences.mdlists every way the two differ, and gets a line for each new one. internal/cli/docs_test.goruns everyldo-goexample in the docs with--help, andinternal/project/docs_test.gochecks everyjustexample is a recipe, every relative link and heading it names is there, and no file holds an em or en dash. Keep examples real.- UK English (colour, organisation, licence as a noun). Never use em or en dashes, in any file: use commas, colons, brackets or a plain hyphen.
- Do not commit, push, tag or release until the person asks. Use their own git identity.
- CI (
.github/workflows/ci.yml) runs gitleaks,just cion Linux, the tests on macOS and Windows, and builds the binaries. A release isjust release <version>onceCHANGELOG.mdhas the version's section:release.ymlthen runs CI as its gate and publishes the binaries it built, with checksums and build provenance. Never move or reuse a released tag. - No AI attribution in commits or pull requests: no
Co-Authored-Byor "Generated with" lines. - Never put a real tenant's names or ids in a test: use
web01,corp.exampleand the fakes' own ids.