Skip to content

Instantly share code, notes, and snippets.

@tinnet
Last active February 9, 2026 00:24
Show Gist options
  • Select an option

  • Save tinnet/5ad25cabb39e6219c0192d8b282512bb to your computer and use it in GitHub Desktop.

Select an option

Save tinnet/5ad25cabb39e6219c0192d8b282512bb to your computer and use it in GitHub Desktop.
CLI Template Spec — Language-Agnostic

CLI Template Spec — Language-Agnostic

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.

Goal

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.

What the Template Must Provide

1. Example CLI with Subcommands

The template ships a working CLI with:

  • A root command (mycli) that prints help/usage when invoked with no args
  • A --help flag showing all subcommands
  • A --version flag printing mycli 2025.06.15 (built 2025-06-15T12:00:00Z) (version from git tag, build timestamp)
  • One example subcommand (greet) with a --name flag that prints Hello, <name>! (defaults to World)

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.

2. Version Injection

The CLI's --version flag must show the version derived from git tags at build time.

  • CalVer format: vYYYY.MM.DD tags (e.g., v2025.06.15), with v prefix 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 shows dev as 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 describe in build script + properties file

3. mise Integration

Tool Management

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.

Task Runner

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.

4. Code Formatting

  • 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 lefthook to mise.toml tools if used

5. Linting / Static Analysis

Use the language's built-in or standard linter:

  • Rust: clippy
  • Go: go vet + staticcheck
  • Zig: compiler warnings
  • Java: Error Prone

6. Testing

  • Use the language's standard/built-in test framework
  • Include tests for the example subcommand that verify:
    • --help output contains expected text
    • --version output is present
    • greet with default name prints Hello, World!
    • greet --name Ada prints Hello, Ada!
  • Tests must be runnable via mise run test

7. CI (GitHub Actions)

CI Workflow (.github/workflows/ci.yml)

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 run mycli --help, mycli --version, mycli greet --name CI

Dependabot (.github/dependabot.yml)

Weekly updates for:

  • The language's package ecosystem (cargo, gomod, etc.)
  • github-actions

8. Release Workflow (.github/workflows/release.yml)

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 PowerShell Compress-Archive on 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 create directly or the softprops/action-gh-release action
  • 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

9. Project Metadata

  • 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/REPO backend)
    • 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

10. .gitignore

Cover at minimum:

  • Build output directories
  • IDE files (.idea, .vscode, *.swp, etc.)
  • OS files (.DS_Store, Thumbs.db)
  • .claude/
  • .env
  • Language-specific artifacts

11. Conventional Commits

All example commits in the template should use conventional commit format:

  • feat:, fix:, build:, ci:, docs:, refactor:, test:

What the Template Must NOT Include

  • 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 get is trivial)
  • No SNAPSHOT / pre-release versioning workflows

When to Ask the User

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: clap vs argh vs manual; cargo-dist vs cargo-release vs raw gh release
  • Go: cobra vs urfave/cli vs kong; goreleaser vs 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.

Language-Specific Notes

If the target language is Rust

  • Use Cargo workspace with a single crate
  • clap with derive macros for CLI
  • rustfmt for formatting (already built-in)
  • clippy for linting (already built-in)
  • Built-in #[test] for testing, consider assert_cmd for CLI integration tests
  • Cross-compile in CI with cross or cargo's built-in --target
  • Consider cargo-dist for releases, or just gh release + manual archive steps
  • Add a rust-toolchain.toml in addition to mise.toml

If the target language is Go

  • Use Go modules
  • cobra for CLI framework
  • gofmt/goimports for formatting (built-in)
  • go vet + staticcheck for linting
  • Built-in testing package, consider testify for assertions
  • Cross-compile with GOOS/GOARCH env vars (trivial, single runner)
  • goreleaser is the standard release tool for Go CLIs and handles everything (builds, archives, checksums, changelog)

If the target language is Zig

  • 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

GitHub Repository Setup

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

Shell Completions

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.

CI/Release Implementation Pitfalls

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 set fetch-depth: 0. Without it, GitHub Actions does a shallow clone and git describe --tags fails silently.
  • permissions: contents: write — The release workflow needs this at the top level, otherwise GITHUB_TOKEN can't create releases or upload assets.
  • Windows packagingzip and tar are not reliably available on Windows runners. Use PowerShell Compress-Archive for 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 the v prefix as an invalid version number. Strip it: ${REF_NAME#v}. Pass the tag as-is to Git, but strip the v before passing to version-sensitive tools.
  • macOS runnersmacos-13 (Intel) runners have been retired by GitHub. Use macos-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 to fail-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.

Local Releases and Forge Portability

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 release task (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:

  1. Debugging — iterate on the release pipeline locally in seconds instead of pushing tags and waiting for CI
  2. 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.

Success Criteria

After the agent finishes, I should be able to:

  1. mise install — toolchain ready
  2. mise run build — compiles and tests pass
  3. mise run fmt — formats code
  4. mise run lint — no warnings
  5. mise run pkg — native binary produced
  6. ./result-binary --version — shows dev version
  7. ./result-binary greet --name World — prints Hello, World!
  8. Push to GitHub, CI passes on all 3 OSes
  9. git tag v2025.06.15 && git push --tags — release workflow produces binaries
  10. mise use -g github:OWNER/REPO — installs the binary from the GitHub release
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment