agentleFS
buildosaurusbuildosaurus/scripteratops/skills/go-script/SKILL.md

go-script

Skill0 starsChanged 3 months agoMITView on GitHub

Write self-contained, portable, testable Go command-line tools — a small Go module with pure-logic/side-effect separation (boundaries as interfaces) that `go test` can drive with fakes, a cobra CLI, and a build/test/vet/gofmt gate. Use as soon as the user wants a Go script, CLI utility, automation tool, or "glue" code (orchestrating git, curl/http, file manipulation, parsing) in Go, even if they never say "module". Also triggers on adding tests to an existing Go tool, making one portable/testable, or porting a shell/Python script to Go.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every boundary takes a context.Context as its first parameter, and DoWork passes one down.

    line 79
  2. Very specific:

    Every tool ships *_test.go files in internal/core (same package, white-box).

    line 202
  3. Very specific:

    Use plain testing (t.Errorf) and reflect.DeepEqual for slices.

    line 209

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Go script

Standard for all Go command-line tools: a small Go module whose logic lives in a testable internal/core package, driven by a thin cobra CLI in main.go, verified by the compiler + go test + go vet + gofmt. The goal is a self-contained tool (one static binary, no runtime to install) whose logic is trivially testable — the Go counterpart of the python-script, swift-script, kotlin-script, and typescript-script skills.

Three decisions are already settled. Don't second-guess them, apply them:

  1. Distribution → a Go module compiled to a single static binary (go build) — Go's best-in-class story: no runtime, trivial cross-compile.
  2. Interface → cobra (github.com/spf13/cobra).
  3. Tests → go test (the built-in testing package), made easy by an architecture that isolates side effects behind interfaces.

This file carries the decisions; the code implementing them is a complete, gate-passing module in assets/template/. Copy and adapt that module — don't retype it from here. Then delete what your tool doesn't earn.

1. Module layout

tool/
├── go.mod                  # module path + the one dependency (cobra), pinned
├── go.sum                  # integrity-pinned (go mod tidy)
├── main.go                 # cobra wiring → thin entry point (package main)
├── internal/core/          # pure logic + boundaries → testable package
│   ├── errors.go           # typed errors, each with ExitCode()
│   ├── logic.go            # pure Transform
│   ├── boundaries.go       # CommandRunner / HTTPFetcher / EnvironmentReading
│   ├── config.go           # Config + precedence + JSON file read
│   ├── color.go            # ColorMode + ShouldColor (pure)
│   ├── log.go              # net-verbosity → level; stderr Logger
│   ├── runner.go           # os/exec subprocess boundary
│   ├── http.go             # net/http boundary
│   ├── work.go             # orchestration (DoWork)
│   ├── signals.go          # SIGTERM → 143, SIGINT → 130
│   └── *_test.go           # go tests, same package (white-box)
└── README.md
  • Give the module a path-style name (module example.com/tool), not a bare word — a bare module tool trips go mod tidy. Rename to your real path.
  • Logic lives in internal/core so main stays a thin wiring layer and the tests import the package directly. internal/ keeps it unexportable.
  • cobra is the only dependency. The stdlib gives subprocess (os/exec), HTTP (net/http), JSON (encoding/json), files, and env for free — reach for a dependency only for a real gain. Pin everything via go.mod/go.sum (go mod tidy records the exact versions and hashes).

2. Structure

The structure is designed to make tests trivial: pure logic never touches the outside world directly. Everything that leaves the process (subprocess, network, filesystem, environment) sits behind an interface boundary, injected into the orchestration. Tests pass a fake implementation; production passes the real one.

  • Boundaries are interfaces — CommandRunner, HTTPFetcher, EnvironmentReading. These are the single mock hooks. A fake is a tiny struct with one method.
  • Pure logic (Transform) — input → output, no I/O, tested by direct call.
  • Config is a struct with json tags and pointer fields (so "absent" is distinct from "empty"); precedence is one pure function, ResolveConfig.
  • Orchestration (DoWork) assembles boundary calls + pure logic: takes a context.Context first, data positional, behaviour flags last, returns (int, error). It takes the boundaries as interfaces, so a test drives it with no process and no socket.
  • main.go is thin: parse flags, build the real boundaries, call DoWork, map the error to an exit code. Nothing worth testing lives here.

2.1 Context threads the timeouts

Every boundary takes a context.Context as its first parameter, and DoWork passes one down. Subprocess and HTTP derive a context.WithTimeout from it, so a deadline (or cancellation) propagates uniformly — Go's idiom for bounding I/O.

2.2 Subprocess boundary

ProcessRunner routes all subprocess calls through exec.CommandContext:

  • Never a shell. Pass the program + an args slice — no sh -c, so no injection from a filename or argument.
  • Always a timeout. A context.WithTimeout kills the child on deadline; detect ctx.Err() == context.DeadlineExceeded and return TimeoutError (exit 124).
  • On non-zero exit, return CommandError{Command, Code, Stderr} (extract the code via errors.As on *exec.ExitError).

2.3 Config and precedence

Resolve settings with an explicit, documented precedence — higher wins:

CLI flags  >  environment variables  >  config file  >  built-in defaults

ResolveConfig is pure (coalesces the pointer layers so an absent value never clobbers a lower layer). Each source is read by one boundary — ReadConfigFile (JSON), ConfigFromEnvironment (through the EnvironmentReading hook). Secrets get no CLI flag (argv is world-readable via ps); a token comes from the environment or config file. Never log a raw Config — route it through Redacted(…). The template plumbs the token (precedence + redaction) but does not attach it to a request — the auth scheme is tool-specific. Wire it where your API needs it, e.g. in HTTPClient.Fetch: req.Header.Set("Authorization", "Bearer "+token) (thread config.Token in).

