Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ before:
builds:
- env:
- CGO_ENABLED=1
ldflags:
- -s -w -X eko/cmd.Version={{.Version}} -X eko/cmd.Commit={{.Commit}} -X eko/cmd.BuildDate={{.Date}}
goos:
- linux
- windows
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ go build -o eko main.go
# Initialize Eko in any directory
eko init

# Inspect the installed version and build metadata
eko version

# Save an instant local snapshot (with optional AI summary)
eko save -m "Refactored payment gateway handler" --ai

Expand Down
83 changes: 83 additions & 0 deletions cmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1919,3 +1919,86 @@ func TestCompletionCmd_TooManyArgs(t *testing.T) {
t.Error("expected error when more than one shell argument is given, got nil")
}
}

func TestFormatVersion(t *testing.T) {
got := formatVersion("1.1.0", "8c9d1a2f", "2026-08-26T08:00:00Z")
want := "eko version 1.1.0 (" + runtime.GOOS + "/" + runtime.GOARCH + ")\n" +
"Go version: " + runtime.Version() + "\n" +
"Git commit: 8c9d1a2f\n" +
"Build date: 2026-08-26T08:00:00Z\n"

if got != want {
t.Fatalf("unexpected version banner:\nwant:\n%s\ngot:\n%s", want, got)
}
}

func TestFormatVersion_blankBuildMetadataFallsBackToPlaceholders(t *testing.T) {
// A build without -ldflags (or with empty values injected) must never print
// blank fields.
got := formatVersion("", "", " ")
want := "eko version dev (" + runtime.GOOS + "/" + runtime.GOARCH + ")\n" +
"Go version: " + runtime.Version() + "\n" +
"Git commit: unknown\n" +
"Build date: unknown\n"

if got != want {
t.Fatalf("unexpected fallback version banner:\nwant:\n%s\ngot:\n%s", want, got)
}
}

func TestVersionCommand_defaultBuildMetadata(t *testing.T) {
// The package defaults are what an un-stamped `go build` produces.
if Version != "dev" || Commit != "unknown" || BuildDate != "unknown" {
t.Fatalf("unexpected default build metadata: version=%q commit=%q buildDate=%q", Version, Commit, BuildDate)
}
}

func TestVersionCommand(t *testing.T) {
got, err := executeCommand(rootCmd, "version")
if err != nil {
t.Fatalf("eko version returned an error: %v", err)
}

want := formatVersion(Version, Commit, BuildDate)
if got != want {
t.Fatalf("unexpected eko version output:\nwant:\n%s\ngot:\n%s", want, got)
}

for _, fragment := range []string{
runtime.Version(),
runtime.GOOS + "/" + runtime.GOARCH,
"Git commit:",
} {
if !strings.Contains(got, fragment) {
t.Errorf("expected version output to contain %q, got:\n%s", fragment, got)
}
}
}

func TestVersionCommand_rejectsArguments(t *testing.T) {
if _, err := executeCommand(rootCmd, "version", "extra"); err == nil {
t.Error("expected an error when eko version is given a positional argument, got nil")
}
}

func TestVersionFlags_matchVersionSubcommand(t *testing.T) {
want := formatVersion(Version, Commit, BuildDate)

for _, flag := range []string{"--version", "-v"} {
t.Run(flag, func(t *testing.T) {
t.Cleanup(func() {
if err := rootCmd.Flags().Set("version", "false"); err != nil {
t.Errorf("reset version flag: %v", err)
}
})

got, err := executeCommand(rootCmd, flag)
if err != nil {
t.Fatalf("eko %s returned an error: %v", flag, err)
}
if got != want {
t.Fatalf("unexpected eko %s output:\nwant:\n%s\ngot:\n%s", flag, want, got)
}
})
}
}
11 changes: 9 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ import (
)

var rootCmd = &cobra.Command{
Use: "eko",
Short: "eko – AI Snapshot Versioning CLI",
Use: "eko",
Short: "eko – AI Snapshot Versioning CLI",
Version: Version,

SilenceUsage: true,
SilenceErrors: true,
}

func init() {
// `eko -v` / `eko --version` print the same banner as `eko version`.
rootCmd.SetVersionTemplate(formatVersion(Version, Commit, BuildDate))
rootCmd.Flags().BoolP("version", "v", false, "print version, runtime and build information")
}

func Execute() {
shutdown, err := telemetry.Init(context.Background())
if err != nil {
Expand Down
55 changes: 55 additions & 0 deletions cmd/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"fmt"
"runtime"
"strings"

"github.com/spf13/cobra"
)

// Build metadata. These are overridden at release time through the Go linker,
// e.g. -ldflags "-X eko/cmd.Version=1.1.0 -X eko/cmd.Commit=8c9d1a2f".
// The defaults keep locally built binaries readable instead of printing blanks.
var (
Version = "dev"
Commit = "unknown"
BuildDate = "unknown"
)

var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version, runtime and build information",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
_, err := fmt.Fprint(cmd.OutOrStdout(), formatVersion(Version, Commit, BuildDate))
return err
},
}

// formatVersion renders the version banner shared by `eko version` and the
// root-level -v/--version flags.
func formatVersion(version, commit, buildDate string) string {
return fmt.Sprintf(
"eko version %s (%s/%s)\nGo version: %s\nGit commit: %s\nBuild date: %s\n",
orFallback(version, "dev"),
runtime.GOOS,
runtime.GOARCH,
runtime.Version(),
orFallback(commit, "unknown"),
orFallback(buildDate, "unknown"),
)
}

// orFallback guards against linker flags that inject empty strings, so the
// banner never reports a blank field.
func orFallback(value, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}

func init() {
rootCmd.AddCommand(versionCmd)
}
37 changes: 37 additions & 0 deletions docs/docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Exhaustive reference guide for all commands, flags, options, and aliases in the
| [`eko ai next`](#10-eko-ai-next) | None | AI task & issue recommendation engine | None |
| [`eko ai security`](#10-eko-ai-security) | None | AI hardcoded secret & vulnerability scanner | None |
| [`eko ai gate`](#10-eko-ai-gate) | None | AI pre-commit quality gate evaluation | None |
| [`eko version`](#11-eko-version) | None | Print CLI version, Go runtime, OS/arch, git commit | `-v`/`--version` on the root command |

---

Expand Down Expand Up @@ -314,6 +315,42 @@ eko ai gate

---

## 11. `eko version`

Prints the Eko release version, target operating system and architecture, Go runtime version, Git commit, and build date. The root-level `-v` and `--version` flags produce the exact same output.

```bash
eko version
eko --version
eko -v
```

```text
eko version 1.1.0 (darwin/arm64)
Go version: go1.26.7
Git commit: 8c9d1a2f
Build date: 2026-08-26T08:22:00Z
```

### Build Metadata

Release archives are built by GoReleaser, which stamps the values through the Go linker:

```bash
go build -ldflags "-X eko/cmd.Version=1.1.0 -X eko/cmd.Commit=8c9d1a2f -X eko/cmd.BuildDate=2026-08-26T08:22:00Z" -o eko .
```

A plain `go build` leaves those variables at their defaults, so locally built binaries report `dev` and `unknown` instead of blank fields:

```text
eko version dev (darwin/arm64)
Go version: go1.26.7
Git commit: unknown
Build date: unknown
```

---

## Global Environment Variables

| Variable | Default Value | Usage |
Expand Down
Loading