From 2f159157cc243e0478f17b5f9ca016c60ad2e2cf Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Thu, 21 May 2026 13:19:55 +1000 Subject: [PATCH] feat(version): replace --version with 'version' command to print build information for uc and uncloudd --- cmd/uncloud/art.txt | 4 ++ cmd/uncloud/main.go | 12 ++---- cmd/uncloud/version.go | 82 +++++++++++++++++++++++++++++++++++++ cmd/uncloudd/art.txt | 5 +++ cmd/uncloudd/main.go | 7 ++-- cmd/uncloudd/version.go | 82 +++++++++++++++++++++++++++++++++++++ internal/cli/tui/style.go | 1 + internal/version/version.go | 18 +++++--- 8 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 cmd/uncloud/art.txt create mode 100644 cmd/uncloud/version.go create mode 100644 cmd/uncloudd/art.txt create mode 100644 cmd/uncloudd/version.go diff --git a/cmd/uncloud/art.txt b/cmd/uncloud/art.txt new file mode 100644 index 00000000..fc0e4e4a --- /dev/null +++ b/cmd/uncloud/art.txt @@ -0,0 +1,4 @@ + _ _ ___ +| | | |/ __| +| |_| | (__ + \__,_|\___| diff --git a/cmd/uncloud/main.go b/cmd/uncloud/main.go index 686b483e..be8d93d6 100644 --- a/cmd/uncloud/main.go +++ b/cmd/uncloud/main.go @@ -8,7 +8,6 @@ import ( "os" "strings" - "charm.land/lipgloss/v2" "github.com/psviderski/uncloud/cmd/uncloud/caddy" cmdcontext "github.com/psviderski/uncloud/cmd/uncloud/context" "github.com/psviderski/uncloud/cmd/uncloud/dns" @@ -19,6 +18,7 @@ import ( "github.com/psviderski/uncloud/cmd/uncloud/wg" "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli/config" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/log" "github.com/psviderski/uncloud/internal/machine" @@ -39,7 +39,6 @@ func main() { cmd := &cobra.Command{ Use: "uc", Short: "A CLI tool for managing Uncloud resources such as machines, services, and volumes.", - Version: version.String(), SilenceUsage: true, SilenceErrors: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { @@ -113,15 +112,11 @@ func main() { defaultHelpFunc(c, args) // Only show links for the root 'uc' command. if c.Name() == "uc" { - urlStyle := lipgloss.NewStyle(). - Underline(true). - Foreground(lipgloss.Color("12")) // light blue - fmt.Fprintln(c.OutOrStdout()) fmt.Fprintf(c.OutOrStdout(), "Learn more about Uncloud: %s\n", - urlStyle.Render("https://uncloud.run/docs")) + tui.URLStyle.Render(version.DocsURL)) fmt.Fprintf(c.OutOrStdout(), "Join our Discord community: %s\n", - urlStyle.Render("https://uncloud.run/discord")) + tui.URLStyle.Render(version.DiscordURL)) } }) @@ -151,6 +146,7 @@ func main() { service.NewScaleCommand("service"), service.NewStartCommand("service"), service.NewStopCommand("service"), + NewVersionCommand(), volume.NewRootCommand(), wg.NewRootCommand(), ) diff --git a/cmd/uncloud/version.go b/cmd/uncloud/version.go new file mode 100644 index 00000000..27be1372 --- /dev/null +++ b/cmd/uncloud/version.go @@ -0,0 +1,82 @@ +package main + +import ( + "bytes" + _ "embed" + "fmt" + "strings" + "text/template" + + "github.com/psviderski/uncloud/internal/cli/tui" + "github.com/psviderski/uncloud/internal/version" + "github.com/spf13/cobra" +) + +//go:embed art.txt +var asciiArt string + +// NewVersionCommand creates a new command to print the version and build information for the binary. +func NewVersionCommand() *cobra.Command { + var output string + + cmd := &cobra.Command{ + Use: "version", + Short: "Show version and build information.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + info := version.GetInfo() + w := cmd.OutOrStdout() + + switch output { + case "": + fmt.Fprint(w, humanVersion(info)) + case "json": + s, err := info.JSONString() + if err != nil { + return err + } + fmt.Fprintln(w, s) + default: + s, err := templateVersion(output, info) + if err != nil { + return err + } + fmt.Fprint(w, s) + } + + return nil + }, + } + + cmd.Flags().StringVarP(&output, "output", "o", "", + "Output format: 'json' or a Go template (e.g. '{{.Version}}').\n"+ + "Run with '-o json' to discover the field names available to the template.\n"+ + "(default is human-readable)") + + return cmd +} + +func humanVersion(info version.Info) string { + var b strings.Builder + b.WriteString(asciiArt) + b.WriteString("\n") + b.WriteString(fmt.Sprintf("uc: Uncloud CLI tool for deploying apps and managing resources (%s)", + tui.URLStyle.Render(version.WebsiteURL))) + b.WriteString("\n\n") + b.WriteString(info.String()) + return b.String() +} + +// templateVersion renders the version info using the provided Go template. +func templateVersion(tmpl string, info version.Info) (string, error) { + t, err := template.New("version").Parse(tmpl) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + + var buf bytes.Buffer + if err = t.Execute(&buf, info); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + return buf.String(), nil +} diff --git a/cmd/uncloudd/art.txt b/cmd/uncloudd/art.txt new file mode 100644 index 00000000..3f40c5ce --- /dev/null +++ b/cmd/uncloudd/art.txt @@ -0,0 +1,5 @@ + _ _ _ + _ _ _ __ ___| | ___ _ _ __| | __| | +| | | | '_ \ / __| |/ _ \| | | |/ _` |/ _` | +| |_| | | | | (__| | (_) | |_| | (_| | (_| | + \__,_|_| |_|\___|_|\___/ \__,_|\__,_|\__,_| diff --git a/cmd/uncloudd/main.go b/cmd/uncloudd/main.go index 1db71da1..5b6e2b37 100644 --- a/cmd/uncloudd/main.go +++ b/cmd/uncloudd/main.go @@ -7,6 +7,7 @@ import ( "os/signal" "syscall" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/daemon" "github.com/psviderski/uncloud/internal/log" "github.com/psviderski/uncloud/internal/machine" @@ -24,7 +25,7 @@ func main() { cmd := &cobra.Command{ Use: "uncloudd", Short: "Uncloud machine daemon.", - Version: version.String(), + Long: "Uncloud machine daemon.\n" + tui.URLStyle.Render(version.WebsiteURL), SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -39,11 +40,11 @@ func main() { }, } cmd.PersistentFlags().StringVarP(&dataDir, "data-dir", "d", machine.DefaultDataDir, - "Directory for storing persistent machine state") + "Directory for storing persistent machine state.") _ = cmd.MarkFlagDirname("data-dir") - // Add dial-stdio subcommand. cmd.AddCommand(newDialStdioCommand()) + cmd.AddCommand(newVersionCommand()) // ctx is canceled when the daemon command is interrupted. ctx, cancel := context.WithCancel(context.Background()) diff --git a/cmd/uncloudd/version.go b/cmd/uncloudd/version.go new file mode 100644 index 00000000..dbc04b02 --- /dev/null +++ b/cmd/uncloudd/version.go @@ -0,0 +1,82 @@ +package main + +import ( + "bytes" + _ "embed" + "fmt" + "strings" + "text/template" + + "github.com/psviderski/uncloud/internal/cli/tui" + "github.com/psviderski/uncloud/internal/version" + "github.com/spf13/cobra" +) + +//go:embed art.txt +var asciiArt string + +// newVersionCommand creates a new command to print the version and build information for the binary. +func newVersionCommand() *cobra.Command { + var output string + + cmd := &cobra.Command{ + Use: "version", + Short: "Show version and build information.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + info := version.GetInfo() + w := cmd.OutOrStdout() + + switch output { + case "": + fmt.Fprint(w, humanVersion(info)) + case "json": + s, err := info.JSONString() + if err != nil { + return err + } + fmt.Fprintln(w, s) + default: + s, err := templateVersion(output, info) + if err != nil { + return err + } + fmt.Fprint(w, s) + } + + return nil + }, + } + + cmd.Flags().StringVarP(&output, "output", "o", "", + "Output format: 'json' or a Go template (e.g., '{{.Version}}').\n"+ + "Run with '-o json' to discover the field names available to the template.\n"+ + "(default is human-readable)") + + return cmd +} + +func humanVersion(info version.Info) string { + var b strings.Builder + b.WriteString(asciiArt) + b.WriteString("\n") + b.WriteString(fmt.Sprintf("uncloudd: Uncloud machine daemon (%s)", + tui.URLStyle.Render(version.WebsiteURL))) + b.WriteString("\n\n") + b.WriteString(info.String()) + return b.String() +} + +// templateVersion renders the version info using the provided Go template. +func templateVersion(tmpl string, info version.Info) (string, error) { + t, err := template.New("version").Parse(tmpl) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + + var buf bytes.Buffer + if err = t.Execute(&buf, info); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + return buf.String(), nil +} diff --git a/internal/cli/tui/style.go b/internal/cli/tui/style.go index 523b5486..fb829763 100644 --- a/internal/cli/tui/style.go +++ b/internal/cli/tui/style.go @@ -19,6 +19,7 @@ var ( BoldYellow = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Yellow) NameStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("152")) + URLStyle = lipgloss.NewStyle().Underline(true).Foreground(lipgloss.BrightBlue) ) // FormatImage renders an image reference with the given style, using a faint colon separator for tagged images. diff --git a/internal/version/version.go b/internal/version/version.go index 6fb82c7d..2ad396b9 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -9,12 +9,11 @@ import ( goversion "github.com/caarlos0/go-version" ) -func String() string { - if version == "" { - return "999.0.0-dev" - } - return version -} +const ( + WebsiteURL = "https://uncloud.run" + DocsURL = "https://uncloud.run/docs" + DiscordURL = "https://uncloud.run/discord" +) // Build-time metadata injected via -ldflags by GoReleaser (see .goreleaser.yaml). // These are package-level so the same paths can be referenced from any binary's ldflags. @@ -26,6 +25,13 @@ var ( builtBy string ) +func String() string { + if version == "" { + return "999.0.0-dev" + } + return version +} + // Info holds version and build metadata for an Uncloud binary. type Info struct { Version string