diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index 65f05aa..7a06a0f 100644
--- a/.gitea/workflows/release.yml
+++ b/.gitea/workflows/release.yml
@@ -30,7 +30,8 @@ jobs:
CGO_ENABLED: 1
run: |
mkdir -p dist
- go build -trimpath -ldflags="-s -w" \
+ go build -trimpath \
+ -ldflags="-s -w -X main.version=${{ github.ref_name }}" \
-o dist/patterm-${{ github.ref_name }}-linux-amd64 \
./cmd/patterm
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb9ce0b..ddbfdd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,21 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- `patterm --version` prints the build version, git commit, and build
+ date (e.g. `patterm v0.0.1 (commit abc1234, built 2026-05-14)`). The
+ version string is injected by the build (`make patterm` derives it
+ from `git describe`; the release workflow injects the pushed tag).
+ Commit and date come from the Go toolchain's embedded VCS info, so
+ nothing has to be bumped by hand.
+
+### Changed
+- CLI flag parsing switched from Go's stdlib `flag` to `spf13/pflag`.
+ `--project` (and the internal `--socket` / `--identity` /
+ `--scenario` / `--patterm-bin` flags) are now the only accepted form
+ — single-hyphen long flags like `-project` are rejected. Help output
+ renders the canonical `--flag` form.
+
## [0.0.1] - 2026-05-14
### Fixed
diff --git a/Makefile b/Makefile
index 799570a..2b62e4d 100644
--- a/Makefile
+++ b/Makefile
@@ -32,11 +32,13 @@ deps-build: $(INSTALL)/lib/libghostty-vt.a
clean-deps:
rm -rf $(SOURCE) $(INSTALL)
+VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
+
spike: deps
go build -o ./bin/spike ./cmd/spike
patterm: deps
- go build -o ./bin/patterm ./cmd/patterm
+ go build -ldflags "-X main.version=$(VERSION)" -o ./bin/patterm ./cmd/patterm
test: deps
go test ./...
diff --git a/cmd/patterm/debug_harness.go b/cmd/patterm/debug_harness.go
index dee275b..768cfd2 100644
--- a/cmd/patterm/debug_harness.go
+++ b/cmd/patterm/debug_harness.go
@@ -2,10 +2,11 @@ package main
import (
"encoding/json"
- "flag"
"fmt"
"os"
+ flag "github.com/spf13/pflag"
+
"github.com/hjbdev/patterm/internal/harness"
)
diff --git a/cmd/patterm/main.go b/cmd/patterm/main.go
index e45d141..61c009c 100644
--- a/cmd/patterm/main.go
+++ b/cmd/patterm/main.go
@@ -4,6 +4,7 @@
//
// patterm run in $PWD
// patterm --project
run in
+// patterm --version print version and exit
// patterm mcp-stdio --socket S --identity I
// internal: stdio MCP proxy spawned for
// children, forwards JSON-RPC over S
@@ -13,15 +14,22 @@ package main
import (
"context"
- "flag"
"fmt"
"os"
+ "runtime/debug"
+ "time"
+
+ flag "github.com/spf13/pflag"
"github.com/hjbdev/patterm/internal/app"
"github.com/hjbdev/patterm/internal/mcp"
"github.com/hjbdev/patterm/internal/projectkey"
)
+// version is overridden at build time via `-ldflags "-X main.version=..."`.
+// Defaults to "dev" so source builds are still meaningful.
+var version = "dev"
+
func main() {
// The mcp-stdio subcommand is a separate top-level mode: when an
// agent CLI launches `patterm mcp-stdio --socket ...`, the same
@@ -38,9 +46,17 @@ func main() {
return
}
- var projectDir = flag.String("project", "", "project directory (default $PWD)")
+ var (
+ projectDir = flag.String("project", "", "project directory (default $PWD)")
+ showVersion = flag.Bool("version", false, "print version and exit")
+ )
flag.Parse()
+ if *showVersion {
+ fmt.Println(versionString())
+ return
+ }
+
cwd, err := os.Getwd()
if err != nil {
die("getwd: %v", err)
@@ -80,6 +96,33 @@ func runMCPProxy() {
}
}
+func versionString() string {
+ commit, date := "unknown", "unknown"
+ if info, ok := debug.ReadBuildInfo(); ok {
+ dirty := false
+ for _, s := range info.Settings {
+ switch s.Key {
+ case "vcs.revision":
+ if len(s.Value) >= 7 {
+ commit = s.Value[:7]
+ } else if s.Value != "" {
+ commit = s.Value
+ }
+ case "vcs.time":
+ if t, err := time.Parse(time.RFC3339, s.Value); err == nil {
+ date = t.Format("2006-01-02")
+ }
+ case "vcs.modified":
+ dirty = s.Value == "true"
+ }
+ }
+ if dirty && commit != "unknown" {
+ commit += "-dirty"
+ }
+ }
+ return fmt.Sprintf("patterm %s (commit %s, built %s)", version, commit, date)
+}
+
func die(format string, args ...any) {
fmt.Fprintf(os.Stderr, "patterm: "+format+"\n", args...)
os.Exit(1)
diff --git a/go.mod b/go.mod
index 5e0e902..6a63a7f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.26.3
require (
github.com/creack/pty v1.1.24
+ github.com/spf13/pflag v1.0.10
golang.org/x/term v0.43.0
)
diff --git a/go.sum b/go.sum
index bea4245..9d57516 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=