Feed this document to a coding agent along with the target language (e.g., "Rust", "Go", "Zig"). The agent should produce a GitHub template repository equivalent to java-native-cli-template, adapted idiomatically for the chosen language.
Note to the implementing agent: This spec was written in February 2026. The language-specific suggestions (libraries, tools, versions) reflect what was current at that time. Before blindly following them, verify that these are still the best choices. Check for newer/better alternatives, deprecated tools, or ecosystem shifts. The requirements (what the template must do) are fixed — the implementation choices (which libraries and tools to use) should reflect the best option available when you run.
A GitHub template repo that lets me click "Use this template", clone, and immediately start building a CLI tool. All the ceremony — formatting, linting, testing, CI, releases, distribution — is pre-configured. I just add commands.
The template ships a working CLI with:
- A root command (
mycli) that prints help/usage when invoked with no args - A
--helpflag showing all subcommands - A
--versionflag printingmycli 2025.06.15 (built 2025-06-15T12:00:00Z)(version from git tag, build timestamp) - One example subcommand (
greet) with a--nameflag that printsHello, <name>!(defaults toWorld)
Use the best CLI framework for the language:
- Rust:
clap(derive) - Go:
cobra - Zig: manual arg parsing or a lightweight lib
- Java:
picocli
The example subcommand exists to demonstrate the pattern. The README should explain how to add more.
The CLI's --version flag must show the version derived from git tags at build time.
- CalVer format:
vYYYY.MM.DDtags (e.g.,v2025.06.15), withvprefix stripped for the version number - Same-day releases:
v2025.06.15.1,v2025.06.15.2, etc. - No tag =
dev: local development without a tag showsdevas the version - The build timestamp should also be embedded
Use the language's idiomatic approach:
- Rust:
env!()or build script - Go:
-ldflags -X - Zig: build options
- Java:
git describein build script + properties file
A mise.toml at the repo root that installs all required toolchain components:
[tools]
# language-specific, e.g.:
rust = "latest"
# or
go = "latest"
Running mise install must be sufficient to set up the entire dev environment.
Provide these standard mise tasks (these names are consistent across all language templates):
| Task | Alias | Description |
|---|---|---|
mise run build |
Compile + run tests | |
mise run test |
Run tests only | |
mise run format |
fmt |
Auto-format code |
mise run lint |
Check formatting + lint | |
mise run package |
pkg |
Build release/native binary |
The underlying commands will differ per language, but the mise run interface is identical.
- Use the standard/canonical formatter for the language
- Rust:
rustfmt(built-in) - Go:
gofmt/goimports(built-in) - Zig:
zig fmt(built-in) - Java: Palantir Java Format via Spotless plugin
Auto-format enforcement:
- If the language's build/compile step does NOT auto-format, add a git pre-commit hook using lefthook that runs the formatter on staged files
- If the language's compiler or build tool already formats on build (none currently do, but hypothetically), the hook is unnecessary
- CI must also check formatting as a safety net
- Add
lefthooktomise.tomltools if used
Use the language's built-in or standard linter:
- Rust:
clippy - Go:
go vet+staticcheck - Zig: compiler warnings
- Java: Error Prone
- Use the language's standard/built-in test framework
- Include tests for the example subcommand that verify:
--helpoutput contains expected text--versionoutput is presentgreetwith default name printsHello, World!greet --name AdaprintsHello, Ada!
- Tests must be runnable via
mise run test
Triggers on push to main and pull requests:
- Build & test on a matrix:
ubuntu-latest,macos-latest,windows-latest - Check formatting (run the formatter in check mode)
- Lint (run the linter)
- Native smoke test on
ubuntu-latest: build the binary and runmycli --help,mycli --version,mycli greet --name CI
Weekly updates for:
- The language's package ecosystem (cargo, gomod, etc.)
github-actions
Triggers on tags matching v20* (CalVer tags).
Build matrix — produce native binaries for:
linux-x86_64(ubuntu-latest)macos-aarch64(macos-latest or macos-15)windows-x86_64(windows-latest)
For languages that support cross-compilation (Rust, Go, Zig): a single runner can build all targets. Use cross-compilation rather than a multi-runner matrix. This is faster and cheaper.
For languages that don't cross-compile (Java/GraalVM): use the multi-runner matrix as we did.
Packaging:
- Linux/macOS:
tar czf mycli-<target>.tar.gz mycli - Windows:
zip mycli-<target>.zip mycli.exe(use PowerShellCompress-Archiveon Windows runners, or a cross-platform approach)
Release creation:
- Use the simplest approach for the language
- If a release tool exists and adds value (like goreleaser for Go, cargo-dist for Rust), use it
- Otherwise, use
gh release createdirectly or thesoftprops/action-gh-releaseaction - The release must include:
- The platform-specific archives (tar.gz / zip)
- SHA256 checksums
- A changelog (conventional commits format, auto-generated)
Critical: the release assets must be compatible with mise's GitHub backend. This means:
- Archives contain a single binary (no nested directories)
- Archive names include platform identifiers that mise can auto-detect (e.g.,
linux,darwin/macos,windows,x86_64/amd64,aarch64/arm64) - The binary inside has a consistent name
Users should be able to install the resulting CLI with:
mise use -g github:OWNER/REPO
- License: MIT (include LICENSE file with empty copyright holder — it's a template)
- README.md covering:
- What this is (one-liner)
- Quick start (prerequisites via mise, build, run)
- Developer commands (both native and mise tasks)
- What's included (table of components)
- Customization checklist (what to rename/update after cloning the template)
- How to add a subcommand (code example)
- Versioning (CalVer format, how to tag)
- Releasing (what happens when you push a tag)
- Installing with mise (the
github:OWNER/REPObackend) - Project structure (tree listing)
- CLAUDE.md — agent steering file with:
- Project overview
- Tech stack summary
- Build commands
- Versioning scheme
- Code conventions
- Project structure
- How to add a subcommand
Cover at minimum:
- Build output directories
- IDE files (.idea, .vscode, *.swp, etc.)
- OS files (.DS_Store, Thumbs.db)
.claude/.env- Language-specific artifacts
All example commits in the template should use conventional commit format:
feat:,fix:,build:,ci:,docs:,refactor:,test:
- No Docker
- No cloud deployment
- No database
- No HTTP server
- No over-engineered abstractions
- No optional/commented-out libraries unless the language's package management has enough friction to justify it (e.g., Java's version catalogs benefit from pre-configured optional deps; Rust/Go do not since
cargo add/go getis trivial) - No SNAPSHOT / pre-release versioning workflows
Every language ecosystem has opinionated splits where reasonable people disagree. Don't just pick one — ask the user when there's a genuine choice with trade-offs. Examples:
- Java: Gradle vs Maven
- Rust:
clapvsarghvs manual;cargo-distvscargo-releasevs rawgh release - Go:
cobravsurfave/clivskong;goreleaservs manual - General: which linter config (strict vs relaxed), testing library choices
If there's a clear community default (e.g., rustfmt is THE Rust formatter), just use it. Only ask when there are two or more well-established alternatives with real trade-offs.
- Use Cargo workspace with a single crate
clapwith derive macros for CLIrustfmtfor formatting (already built-in)clippyfor linting (already built-in)- Built-in
#[test]for testing, considerassert_cmdfor CLI integration tests - Cross-compile in CI with
crossor cargo's built-in--target - Consider
cargo-distfor releases, or justgh release+ manual archive steps - Add a
rust-toolchain.tomlin addition tomise.toml
- Use Go modules
cobrafor CLI frameworkgofmt/goimportsfor formatting (built-in)go vet+staticcheckfor linting- Built-in
testingpackage, considertestifyfor assertions - Cross-compile with
GOOS/GOARCHenv vars (trivial, single runner) goreleaseris the standard release tool for Go CLIs and handles everything (builds, archives, checksums, changelog)
- Use the Zig build system
- Manual arg parsing or a community lib
zig fmt(built-in)- Compiler warnings for linting
- Built-in test framework
- Cross-compile with
-Dtarget(trivial, single runner) - Just use
gh release— no special release tooling needed
The repo must be configured as a GitHub template repository (Settings → General → Template repository checkbox). This is the whole point — users click "Use this template" to create their own CLI project.
Also set:
- A short, catchy description
- A few relevant topics (max 4) for discoverability
If the CLI framework supports generating shell completions (bash, zsh, fish) and it's easy to set up, include a section in the README explaining how. Don't over-engineer it — just document the command or flag that generates completions. Examples:
- Rust/clap:
mycli completions zsh > _mycli - Go/cobra:
mycli completion zsh > _mycli - Java/picocli:
picocli.AutoComplete
If the framework doesn't support it natively or it requires significant setup, skip it.
These are gotchas we hit building the Java template. They'll apply to most languages:
fetch-depth: 0— Any checkout step that needs git tag history (for version injection or changelogs) must setfetch-depth: 0. Without it, GitHub Actions does a shallow clone andgit describe --tagsfails silently.permissions: contents: write— The release workflow needs this at the top level, otherwiseGITHUB_TOKENcan't create releases or upload assets.- Windows packaging —
zipandtarare not reliably available on Windows runners. Use PowerShellCompress-Archivefor zip files on Windows, or use a cross-platform action. - v-prefix stripping — If using CalVer tags like
v2025.06.15, the release tool may reject thevprefix as an invalid version number. Strip it:${REF_NAME#v}. Pass the tag as-is to Git, but strip thevbefore passing to version-sensitive tools. - macOS runners —
macos-13(Intel) runners have been retired by GitHub. Usemacos-15(ARM) for aarch64 builds. Intel macOS builds may not be worth supporting — GraalVM Community dropped x86_64 macOS entirely for Java 25. - Matrix
fail-fast— GitHub Actions matrices default tofail-fast: true, meaning one failed job cancels all others. This is usually fine, but be aware during debugging: if one platform fails early, you won't see results from the others. - Validate release config locally before pushing — If using a release tool (goreleaser, cargo-dist, jreleaser), run its config validation / dry-run locally first. Don't iterate by pushing tags and waiting 5+ minutes per attempt.
For languages that support cross-compilation (Rust, Go, Zig), it should be possible to build all platform binaries and create a release from a local machine, not just from CI. This means:
- The packaging logic (tarball/zip creation, checksumming, naming) should live in a mise task or script, not be embedded solely in GitHub Actions workflow YAML
- A
mise run releasetask (or similar) should cross-compile, package, and optionally upload — so the developer can run the full release pipeline locally - The CI release workflow should ideally call the same script/task, keeping the workflow file thin (just checkout, install tools, run the task)
This has two benefits:
- Debugging — iterate on the release pipeline locally in seconds instead of pushing tags and waiting for CI
- Forge portability — the template can be adapted to Forgejo, Gitea, Codeberg, or any Git forge with minimal changes (just swap the CI workflow syntax, the build logic stays the same)
For languages that can't cross-compile (Java/GraalVM), this isn't achievable — the multi-runner matrix is unavoidable. But even then, keep forge-specific config (workflow files) as thin as possible.
After the agent finishes, I should be able to:
mise install— toolchain readymise run build— compiles and tests passmise run fmt— formats codemise run lint— no warningsmise run pkg— native binary produced./result-binary --version— showsdevversion./result-binary greet --name World— printsHello, World!- Push to GitHub, CI passes on all 3 OSes
git tag v2025.06.15 && git push --tags— release workflow produces binariesmise use -g github:OWNER/REPO— installs the binary from the GitHub release