Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`devctl` — Go CLI (cobra-based) for development productivity: listing source files by language, and a convention-based dependency-version system backed by plaintext files in `.versions/`.

## Commands

Build/test go through `make`, which drives itself using `devctl` (dogfooding — `bin/devctl list --go` discovers Go source files for its own build deps).

```shell
make build # builds bin/devctl
make test # ginkgo run, non-E2E specs only (uses .make/test as a stamp file)
make test_all # ginkgo run -r ./ , including E2E specs
Comment thread
UnstoppableMango marked this conversation as resolved.
make format # dprint fmt (README.md, .github/renovate.json, .vscode/extensions.json, .dprint.json)
make check # nix flake check --all-systems
make tidy # regenerates go.sum and gomod2nix.toml from go.mod
```

Run a single spec/package directly with ginkgo (installed as a go tool):

```shell
go tool ginkgo run --label-filter '!E2E' ./pkg/version/...
go tool ginkgo run -r --focus "RelPath" ./pkg/version/
```

E2E specs are tagged with the Ginkgo label `E2E` (see `test/e2e/`) and build the real binary via `gexec.Build` in `BeforeSuite` — they're excluded from `make test` (`TEST_FLAGS := --label-filter !E2E`) unless `CI` is set, but included in `make test_all`.

Tests use Ginkgo v2 + Gomega exclusively (no stdlib `testing.T` assertions). New test files/packages follow the Ginkgo bootstrap/generate convention:

```shell
cd pkg/foo && go tool ginkgo bootstrap # creates foo_suite_test.go
cd pkg/foo && go tool ginkgo generate bar # creates bar_test.go for bar.go
```
(Also wired as Makefile pattern rules: `%_suite_test.go` and `%_test.go`.)

No standalone lint command; formatting is via `dprint` (`.dprint.json`) for non-Go files, `gofmt`/`go vet` conventions for Go.

## Environment

Nix flake + `direnv` (`.envrc`) is the primary dev environment (`devShells.default` in `flake.nix` provides `go`, `gomod2nix`, `dprint`, `nixfmt`, etc). Building the Nix package itself runs `ginkgo run --label-filter=!E2E -r .` as its `checkPhase`.

