diff --git a/.claude/commands/new-package.md b/.claude/commands/new-package.md new file mode 100644 index 0000000..741077e --- /dev/null +++ b/.claude/commands/new-package.md @@ -0,0 +1,28 @@ +Scaffold a new internal domain package named `$ARGUMENTS`. + +## Steps + +1. Create `internal/$ARGUMENTS/$ARGUMENTS.go`: + - Package declaration `package $ARGUMENTS` + - One exported concrete type named after the package's responsibility (e.g. `Service`, `Store`, `Client`, `Parser`) + - Constructor: `func New(...) *` — only accept dependencies the type actually needs; leave the signature empty if none are obvious yet + - At least one exported method stub representing the package's primary operation; return `result.Expect[T]` if the operation can fail + - Doc comment on every exported symbol (the linter enforces this) + +2. Create `internal/$ARGUMENTS/$ARGUMENTS_test.go`: + - Package: `package $ARGUMENTS_test` (black-box) + - Declare a minimal interface covering only the methods the test calls + - Write a manual fake struct that satisfies that interface (no code generation) + - One table-driven test using `t.Run` and helpers from `gitea.djmil.dev/go/template/pkg/testutil` + - Use `logger.NewNop()` from `gitea.djmil.dev/go/template/pkg/logger` if logging is needed + +## Rules (do not break these) + +- Never define an interface inside the package itself — consumers define interfaces +- Never call `.Expect()`, `.Must()`, or `.Expectf()` inside the package — only return `result.Expect[T]` +- No third-party imports +- No hard-coded configuration values + +## After scaffolding + +Run `make lint test` to verify the files compile and the stub test passes. Report what was created and suggest what the caller should wire in `cmd/app/main.go`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d9be02e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(go test *)", + "Bash(go vet *)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 4c3c7c8..2db4ded 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,15 +8,12 @@ Keep it concise — the agent needs signal, not essays. ## Project overview Go 1.25 template for PoC, hobby projects, and small publishable packages. -Demonstrates: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes, -result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck), -git hooks, devcontainer, VSCode tasks. +Stack: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes, +result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck). Key constraint: `go.mod` stays free of dev tool deps (tools are pinned in `tools.versions` and run via `go run tool@version`) so packages published from this repo have a clean module graph for consumers. -Module: `gitea.djmil.dev/go/template` — update this when you fork. - --- ## Design philosophy @@ -39,11 +36,13 @@ Key factors that shape this codebase most directly: ## Project structure ``` -cmd/app/main.go composition root — wires deps, no logic here -internal/config/ flag-based config loader (config.Load) -internal/logger/ slog wrapper with WithField / WithFields -internal/greeter/ Example domain package (delete or repurpose) -pkg/result/ Example publishable package (Result/Expect types) +cmd/app/ CLI entrypoint: parses flags, wires dependencies, and expresses + what the program does in high-level readable steps — calls internal/ + and pkg/ packages; no domain logic lives here +internal/ domain-specific packages; logical grouping of substantial code into + digestible types and interfaces; not importable outside this module +pkg/ generic, publishable packages reusable across projects; no + assumptions about the calling application tools.versions Pinned tool versions (sourced by Makefile and pre-push hook) .golangci.yml Linter rules .githooks/pre-push Runs gofmt + go vet + golangci-lint + gosec before push @@ -53,8 +52,8 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push ## Project rules -- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...` -- **Packages** — keep `cmd/` thin (wiring only); business logic belongs in `internal/` +- **Module imports** — always use the full module path from `go.mod` (never relative imports) +- **Packages** — `cmd/` owns CLI parsing, dependency wiring, and high-level orchestration; domain logic belongs in `internal/` - **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free) - **Errors** — `pkg/result` is a convenience tool for removing error-threading clutter from application logic; use it as follows: - `pkg/` libraries **only return** `result.Expect[T]` — never call `.Expect()`, `.Must()`, or `.Expectf()` inside library code; those methods exit the goroutine via `runtime.Goexit` and are only safe in application-layer code protected by a boundary @@ -65,7 +64,7 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push - use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error - still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail` - **Logging** — logs go to `stderr` (structured, machine-readable, per 12-factor XI); human output goes to `stdout` via `fmt.Print*`. Use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages; `log/slog` is the backend -- **Config** — all configuration through `internal/config` (flag-parsed); no hard-coded values in logic packages +- **Config** — all configuration parsed in `cmd/app/config.go` (flags); no hard-coded values in `internal/` or `pkg/` packages --- @@ -77,7 +76,6 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push - Constructors: `New(deps...) *Type` pattern - Comment every exported symbol (golangci-lint will warn if missing) - Max line length: 120 chars (configured in `.golangci.yml`) -- Prefer explicit over clever; PoC code should be readable first --- @@ -91,7 +89,7 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push - Table-driven tests with `t.Run("description", ...)` for multiple cases - The race detector is enabled in CI (`make test-race`); don't introduce data races - Never use `time.Sleep` in tests; use channels or `t.Cleanup` -- Use `internal/testutil` helpers instead of manual checks — `ResultOk`, `ResultOkNotNil`, `ResultErr` for `result.Expect[T]`; `NoError`, `Error`, `ErrorContains`, `Equal` for plain values +- Use `gitea.djmil.dev/go/template/pkg/testutil` helpers instead of manual checks — `ResultOk`, `ResultOkNotNil`, `ResultErr` for `result.Expect[T]`; `NoError`, `Error`, `ErrorContains`, `Equal` for plain values --- @@ -111,9 +109,6 @@ make release # list releases, or tag+push after full checks (make release make clean # remove bin/ ``` -VSCode: `Ctrl+Shift+B` = build, `Ctrl+Shift+T` = test. -Debug: use launch config "Debug: app" (F5). - --- ## Adding new features (checklist) diff --git a/README.md b/README.md index ef176df..3bfd6c6 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,16 @@ make release # list tags, or run full checks then tag+push (make release V > **Keyboard shortcut (VSCode):** `Ctrl+Shift+B` → build, `Ctrl+Shift+T` → test. +### Claude Code commands + +If you use [Claude Code](https://claude.ai/code), the repo ships a custom slash command: + +| Command | What it does | +|---|---| +| `/new-package ` | Scaffolds `internal//` with a concrete type, constructor, doc comments, and a black-box test file — all wired to project conventions | + +Commands live in `.claude/commands/` and are available to anyone who clones the repo. + --- ## Project structure diff --git a/rename.sh b/rename.sh index c5089f0..2b37632 100755 --- a/rename.sh +++ b/rename.sh @@ -10,7 +10,7 @@ # **/*.go import paths # .devcontainer/devcontainer.json name field # README.md heading + module path references -# CLAUDE.md Module line +# CLAUDE.md internal import references (pkg/* preserved) # .golangci.yml goimports local-prefixes # git tags all template tags deleted # git history squashed into one INIT commit @@ -126,17 +126,17 @@ sedi() { fi } -# ── Helper: rename module path in a file, preserving pkg/result imports ─────── -# pkg/result is a standalone publishable package; its import path must not -# change when the consuming project is renamed. -RESULT_PKG="${OLD_MODULE}/pkg/result" -PLACEHOLDER="__RESULT_PKG_PLACEHOLDER__" +# ── Helper: rename module path in a file, preserving all pkg/* imports ──────── +# pkg/ packages are standalone publishable packages from this template repo; +# their import paths must not change when a consuming project is renamed. +PKG_BASE="${OLD_MODULE}/pkg/" +PLACEHOLDER="__TEMPLATE_PKG_BASE__" rename_module_in() { local file="$1" - sedi "s|${RESULT_PKG}|${PLACEHOLDER}|g" "$file" + sedi "s|${PKG_BASE}|${PLACEHOLDER}|g" "$file" sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$file" - sedi "s|${PLACEHOLDER}|${RESULT_PKG}|g" "$file" + sedi "s|${PLACEHOLDER}|${PKG_BASE}|g" "$file" } # ── Apply substitutions ───────────────────────────────────────────────────────