go
The go spell wires the Go toolchain into a magusfile: each op forks a go (or gofmt) subcommand directly, with no shell. Lint and vulnerability scanning run as go tool invocations so they resolve from the module's tool block rather than PATH.
Runtime name: go (source spells/golang/)
Version probe (go): go version
Version probe (golangci-lint): golangci-lint --version
Passing arguments to ops
Every op is invoked as go["<op>"](ctx, opts?). The first argument is the target's context, which is what carries the execution environment; the optional options map shapes the command itself:
| Key | Type | Description | Source |
|---|---|---|---|
args |
[str] |
Extra arguments appended to the resolved command, replacing any trailing defaults the op declares (go-test's ./...), so passing args also states the scope. Omit it and a bare go["<op>"]() keeps the defaults and forwards magus run <target> -- <extra> to the tool automatically; pass it to set the arguments explicitly, which replaces that passthrough. To keep the passthrough too, append the target's own args parameter: {"args": ["-race", "./..."] + args}. |
source |
stdin |
str |
Data written to the command's standard input. | source |
Working directory and environment are NOT options: they ride the context, as go["<op>"](ctx.withCwd("sub")) and go["<op>"](ctx.withEnv({"CGO_ENABLED": "0"})). Only the context reaches the cache key, so an option-table cwd or env would change what the tool did while the key said otherwise; passing either as an option is an error.
Charms (the :charm suffix, e.g. magus run test:rw) are orthogonal: they patch the base argv, while these options add to it. See Charms.
go-build
Command: go build
Example
// Wire go-build into a `build` target. `magus run build` forks `go build`.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun build(ctx: magus\Context, args: [str]) > void {
go["go-build"](ctx);
}
go-clean
go rejects -cache/-testcache/-modcache/-fuzzcache combined with ANY package pattern ("cannot be used with package arguments"), in any order, so "./..." can only be safe on a fully bare invocation. defaultArgs cannot express that: it rides along even when magus run <t> -- <extra> forwards a flag. trailingArgs drops out the moment any arg is forwarded, keeping a bare magus run go::go-clean . identical to today (go clean ./...) while -- -cache (or -i/-r/-n/-x) reaches go clean with no pattern at all.
Command: go clean ./...
Example
// Wire go-clean into a `clean` target: `magus run clean` forks `go clean ./...`.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun clean(ctx: magus\Context, args: [str]) > void {
go["go-clean"](ctx);
}
go-fmt
Command: gofmt -l .
rw
Replaces -l with -w.
JSON Patch
[
{
"op": "replace",
"path": "/0",
"value": "-w"
}
]
Example
// go-fmt lists misformatted files; the rw charm rewrites them in place.
// `magus run format` checks, `magus run format:rw` applies gofmt.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun format(ctx: magus\Context, args: [str]) > void {
go["go-fmt"](ctx);
}
go-generate
Command: go generate ./...
Example
// Wire go-generate into a `generate` target: `magus run generate` forks
// `go generate ./...`.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun generate(ctx: magus\Context, args: [str]) > void {
go["go-generate"](ctx);
}
go-mod-download
Command: go mod download
update
Replaces mod with get, replaces download with -u, inserts ./....
JSON Patch
[
{
"op": "replace",
"path": "/0",
"value": "get"
},
{
"op": "replace",
"path": "/1",
"value": "-u"
},
{
"op": "add",
"path": "/2",
"value": "./..."
}
]
go-mod-edit
Edit is offline: it "reads only go.mod; it does not look up information about the modules involved" (go help mod edit), so the same tree always yields the same bytes. The write charm is therefore rw, not update.
Command: go mod edit -print
rw
Drops -print.
JSON Patch
[
{
"op": "remove",
"path": "/2"
}
]
go-mod-json
Captures Go's structured module view for the spell's higher-level Buzz helper. This is deliberately a separate read-only op: -json and -print are distinct Go modes, while go-mod-edit remains the one command that applies derived edits.
Command: go mod edit -json
go-mod-tidy
Tidy resolves against the module proxy, and an import go.mod does not require yet arrives at latest, so its result turns on what upstream serves today rather than on this tree alone. The write charm is therefore update, not rw.
Command: go mod tidy --diff
update
Drops --diff.
JSON Patch
[
{
"op": "remove",
"path": "/2"
}
]
Example
// go-mod-tidy checks go.mod/go.sum with --diff (CI-safe); the rw charm drops
// --diff so `magus run tidy:rw` applies the changes.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun tidy(ctx: magus\Context, args: [str]) > void {
go["go-mod-tidy"](ctx);
}
go-run
Command: go run
Example
// Run a repo-local Go tool through the spell instead of proc.exec. go-run has no
// useful bare form: name the package and its flags via the "args" option, which
// append after `go run`. This forks `go run ./cmd/gen-docs -out ./docs`.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun generate(ctx: magus\Context, args: [str]) > void {
go["go-run"](ctx, {"args": ["./cmd/gen-docs", "-out", "./docs"]});
}
go-test
./... is a default, not a fixed arg: a magusfile that passes args replaces it, so one package's tests need not compile every test binary in the module. A bare go.test() (and magus run <t> -- <extra> forwarding) still runs the whole tree.
Command: go test ./...
cd
Appends -covermode=atomic, appends -coverprofile=coverage.out.
JSON Patch
[
{
"op": "add",
"path": "/-",
"value": "-covermode=atomic"
},
{
"op": "add",
"path": "/-",
"value": "-coverprofile=coverage.out"
}
]
debug
Appends -v.
JSON Patch
[
{
"op": "add",
"path": "/-",
"value": "-v"
}
]
Example
// go-test runs the suite; here with the race detector. Explicit args replace the
// op's `./...` default, so the scope rides along with the flags and `magus run test`
// forks `go test -race ./...`. The cd charm (`magus run test:cd`) adds the atomic
// coverage profile a CD pipeline ships.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun test(ctx: magus\Context, args: [str]) > void {
go["go-test"](ctx, { "args": ["-race", "./..."] });
}
go-vet
Command: go vet ./...
Example
// go-vet is static analysis, so it composes into the canonical `lint` target
// (alongside golangci-lint) rather than a bespoke `vet` target. `magus run lint`
// forks `go vet ./...`.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun lint(ctx: magus\Context, args: [str]) > void {
go["go-vet"](ctx);
}
golangci-lint
Invoked directly rather than through go tool: golangci-lint generates no code, so it has none of the generator/runtime lockstep that keeps protoc-gen-go pinned in go.mod. go tool golangci-lint also required the binary in the module's tool block, and a workspace that had not put it there got "no such tool" - the op could not run at all. On PATH it is pinned by whatever the workspace uses (mise, asdf, a system package), and the spell's version probe records which.
Command: golangci-lint run ./...
debug
Appends -v.
JSON Patch
[
{
"op": "add",
"path": "/-",
"value": "-v"
}
]
rw
Inserts --fix.
JSON Patch
[
{
"op": "add",
"path": "/1",
"value": "--fix"
}
]
Example
// golangci-lint runs as a `go tool` (resolved from go.mod's tool block). The rw
// charm inserts --fix, so `magus run lint:rw` applies the autofixable findings.
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun lint(ctx: magus\Context, args: [str]) > void {
go["golangci-lint"](ctx);
}
govulncheck
Invoked directly rather than through go tool, for the same reason as golangCILint above: go tool govulncheck requires the binary in the module's tool block, and a workspace that had not put it there got "no such tool" - so the op could not run at all. On PATH it is pinned by whatever the workspace uses, and the OBSERVE probe records which, alongside the database date, for the targets that compose this op. There is no version probe: see the tools table above for why one would key every target in the project.
Command: govulncheck ./...
Example
// govulncheck scans the module's call graph for known vulnerabilities, run as a
// `go tool` so it resolves from go.mod's tool block. Security scanning is static
// analysis, so it composes into the canonical `lint` target - not a bespoke
// `audit`/`security` target. (A slow scan can instead be gated in `ci`.)
import "magus";
import "magus/spell/go";
magus\project({ "spells": [go] });
export fun lint(ctx: magus\Context, args: [str]) > void {
go["govulncheck"](ctx);
}
scip
magus injects MAGUS_SYMBOL_INDEX with the cache destination, so the index never lands in the tree; scip-go writes there via --output. The runner resolves the bare $MAGUS_SYMBOL_INDEX token against that destination, so no shell is needed to expand it.
Command: scip-go --output $MAGUS_SYMBOL_INDEX