`gomod2nix.toml` must stay in sync with `go.mod`/`go.sum` — regenerate with `go tool gomod2nix` after dependency changes (CI's `gomod2nix` job fails the PR if it drifts).

Go module tools are declared in the `tool (...)` block of `go.mod` (ginkgo, gomod2nix) and invoked via `go tool <name>` rather than being globally installed.

## Architecture

### Command layer (`cmd/`)
Cobra commands. `cmd/root.go` wires subcommands: `initialize`/`init` (`cmd/initialize/`), `config` (`cmd/config/`), `install`, `list`, `localbin`, `version`. Each subpackage under `cmd/` defines a package-level `Cmd`/`*Cmd` var plus a `New*()` constructor so commands are testable/composable independent of the global `root`.

Commands consistently resolve their working directory through `pkg/work` (either `work.Load(ctx)` — tries git root, then cwd — or an explicit `--chdir/-C` flag via `work.ChdirOptions`/`work.ChdirFlag`), rather than assuming cwd.

### `pkg/work` — the workspace abstraction
`work.Directory` is a string-typed path with `.Fs()` (returns an `afero.Fs` base-pathed at that directory) and `.Join()` helpers. Nearly every other package takes a `work.Directory` rather than a raw path, and filesystem access goes through the returned `afero.Fs` (enables afero's in-memory FS for tests, e.g. `work.WithRoot(afero.NewMemMapFs())`).

### `pkg/version` — the `.versions/` convention
Core convention: a repo's root `.versions/<name>` file holds a plaintext version string (optionally `v`-prefixed). `version.RelPath`, `version.Regex`, `version.Clean`/`Prefixed` are the low-level string helpers; `version.Cat`/`Init`/`WriteMakefile` do the file I/O and Makefile-snippet generation. `version.Source` is the interface for resolving a version from elsewhere (`version.String` is a literal; `version.GitHub` — via `github.com/unmango/aferox/github` — is stubbed/unimplemented (`panic("unimplemented")` for `Latest`/`Name`)). `version.GuessSource` picks a `Source` by pattern-matching the input (looks like a semver → `String`, contains "github" → `GitHub`).

### `pkg/config` — devctl's own config file
`devctl.yml` (or `.yaml`) in the workspace root, loaded via viper (`pkg/config.Viper`/`FromDirectory`/`Init`). Schema is just `Config{ Tools map[string]tool.Config }`.

### `pkg/tool` — tool installation
`tool.Config` (`url`, `version`, `script`) + `tool.Tool` describes an installable binary; `Tool.Install` downloads (`.tar.gz` auto-extracted via `Untar`) into a `work.Directory`'s `bin/`, chmod'ing it executable if it has no file extension.

### `pkg/list` — source file discovery
Language-aware source listing (`--go`, `--ts`, `--proto`, `--cs`, `--fs`, `--dotnet`, with `--exclude-tests`/`--absolute`) used both as a CLI feature and by the Makefile to compute Go build dependencies (`bin/devctl: $(shell $(DEVCTL) list --go --exclude-tests)`).

### `pkg/renovate` — generated schema
`pkg/renovate/zz_generated.schema.go` is generated from `.make/renovate-schema.json` (downloaded from docs.renovatebot.com, filtered by `hack/renovate/*.jq`, then run through `go-jsonschema`). Regenerate via `make pkg/renovate/zz_generated.schema.go`; don't hand-edit the `zz_generated` file. CI's `clean` job fails if this file is out of sync with its inputs.

## Conventions

- Command errors: use `github.com/unmango/go/cli.Fail(err)` in `Run` funcs (prints and exits non-zero) rather than returning errors from cobra's `RunE`.
- Logging: `github.com/charmbracelet/log`, with `ReportTimestamp: false`.
- External deps favor the `unmango/*` and `unmango/aferox/*` ecosystem (companion libraries by the same author) over reinventing filesystem/git/CLI helpers.
19 changes: 11 additions & 8 deletions gomod2nix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ schema = 3
[mod."github.com/google/go-github/v81"]
version = "v81.0.0"
hash = "sha256-+ufveUz5MphSn3KVU0v+5mlV0sPyeZC5g7jyR37kg0Q="
[mod."github.com/google/go-github/v84"]
version = "v84.0.0"
hash = "sha256-PCi2rQfQ4L2pmgGwOFSIW1u+exocIaq94K0OzuA++5I="
[mod."github.com/google/go-querystring"]
version = "v1.2.0"
hash = "sha256-F/Ve4oDaEqho8RryvdGSRR22/DbYHWZQa6M60n6oSYM="
Expand Down Expand Up @@ -92,11 +95,11 @@ schema = 3
version = "v1.7.0"
hash = "sha256-KSsBTMpggbVO4AmI0JaXbALrXeygr2sT+IuoVAfkNqw="
[mod."github.com/onsi/ginkgo/v2"]
version = "v2.27.5"
hash = "sha256-4YtMCQDR+odfRReIah0jHLXFdG8UyX+EOaDT00XpMr0="
version = "v2.28.1"
hash = "sha256-ndaovLQb6s4cztI8MXXJajJDY1kMsuz5zEbVjlfKgAE="
[mod."github.com/onsi/gomega"]
version = "v1.39.0"
hash = "sha256-in2eEUjcDC3JGDSAQxBI/mWdT0E2X1yCBKoGXemlWaE="
version = "v1.39.1"
hash = "sha256-ZlbQhUVwQBzmhBWCQ9iPWoJOVr+OqqIHB4iCNDFMEds="
[mod."github.com/pelletier/go-toml/v2"]
version = "v2.2.4"
hash = "sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q="
Expand Down Expand Up @@ -128,11 +131,11 @@ schema = 3
version = "v1.6.0"
hash = "sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A="
[mod."github.com/unmango/aferox"]
version = "v0.3.3"
hash = "sha256-pdIvZefa8Yl5zK1PFQYab+ExPHALTKgfKDZ5WLEcT3A="
version = "v0.5.0"
hash = "sha256-ugi7ILxoL3R6xjgl/8ruxzx+BAaux7IEJNnfae3FqeM="
[mod."github.com/unmango/aferox/github"]
version = "v0.0.4"
hash = "sha256-tz0jH4jLGR2S4rQPpHngzy9Ho/OAuQTBgqWmt9xR51s="
version = "v0.0.6"
hash = "sha256-8OinTfBi4FYya+TU6zUtwwhPBsw6Xwf7ueah0pSrCHM="
[mod."github.com/unmango/go"]
version = "v0.15.1"
hash = "sha256-iaw6AuhEYu7LdLQnOL6Zmu9kp41nRwaz+lpeZ5ihRww="
Expand Down
Loading