From b3c9b672b07cfa920e0aa3ec1efe82a0efe3fb3f Mon Sep 17 00:00:00 2001 From: Mike Peachey Date: Sat, 25 Apr 2026 01:00:14 +0100 Subject: [PATCH] feat: Go edition scaffolding, CLI framework & multi-call entry point (#489) --- go/cmd/tfenv/main.go | 32 +++++++++ go/go.mod | 3 + go/internal/cli/cli.go | 111 +++++++++++++++++++++++++++++++ go/internal/cli/cli_test.go | 54 +++++++++++++++ go/internal/config/config.go | 2 + go/internal/install/install.go | 2 + go/internal/list/list.go | 2 + go/internal/logging/logging.go | 2 + go/internal/platform/platform.go | 2 + go/internal/resolve/resolve.go | 2 + go/internal/shim/shim.go | 15 +++++ go/internal/shim/shim_test.go | 12 ++++ 12 files changed, 239 insertions(+) create mode 100644 go/cmd/tfenv/main.go create mode 100644 go/go.mod create mode 100644 go/internal/cli/cli.go create mode 100644 go/internal/cli/cli_test.go create mode 100644 go/internal/config/config.go create mode 100644 go/internal/install/install.go create mode 100644 go/internal/list/list.go create mode 100644 go/internal/logging/logging.go create mode 100644 go/internal/platform/platform.go create mode 100644 go/internal/resolve/resolve.go create mode 100644 go/internal/shim/shim.go create mode 100644 go/internal/shim/shim_test.go diff --git a/go/cmd/tfenv/main.go b/go/cmd/tfenv/main.go new file mode 100644 index 0000000..5023d06 --- /dev/null +++ b/go/cmd/tfenv/main.go @@ -0,0 +1,32 @@ +// Package main provides the multi-call entry point for the tfenv Go edition. +// +// When invoked as "tfenv", it dispatches to the CLI subcommand handler. +// When invoked as "terraform" (e.g. via symlink), it delegates to the shim. +// +// Multi-call detection uses the raw basename of os.Args[0] (before symlink +// resolution). A symlink named "terraform" → "tfenv" will see "terraform" +// as the basename and route to the shim. +package main + +import ( + "os" + "path/filepath" + + "github.com/tfutils/tfenv/go/internal/cli" + "github.com/tfutils/tfenv/go/internal/shim" +) + +// version is set at build time via -ldflags "-X main.version=...". +// It defaults to "dev" for local builds. +var version = "dev" + +func main() { + basename := filepath.Base(os.Args[0]) + + switch basename { + case "terraform": + os.Exit(shim.Run(os.Args[1:])) + default: + os.Exit(cli.Run(version, os.Args[1:])) + } +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..589ed57 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module github.com/tfutils/tfenv/go + +go 1.24 diff --git a/go/internal/cli/cli.go b/go/internal/cli/cli.go new file mode 100644 index 0000000..3bd065a --- /dev/null +++ b/go/internal/cli/cli.go @@ -0,0 +1,111 @@ +// Package cli provides the command dispatch framework for tfenv. +// +// Subcommands are registered in a map of name to handler function. +// Other packages plug in by adding entries to the registry. +package cli + +import ( + "fmt" + "os" + "sort" + "strings" +) + +// Handler is the function signature for a subcommand handler. +// It receives the remaining command-line arguments and returns an exit code. +type Handler func(args []string) int + +// command holds metadata for a registered subcommand. +type command struct { + handler Handler + description string +} + +// registry maps subcommand names to their handlers and descriptions. +var registry = map[string]command{} + +// Register adds a subcommand to the dispatch registry. +func Register(name string, description string, handler Handler) { + registry[name] = command{ + handler: handler, + description: description, + } +} + +// Run dispatches to the appropriate subcommand based on args. +// It returns an exit code suitable for os.Exit. +func Run(version string, args []string) int { + if len(args) == 0 { + printUsage(version) + return 0 + } + + subcmd := args[0] + + // Handle --version and version as special cases. + if subcmd == "--version" || subcmd == "version" { + fmt.Fprintf(os.Stdout, "tfenv %s\n", version) + return 0 + } + + // Handle help as a special case. + if subcmd == "help" || subcmd == "--help" || subcmd == "-h" { + printUsage(version) + return 0 + } + + // Look up the subcommand in the registry. + cmd, ok := registry[subcmd] + if !ok { + fmt.Fprintf(os.Stderr, "tfenv: unknown command %q\n", subcmd) + fmt.Fprintf(os.Stderr, "Run 'tfenv help' for usage.\n") + return 1 + } + + return cmd.handler(args[1:]) +} + +// printUsage prints the help text listing all registered subcommands. +func printUsage(version string) { + fmt.Fprintf(os.Stdout, "tfenv %s\n\n", version) + fmt.Fprintf(os.Stdout, "Usage: tfenv [args]\n\n") + + // Always include the built-in commands. + builtins := []struct { + name string + desc string + }{ + {"help", "Show this help output"}, + {"version", "Print tfenv version"}, + } + + // Collect registered commands and sort them. + var names []string + for name := range registry { + names = append(names, name) + } + sort.Strings(names) + + fmt.Fprintf(os.Stdout, "Commands:\n") + + // Print built-in commands first. + for _, b := range builtins { + fmt.Fprintf(os.Stdout, " %-16s %s\n", b.name, b.desc) + } + + // Print registered commands. + for _, name := range names { + cmd := registry[name] + fmt.Fprintf(os.Stdout, " %-16s %s\n", name, cmd.description) + } + + // Build the full list for "Available commands" summary. + var all []string + for _, b := range builtins { + all = append(all, b.name) + } + all = append(all, names...) + sort.Strings(all) + + fmt.Fprintf(os.Stdout, "\nAvailable commands: %s\n", strings.Join(all, ", ")) +} diff --git a/go/internal/cli/cli_test.go b/go/internal/cli/cli_test.go new file mode 100644 index 0000000..13a6928 --- /dev/null +++ b/go/internal/cli/cli_test.go @@ -0,0 +1,54 @@ +package cli + +import ( + "testing" +) + +func TestRunVersion(t *testing.T) { + exit := Run("1.2.3", []string{"--version"}) + if exit != 0 { + t.Errorf("expected exit code 0, got %d", exit) + } +} + +func TestRunVersionSubcommand(t *testing.T) { + exit := Run("1.2.3", []string{"version"}) + if exit != 0 { + t.Errorf("expected exit code 0, got %d", exit) + } +} + +func TestRunHelp(t *testing.T) { + exit := Run("1.2.3", []string{"help"}) + if exit != 0 { + t.Errorf("expected exit code 0, got %d", exit) + } +} + +func TestRunNoArgs(t *testing.T) { + exit := Run("1.2.3", []string{}) + if exit != 0 { + t.Errorf("expected exit code 0, got %d", exit) + } +} + +func TestRunUnknownCommand(t *testing.T) { + exit := Run("1.2.3", []string{"unknown-command"}) + if exit != 1 { + t.Errorf("expected exit code 1, got %d", exit) + } +} + +func TestRegisterAndRun(t *testing.T) { + Register("test-cmd", "A test command", func(args []string) int { + return 0 + }) + defer func() { + delete(registry, "test-cmd") + }() + + exit := Run("1.2.3", []string{"test-cmd"}) + if exit != 0 { + t.Errorf("expected exit code 0, got %d", exit) + } +} diff --git a/go/internal/config/config.go b/go/internal/config/config.go new file mode 100644 index 0000000..5ee23d9 --- /dev/null +++ b/go/internal/config/config.go @@ -0,0 +1,2 @@ +// Package config handles environment variable loading and state directory resolution. +package config diff --git a/go/internal/install/install.go b/go/internal/install/install.go new file mode 100644 index 0000000..6b60a62 --- /dev/null +++ b/go/internal/install/install.go @@ -0,0 +1,2 @@ +// Package install implements Terraform binary download, verification, and installation. +package install diff --git a/go/internal/list/list.go b/go/internal/list/list.go new file mode 100644 index 0000000..eafb8be --- /dev/null +++ b/go/internal/list/list.go @@ -0,0 +1,2 @@ +// Package list provides local and remote Terraform version listing. +package list diff --git a/go/internal/logging/logging.go b/go/internal/logging/logging.go new file mode 100644 index 0000000..7bf47aa --- /dev/null +++ b/go/internal/logging/logging.go @@ -0,0 +1,2 @@ +// Package logging provides structured logging for the tfenv Go edition. +package logging diff --git a/go/internal/platform/platform.go b/go/internal/platform/platform.go new file mode 100644 index 0000000..343d3bf --- /dev/null +++ b/go/internal/platform/platform.go @@ -0,0 +1,2 @@ +// Package platform detects the current OS, architecture, and platform-specific behaviour. +package platform diff --git a/go/internal/resolve/resolve.go b/go/internal/resolve/resolve.go new file mode 100644 index 0000000..6c468b1 --- /dev/null +++ b/go/internal/resolve/resolve.go @@ -0,0 +1,2 @@ +// Package resolve implements .terraform-version file discovery and version constraint resolution. +package resolve diff --git a/go/internal/shim/shim.go b/go/internal/shim/shim.go new file mode 100644 index 0000000..b2684bf --- /dev/null +++ b/go/internal/shim/shim.go @@ -0,0 +1,15 @@ +// Package shim implements the Terraform shim, intercepting calls to the +// terraform binary and delegating to the correct installed version. +package shim + +import ( + "fmt" + "os" +) + +// Run is the entry point for the terraform shim. +// It is invoked when the binary is called as "terraform" via symlink. +func Run(args []string) int { + fmt.Fprintf(os.Stderr, "terraform shim not yet implemented\n") + return 1 +} diff --git a/go/internal/shim/shim_test.go b/go/internal/shim/shim_test.go new file mode 100644 index 0000000..dffeba7 --- /dev/null +++ b/go/internal/shim/shim_test.go @@ -0,0 +1,12 @@ +package shim + +import ( + "testing" +) + +func TestRunReturnsOne(t *testing.T) { + exit := Run([]string{"version"}) + if exit != 1 { + t.Errorf("expected exit code 1 (stub not implemented), got %d", exit) + } +}