diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go new file mode 100644 index 000000000..78c69ad02 --- /dev/null +++ b/pkg/cmd/auth/auth.go @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package auth + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/auth/login" + "go.probo.inc/probo/pkg/cmd/auth/logout" + "go.probo.inc/probo/pkg/cmd/auth/status" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "auth ", + Short: "Authenticate proboctl with Probo", + } + + cmd.AddCommand(login.NewCmdLogin(f)) + cmd.AddCommand(logout.NewCmdLogout(f)) + cmd.AddCommand(status.NewCmdStatus(f)) + + return cmd +} diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go new file mode 100644 index 000000000..cbabbc747 --- /dev/null +++ b/pkg/cmd/auth/login/login.go @@ -0,0 +1,118 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package login + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/config" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +func NewCmdLogin(f *cmdutil.Factory) *cobra.Command { + var ( + flagHost string + flagToken string + flagOrganization string + ) + + cmd := &cobra.Command{ + Use: "login", + Short: "Authenticate with a Probo host", + Example: ` # Interactive login (prompts for hostname, token, and org) + proboctl auth login + + # Non-interactive login + proboctl auth login --hostname app.getprobo.com --token --org `, + RunE: func(cmd *cobra.Command, args []string) error { + if f.IOStreams.IsInteractive() { + if flagHost == "" { + err := huh.NewInput(). + Title("Probo hostname"). + Placeholder("app.getprobo.com"). + Value(&flagHost). + Run() + if err != nil { + return err + } + if flagHost == "" { + flagHost = "app.getprobo.com" + } + } + + if flagToken == "" { + err := huh.NewInput(). + Title("API token"). + EchoMode(huh.EchoModePassword). + Value(&flagToken). + Run() + if err != nil { + return err + } + } + + if flagOrganization == "" { + err := huh.NewInput(). + Title("Default organization ID"). + Placeholder("optional"). + Value(&flagOrganization). + Run() + if err != nil { + return err + } + } + } + + if flagHost == "" { + flagHost = "app.getprobo.com" + } + + if flagToken == "" { + return fmt.Errorf("token is required; pass --token or run interactively") + } + + cfg, err := f.Config() + if err != nil { + return err + } + + cfg.Hosts[flagHost] = &config.HostConfig{ + Token: flagToken, + Organization: flagOrganization, + } + cfg.ActiveHost = flagHost + + if err := cfg.Save(); err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "Logged in to %s\n", + flagHost, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagHost, "hostname", "", "Probo hostname (default: app.getprobo.com)") + cmd.Flags().StringVar(&flagToken, "token", "", "API token") + cmd.Flags().StringVar(&flagOrganization, "org", "", "Default organization ID") + + return cmd +} diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go new file mode 100644 index 000000000..202982811 --- /dev/null +++ b/pkg/cmd/auth/logout/logout.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package logout + +import ( + "fmt" + "maps" + "slices" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +func NewCmdLogout(f *cmdutil.Factory) *cobra.Command { + var flagHost string + + cmd := &cobra.Command{ + Use: "logout", + Short: "Log out of a Probo host", + Example: ` # Log out of the active host + proboctl auth logout + + # Log out of a specific host + proboctl auth logout --hostname app.getprobo.com`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + if flagHost == "" { + host, _, err := cfg.DefaultHost() + if err != nil { + return err + } + + hosts := slices.Sorted(maps.Keys(cfg.Hosts)) + if len(hosts) > 1 && f.IOStreams.IsInteractive() { + options := make([]huh.Option[string], len(hosts)) + for i, h := range hosts { + options[i] = huh.NewOption(h, h) + } + err := huh.NewSelect[string](). + Title("Select a host to log out of"). + Options(options...). + Value(&flagHost). + Run() + if err != nil { + return err + } + } else { + flagHost = host + } + } + + if _, ok := cfg.Hosts[flagHost]; !ok { + return fmt.Errorf("not logged in to %s", flagHost) + } + + delete(cfg.Hosts, flagHost) + + if err := cfg.Save(); err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "Logged out of %s\n", + flagHost, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagHost, "hostname", "", "Probo hostname to log out of") + + return cmd +} diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go new file mode 100644 index 000000000..c382e810f --- /dev/null +++ b/pkg/cmd/auth/status/status.go @@ -0,0 +1,77 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package status + +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +func NewCmdStatus(f *cmdutil.Factory) *cobra.Command { + return &cobra.Command{ + Use: "status", + Short: "View authentication status", + Example: ` # Show all configured hosts and their authentication status + proboctl auth status`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + if len(cfg.Hosts) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "You are not logged in to any Probo hosts.") + return nil + } + + bold := lipgloss.NewStyle().Bold(true) + + for host, hc := range cfg.Hosts { + label := host + if host == cfg.ActiveHost { + label += " (active)" + } + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "%s\n", + bold.Render(label), + ) + + tokenStatus := "not set" + if hc.Token != "" { + tokenStatus = "set" + } + _, _ = fmt.Fprintf( + f.IOStreams.Out, + " Token: %s\n", + tokenStatus, + ) + + if hc.Organization != "" { + _, _ = fmt.Fprintf( + f.IOStreams.Out, + " Organization: %s\n", + hc.Organization, + ) + } + } + + return nil + }, + } +}