2.4 HTTP requests

HTTPClient wraps net/http in one boundary:

  • Set an explicit timeout (via the context).
  • Raise on non-2xx yourself. net/http does not error on 4xx/5xx, so check resp.StatusCode and return HTTPError; a 404 page would otherwise sail through as a "successful" body.
  • Inject a fake HTTPFetcher to test offline.

2.5 Colorized logging

Logging uses the template's own Logger, constructed once in main.go and writing to stderr (never stdout — stdout is for program data). Color:

  • ShouldColor(mode, isTTY, env) is a pure decision — precedence --color=always/never > NO_COLOR/FORCE_COLOR > TTY (and not TERM=dumb).
  • TTY detection in main.go uses the char-device bit on os.Stderr.Stat() — no dependency (avoids golang.org/x/term).

2.6 Robustness & signals

  • Bound every subprocess and HTTP call with a timeout (§2.2, §2.4).
  • Signals: a goroutine maps SIGTERM → os.Exit(143) and SIGINT → os.Exit(130) (signals.go). Go surfaces a closed downstream pipe as a write error, not a SIGPIPE kill — no handler needed.
  • Preflight external tools for glue scripts (a exec.LookPath-style check up front), returning ConfigError listing every missing tool.

2.7 Subcommands (if needed)

cobra handles this natively: a root *cobra.Command with root.AddCommand(sub), each its own *cobra.Command with a RunE. No manual dispatch.

2.8 Man page (optional)

cobra can emit man pages via spf13/cobra/doc, but the simplest path (no extra dep) is help2man from the built binary's --help:

help2man --no-info --name "tool" "./tool" > tool.1
sudo install -m 644 tool.1 /usr/local/share/man/man1/tool.1

2.9 Build & install (optional, for tools you distribute)

The README carries the environment set-up, not just the gates: a Prerequisites section with per-platform install of the Go toolchain (macOS/Linux/Windows) and the dependency list, pointing to go.mod + go.sum as the pinned source. For a distributed binary, an optional --dependencies flag printing the pinned versions is a nice touch — skip it for source-run tools.

  • Single binary: go build -o tool . → one static, self-contained executable; install it onto PATH.
  • Cross-compile: GOOS=linux GOARCH=arm64 go build -o tool . — Go's headline feature; no toolchain gymnastics.
  • Run from source: go run . <args> (no build step).

3. CLI contract

cobra gives most of the baseline for free:

  • -h/--help is automatic; --version from the command's Version field.
  • Verbosity: Flags().CountVarP(&verbose, "verbose", "v", …) counts occurrences (-vv = 2); same for quiet. Map the net count with LogLevelForVerbosity(verbose - quiet).
  • --color is a string flag defaulting to auto, parsed into ColorMode.
  • --dry-run short-circuits before output, announced at warning level.
  • Stream discipline: program data → stdout (fmt.Println); logs, progress, diagnostics → stderr (the Logger).
  • Grouped help caveat: unlike the other skills, cobra groups subcommands, not flags — --help lists all flags under one "Flags:" heading. Live with it, or set a custom usage template if grouping matters.

Exit codes

Map typed errors to a code in main: errors.As(err, &ec) where ec is an ExitCoder, then os.Exit(ec.ExitCode()).

CodeMeaning
0Success (also --help/--version)
1Runtime error (a tool error); cobra also uses 1 for usage errors
124A subprocess exceeded its timeout (TimeoutError)
130Interrupted — SIGINT / Ctrl-C (signals.go)
143Terminated — SIGTERM / kill (signals.go)

Set SilenceUsage/SilenceErrors on the root command and print the error yourself, so a runtime failure doesn't dump the usage text.

4. Tests with go test

Every tool ships *_test.go files in internal/core (same package, white-box).

  • Never test subprocess or network for real — inject a fake implementing the boundary interface. That's what the interface is for.
  • Fakes are tiny structs (type fakeRunner struct{…} with one method), not a mocking framework.
  • Pure logic and precedence are tested by direct call — no fakes at all.
  • Use plain testing (t.Errorf) and reflect.DeepEqual for slices; table maps for parameterized cases.

Coverage priority: pure logic and orchestration first, then config precedence, then arg parsing.

5. Quality gates

Four gates, all from the Go toolchain — no extra installs:

gofmt -l .      # formatting: empty output = clean
go vet ./...    # static checks
go build ./...  # compiles — the compiler IS the type gate
go test ./...   # tests
  • Indentation is tabs. gofmt is non-configurable — this is the one place Go overrides the 2-space house style; don't fight it. gofmt -w . fixes layout.
  • go vet is the baseline static analyzer; add staticcheck for a stricter pass if the tool earns it.

Summary of what to produce

Faced with a Go tool request, produce:

  1. A Go module copied from assets/template/: logic + boundaries in internal/core, a thin cobra main.go, a path-style module name, deps pinned by go mod tidy, the §2 structure, the §3 CLI contract and exit codes, and the robustness rules the tool's job earns (shelling out earns the timeout + preflight; the network earns the HTTP boundary + timeout; holding config/ secrets earns precedence + redaction). Delete what it doesn't earn.
  2. *_test.go covering pure logic, config precedence, orchestration (with fakes), and any boundary the tool ships.
  3. In one line: how to run it (go run . <args>), and that the four gates — gofmt + vet + build + test — are green.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.