From c3bd7bb4c48e1ea4063c370c39fde5fc54f175dc Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Wed, 9 Jul 2025 13:55:35 +1000 Subject: [PATCH 1/8] fix auth redirect regression --- cli/internal/api/api.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cli/internal/api/api.go b/cli/internal/api/api.go index 2dd96befe..9b4500a95 100644 --- a/cli/internal/api/api.go +++ b/cli/internal/api/api.go @@ -12,10 +12,18 @@ type NitricApiClient struct { transformers []transformer.RequestTransformer } +func withAcceptHeader(req *http.Request) { + req.Header.Set("Accept", "application/json") +} + func NewNitricApiClient(apiUrl *url.URL, transformers ...transformer.RequestTransformer) *NitricApiClient { + defaultTransformers := []transformer.RequestTransformer{ + withAcceptHeader, + } + return &NitricApiClient{ apiUrl: apiUrl, - transformers: transformers, + transformers: append(defaultTransformers, transformers...), } } From 9fb1bc6958d532e2e751f2d086a032e9a3d0a59e Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Wed, 9 Jul 2025 17:54:23 +1000 Subject: [PATCH 2/8] refactor auth to accept token stores --- cli/cmd/accesstoken.go | 31 ++- cli/cmd/build.go | 59 ++--- cli/cmd/login.go | 39 ++- cli/cmd/logout.go | 33 ++- cli/cmd/new.go | 236 +++++++++--------- cli/cmd/root.go | 38 ++- cli/cmd/templates.go | 47 ++++ cli/cmd/templates/list.go | 44 ---- cli/cmd/templates/templates.go | 11 - cli/internal/api/api.go | 27 +- cli/internal/api/tokens.go | 12 + cli/internal/api/transformer/transformer.go | 2 +- cli/internal/auth/auth.go | 152 ----------- cli/internal/auth/token.go | 155 ------------ cli/internal/plugins/repository.go | 6 +- cli/internal/workos/code.go | 57 ----- cli/internal/workos/{ => http}/client.go | 34 ++- cli/internal/workos/keyring.go | 65 +++++ .../{auth => workos}/login_success.html | 0 cli/internal/workos/pkce.go | 186 ++++++++++++++ cli/internal/workos/workos.go | 113 +++++++++ 21 files changed, 695 insertions(+), 652 deletions(-) create mode 100644 cli/cmd/templates.go delete mode 100644 cli/cmd/templates/list.go delete mode 100644 cli/cmd/templates/templates.go create mode 100644 cli/internal/api/tokens.go delete mode 100644 cli/internal/auth/auth.go delete mode 100644 cli/internal/auth/token.go delete mode 100644 cli/internal/workos/code.go rename cli/internal/workos/{ => http}/client.go (92%) create mode 100644 cli/internal/workos/keyring.go rename cli/internal/{auth => workos}/login_success.html (100%) create mode 100644 cli/internal/workos/pkce.go create mode 100644 cli/internal/workos/workos.go diff --git a/cli/cmd/accesstoken.go b/cli/cmd/accesstoken.go index 88f42a7be..ce219b065 100644 --- a/cli/cmd/accesstoken.go +++ b/cli/cmd/accesstoken.go @@ -3,25 +3,24 @@ package cmd import ( "fmt" - "github.com/nitrictech/nitric/cli/internal/auth" "github.com/spf13/cobra" ) -var accessTokenCmd = &cobra.Command{ - Use: "access-token", - Short: "Print an access token for the Nitric Platform", - Long: `Print an access token for the Nitric Platform, using the current login session.`, - Run: func(cmd *cobra.Command, args []string) { - token, err := auth.GetOrRefreshWorkosToken() - if err != nil { - fmt.Printf("\n Not currently logged in, run `nitric login` to login") - return - } +func NewAccessTokenCmd(deps *Dependencies) *cobra.Command { + var accessTokenCmd = &cobra.Command{ + Use: "access-token", + Short: "Print an access token for the Nitric Platform", + Long: `Print an access token for the Nitric Platform, using the current login session.`, + Run: func(cmd *cobra.Command, args []string) { + token, err := deps.WorkOSAuth.GetAccessToken() + if err != nil { + fmt.Printf("\n Not currently logged in, run `nitric login` to login") + return + } - fmt.Println(token.AccessToken) - }, -} + fmt.Println(token) + }, + } -func init() { - rootCmd.AddCommand(accessTokenCmd) + return accessTokenCmd } diff --git a/cli/cmd/build.go b/cli/cmd/build.go index 680009af7..08bb0116f 100644 --- a/cli/cmd/build.go +++ b/cli/cmd/build.go @@ -18,43 +18,44 @@ func (r *MockTerraformPluginRepository) GetPlugin(name string) (*terraform.Plugi return r.plugins[name], nil } -var buildCmd = &cobra.Command{ - Use: "build", - Short: "Builds the nitric application", - Long: `Builds an application using the nitric.yaml application spec and referenced platform.`, - Run: func(cmd *cobra.Command, args []string) { +func NewBuildCmd(deps *Dependencies) *cobra.Command { + var buildCmd = &cobra.Command{ + Use: "build", + Short: "Builds the nitric application", + Long: `Builds an application using the nitric.yaml application spec and referenced platform.`, + Run: func(cmd *cobra.Command, args []string) { - // Read the nitric.yaml file - fs := afero.NewOsFs() + // Read the nitric.yaml file + fs := afero.NewOsFs() - appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) - cobra.CheckErr(err) + appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) + cobra.CheckErr(err) - mockPlatformRepository := terraform.NewMockPlatformRepository() + mockPlatformRepository := terraform.NewMockPlatformRepository() - // TODO:prompt for platform selection if multiple targets are specified - targetPlatform := appSpec.Targets[0] + // TODO:prompt for platform selection if multiple targets are specified + targetPlatform := appSpec.Targets[0] - platform, err := terraform.PlatformFromId(fs, targetPlatform, mockPlatformRepository) - cobra.CheckErr(err) + platform, err := terraform.PlatformFromId(fs, targetPlatform, mockPlatformRepository) + cobra.CheckErr(err) - engine := terraform.New(platform, terraform.WithRepository(plugins.NewPluginRepository())) - // Parse the application spec - // Validate the application spec - // Build the application using the specified platform - // Handle any errors that occur during the build process + repo := plugins.NewPluginRepository(deps.NitricApiClient) + engine := terraform.New(platform, terraform.WithRepository(repo)) + // Parse the application spec + // Validate the application spec + // Build the application using the specified platform + // Handle any errors that occur during the build process - err = engine.Apply(appSpec) - if err != nil { - fmt.Print("Error applying platform: ", err) - return - } + err = engine.Apply(appSpec) + if err != nil { + fmt.Print("Error applying platform: ", err) + return + } - fmt.Println("Build completed successfully.") + fmt.Println("Build completed successfully.") - }, -} + }, + } -func init() { - rootCmd.AddCommand(buildCmd) + return buildCmd } diff --git a/cli/cmd/login.go b/cli/cmd/login.go index 63f45202d..5eeda11b8 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -3,35 +3,26 @@ package cmd import ( "fmt" - "github.com/nitrictech/nitric/cli/internal/auth" "github.com/nitrictech/nitric/cli/internal/style" "github.com/nitrictech/nitric/cli/internal/style/icons" "github.com/spf13/cobra" ) -var loginCmd = &cobra.Command{ - Use: "login", - Short: "Login to Nitric", - Long: `Login to the Nitric CLI.`, - Run: func(cmd *cobra.Command, args []string) { - token, err := auth.GetOrRefreshWorkosToken() - if err == nil { - user := fmt.Sprintf("%s %s <%s>", token.User.FirstName, token.User.LastName, token.User.Email) +func NewLoginCmd(deps *Dependencies) *cobra.Command { + var loginCmd = &cobra.Command{ + Use: "login", + Short: "Login to Nitric", + Long: `Login to the Nitric CLI.`, + Run: func(cmd *cobra.Command, args []string) { + user, err := deps.WorkOSAuth.Login() + if err != nil { + fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) + return + } - fmt.Printf("\n%s Already logged in as %s\n", style.Green(icons.Check), style.Teal(user)) - return - } + fmt.Printf("\n%s Login successful, welcome %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) + }, + } - fmt.Printf("\n%s Logging in...\n", style.Purple(icons.Lightning+" Nitric")) - - err = auth.PerformPKCEFlow() - if err != nil { - fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) - return - } - }, -} - -func init() { - rootCmd.AddCommand(loginCmd) + return loginCmd } diff --git a/cli/cmd/logout.go b/cli/cmd/logout.go index acf7a6fa6..f130781e8 100644 --- a/cli/cmd/logout.go +++ b/cli/cmd/logout.go @@ -1,31 +1,28 @@ package cmd import ( - "errors" "fmt" - "github.com/nitrictech/nitric/cli/internal/auth" "github.com/nitrictech/nitric/cli/internal/style" "github.com/nitrictech/nitric/cli/internal/style/icons" "github.com/spf13/cobra" ) -var logoutCmd = &cobra.Command{ - Use: "logout", - Short: "Logout from Nitric", - Long: `Logout from the Nitric CLI.`, - Run: func(cmd *cobra.Command, args []string) { +func NewLogoutCmd(deps *Dependencies) *cobra.Command { + var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Logout from Nitric", + Long: `Logout from the Nitric CLI.`, + Run: func(cmd *cobra.Command, args []string) { + err := deps.WorkOSAuth.Logout() + if err != nil { + fmt.Printf("\n%s Error logging out: %s\n", style.Red(icons.Cross), err) + return + } - err := auth.DeleteWorkosToken() - if err != nil && !errors.Is(err, auth.ErrNotFound) { - fmt.Printf("\n%s Error logging out: %s\n", style.Red(icons.Cross), err) - return - } + fmt.Printf("\n%s Logged out\n", style.Green(icons.Check)) + }, + } - fmt.Printf("\n%s Logged out\n", style.Green(icons.Check)) - }, -} - -func init() { - rootCmd.AddCommand(logoutCmd) + return logoutCmd } diff --git a/cli/cmd/new.go b/cli/cmd/new.go index 972ccf122..1689c53b2 100644 --- a/cli/cmd/new.go +++ b/cli/cmd/new.go @@ -11,9 +11,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hashicorp/go-getter" - "github.com/nitrictech/nitric/cli/internal/api" - "github.com/nitrictech/nitric/cli/internal/auth" - "github.com/nitrictech/nitric/cli/internal/config" "github.com/nitrictech/nitric/cli/internal/style/colors" "github.com/nitrictech/nitric/cli/pkg/files" "github.com/nitrictech/nitric/cli/pkg/schema" @@ -38,158 +35,157 @@ func projectExists(fs afero.Fs, projectDir string) (bool, error) { return false, nil } -var newCmd = &cobra.Command{ - Use: "new", - Short: "Create a new Nitric project", - Run: func(cmd *cobra.Command, args []string) { - force, err := cmd.Flags().GetBool("force") - if err != nil { - fmt.Printf("Failed to get force flag: %v", err) - return - } +func NewNewCmd(deps *Dependencies) *cobra.Command { + var newCmd = &cobra.Command{ + Use: "new", + Short: "Create a new Nitric project", + Run: func(cmd *cobra.Command, args []string) { + force, err := cmd.Flags().GetBool("force") + if err != nil { + fmt.Printf("Failed to get force flag: %v", err) + return + } - fs := afero.NewOsFs() + fs := afero.NewOsFs() - projectName := "" - if len(args) > 0 { - projectName = args[0] - } + projectName := "" + if len(args) > 0 { + projectName = args[0] + } - if projectName == "" { - fmt.Println() - var err error - projectName, err = tui.RunTextInput("Project name:", func(input string) error { - if input == "" { - return errors.New("project name is required") + if projectName == "" { + fmt.Println() + var err error + projectName, err = tui.RunTextInput("Project name:", func(input string) error { + if input == "" { + return errors.New("project name is required") + } + + // Must be kebab-case + if !regexp.MustCompile(`^[a-z][a-z0-9-]*$`).MatchString(input) { + return errors.New("project name must start with a letter and be lower kebab-case") + } + + return nil + }) + if err != nil || projectName == "" { + fmt.Println(err) + fmt.Println("+" + projectName + "+") + return } + } - // Must be kebab-case - if !regexp.MustCompile(`^[a-z][a-z0-9-]*$`).MatchString(input) { - return errors.New("project name must start with a letter and be lower kebab-case") + projectDir := filepath.Join(".", projectName) + if !force { + projectExists, err := projectExists(fs, projectDir) + if err != nil { + fmt.Println(err.Error()) + return + } + if projectExists { + fmt.Printf("\nDirectory ./%s already exists and is not empty\n", projectDir) + return } - - return nil - }) - if err != nil || projectName == "" { - fmt.Println(err) - fmt.Println("+" + projectName + "+") - return } - } - projectDir := filepath.Join(".", projectName) - if !force { - projectExists, err := projectExists(fs, projectDir) + resp, err := deps.NitricApiClient.GetTemplates() if err != nil { - fmt.Println(err.Error()) + fmt.Printf("Failed to get templates: %v", err) return } - if projectExists { - fmt.Printf("\nDirectory ./%s already exists and is not empty\n", projectDir) + + if len(resp) == 0 { + fmt.Println("No templates found") return } - } - - client := api.NewNitricApiClient(config.GetNitricServerUrl(), auth.WithAuthHeader) - - resp, err := client.GetTemplates() - if err != nil { - fmt.Printf("Failed to get templates: %v", err) - return - } - if len(resp) == 0 { - fmt.Println("No templates found") - return - } + templateNames := make([]string, len(resp)) + for i, template := range resp { + templateNames[i] = template.String() + } - templateNames := make([]string, len(resp)) - for i, template := range resp { - templateNames[i] = template.String() - } + // Prompt the user to select one of the templates + fmt.Println("") + _, index, err := tui.RunSelect(templateNames, "Template:") + if err != nil || index == -1 { + return + } - // Prompt the user to select one of the templates - fmt.Println("") - _, index, err := tui.RunSelect(templateNames, "Template:") - if err != nil || index == -1 { - return - } + template, err := deps.NitricApiClient.GetTemplate(resp[index].TeamSlug, resp[index].Slug, "") + cobra.CheckErr(err) - template, err := client.GetTemplate(resp[index].TeamSlug, resp[index].Slug, "") - cobra.CheckErr(err) + // Find home directory. + home, err := os.UserHomeDir() + if err != nil { + fmt.Println(err) + return + } - // Find home directory. - home, err := os.UserHomeDir() - if err != nil { - fmt.Println(err) - return - } + templateDir := filepath.Join(home, ".nitric", "templates", template.TeamSlug, template.TemplateSlug, template.Version) - templateDir := filepath.Join(home, ".nitric", "templates", template.TeamSlug, template.TemplateSlug, template.Version) + templateCached, err := afero.Exists(fs, filepath.Join(templateDir, "nitric.yaml")) + if err != nil { + fmt.Printf("Failed read template cache directory: %v", err) + return + } - templateCached, err := afero.Exists(fs, filepath.Join(templateDir, "nitric.yaml")) - if err != nil { - fmt.Printf("Failed read template cache directory: %v", err) - return - } + if !templateCached { + goGetter := &getter.Client{ + Ctx: context.Background(), + Dst: templateDir, + Src: template.GitSource, + Mode: getter.ClientModeAny, + DisableSymlinks: true, + } - if !templateCached { - goGetter := &getter.Client{ - Ctx: context.Background(), - Dst: templateDir, - Src: template.GitSource, - Mode: getter.ClientModeAny, - DisableSymlinks: true, + err = goGetter.Get() + if err != nil { + fmt.Printf("Failed to get template: %v", err) + return + } } - err = goGetter.Get() + // Copy the template dir contents into a new project dir + err = os.MkdirAll(projectDir, 0755) if err != nil { - fmt.Printf("Failed to get template: %v", err) + fmt.Printf("Failed to create project directory: %v", err) return } - } - - // Copy the template dir contents into a new project dir - err = os.MkdirAll(projectDir, 0755) - if err != nil { - fmt.Printf("Failed to create project directory: %v", err) - return - } - err = files.CopyDir(fs, templateDir, projectDir) - if err != nil { - fmt.Printf("Failed to copy template directory: %v", err) - return - } + err = files.CopyDir(fs, templateDir, projectDir) + if err != nil { + fmt.Printf("Failed to copy template directory: %v", err) + return + } - nitricYamlPath := filepath.Join(projectDir, "nitric.yaml") + nitricYamlPath := filepath.Join(projectDir, "nitric.yaml") - appSpec, err := schema.LoadFromFile(fs, nitricYamlPath, false) - cobra.CheckErr(err) + appSpec, err := schema.LoadFromFile(fs, nitricYamlPath, false) + cobra.CheckErr(err) - appSpec.Name = projectName + appSpec.Name = projectName - err = schema.SaveToYaml(fs, nitricYamlPath, appSpec) - cobra.CheckErr(err) + err = schema.SaveToYaml(fs, nitricYamlPath, appSpec) + cobra.CheckErr(err) - successStyle := lipgloss.NewStyle().MarginLeft(3) - highlight := lipgloss.NewStyle().Foreground(colors.Teal).Bold(true) + successStyle := lipgloss.NewStyle().MarginLeft(3) + highlight := lipgloss.NewStyle().Foreground(colors.Teal).Bold(true) - var b strings.Builder + var b strings.Builder - b.WriteString("\n") - b.WriteString("Project created!") - b.WriteString("\n\n") - b.WriteString("Navigate to your project with ") - b.WriteString(highlight.Render("cd ./" + projectDir)) - b.WriteString("\n") - b.WriteString("Install dependencies and you're ready to rock! 🪨") + b.WriteString("\n") + b.WriteString("Project created!") + b.WriteString("\n\n") + b.WriteString("Navigate to your project with ") + b.WriteString(highlight.Render("cd ./" + projectDir)) + b.WriteString("\n") + b.WriteString("Install dependencies and you're ready to rock! 🪨") - fmt.Println(successStyle.Render(b.String())) - }, -} + fmt.Println(successStyle.Render(b.String())) + }, + } -func init() { newCmd.Flags().BoolP("force", "f", false, "Force overwrite existing project directory") - rootCmd.AddCommand(newCmd) + + return newCmd } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 658799ca0..bc3b737f8 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -1,8 +1,12 @@ package cmd import ( - "github.com/nitrictech/nitric/cli/cmd/templates" + "log" + "strings" + + "github.com/nitrictech/nitric/cli/internal/api" "github.com/nitrictech/nitric/cli/internal/config" + "github.com/nitrictech/nitric/cli/internal/workos" "github.com/spf13/cobra" ) @@ -17,19 +21,45 @@ test, and deploy your Nitric applications.`, } ) +type Dependencies struct { + WorkOSAuth *workos.WorkOSAuth + NitricApiClient *api.NitricApiClient +} + +var deps *Dependencies = &Dependencies{} + // Execute adds all child commands to the root command and sets flags appropriately. func Execute() error { return rootCmd.Execute() } func init() { - cobra.OnInitialize(initConfig) + cobra.OnInitialize(initConfig, initDependencies) // Global flags rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.nitric.yaml)") - // Add non-sibling commands - rootCmd.AddCommand(templates.TemplatesCmd) + rootCmd.AddCommand(NewLoginCmd(deps)) + rootCmd.AddCommand(NewLogoutCmd(deps)) + rootCmd.AddCommand(NewAccessTokenCmd(deps)) + + rootCmd.AddCommand(NewTemplatesCmd(deps)) + rootCmd.AddCommand(NewBuildCmd(deps)) +} + +func initDependencies() { + deps.NitricApiClient = api.NewNitricApiClient(config.GetNitricServerUrl()) + + workosDetails, err := deps.NitricApiClient.GetWorkOSPublicDetails() + if err != nil { + if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset by peer") { + log.Fatal("unable to connect to the Nitric API. Please check your connection and try again. If the problem persists, please contact support.") + } + + log.Fatal(err) + } + + deps.WorkOSAuth = workos.NewWorkOSAuth(workos.NewKeyringTokenStore("nitric.v2.cli"), workosDetails.ClientID, workosDetails.ApiHostname) } // initConfig reads in config file and ENV variables if set. diff --git a/cli/cmd/templates.go b/cli/cmd/templates.go new file mode 100644 index 000000000..7632c1e0c --- /dev/null +++ b/cli/cmd/templates.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/nitrictech/nitric/cli/internal/style/colors" + "github.com/spf13/cobra" +) + +func NewTemplatesCmd(deps *Dependencies) *cobra.Command { + var templatesCmd = &cobra.Command{ + Use: "templates", + Short: "Manage Nitric templates", + Long: `Manage Nitric templates.`, + } + + var listCmd = &cobra.Command{ + Use: "list", + Short: "List available Nitric templates", + Long: `List available Nitric templates.`, + Run: func(cmd *cobra.Command, args []string) { + resp, err := deps.NitricApiClient.GetTemplates() + if err != nil { + fmt.Printf("Failed to get templates: %v", err) + return + } + + if len(resp) == 0 { + fmt.Println("No templates found") + return + } + + templateStyle := lipgloss.NewStyle().Foreground(colors.Purple) + + fmt.Println(templateStyle.Render("\nAvailable templates:")) + + for _, template := range resp { + fmt.Printf(" %s\n", template) + } + }, + } + + templatesCmd.AddCommand(listCmd) + + return templatesCmd +} diff --git a/cli/cmd/templates/list.go b/cli/cmd/templates/list.go deleted file mode 100644 index 05ca70bf7..000000000 --- a/cli/cmd/templates/list.go +++ /dev/null @@ -1,44 +0,0 @@ -package templates - -import ( - "fmt" - - "github.com/charmbracelet/lipgloss" - "github.com/nitrictech/nitric/cli/internal/api" - "github.com/nitrictech/nitric/cli/internal/auth" - "github.com/nitrictech/nitric/cli/internal/config" - "github.com/nitrictech/nitric/cli/internal/style/colors" - "github.com/spf13/cobra" -) - -var listCmd = &cobra.Command{ - Use: "list", - Short: "List available Nitric templates", - Long: `List available Nitric templates.`, - Run: func(cmd *cobra.Command, args []string) { - client := api.NewNitricApiClient(config.GetNitricServerUrl(), auth.WithAuthHeader) - - resp, err := client.GetTemplates() - if err != nil { - fmt.Printf("Failed to get templates: %v", err) - return - } - - if len(resp) == 0 { - fmt.Println("No templates found") - return - } - - templateStyle := lipgloss.NewStyle().Foreground(colors.Purple) - - fmt.Println(templateStyle.Render("\nAvailable templates:")) - - for _, template := range resp { - fmt.Printf(" %s\n", template) - } - }, -} - -func init() { - TemplatesCmd.AddCommand(listCmd) -} diff --git a/cli/cmd/templates/templates.go b/cli/cmd/templates/templates.go deleted file mode 100644 index 0e9c49444..000000000 --- a/cli/cmd/templates/templates.go +++ /dev/null @@ -1,11 +0,0 @@ -package templates - -import ( - "github.com/spf13/cobra" -) - -var TemplatesCmd = &cobra.Command{ - Use: "templates", - Short: "Manage Nitric templates", - Long: `Manage Nitric templates.`, -} diff --git a/cli/internal/api/api.go b/cli/internal/api/api.go index 9b4500a95..36ebc41f4 100644 --- a/cli/internal/api/api.go +++ b/cli/internal/api/api.go @@ -8,26 +8,16 @@ import ( ) type NitricApiClient struct { - apiUrl *url.URL - transformers []transformer.RequestTransformer + apiUrl *url.URL } -func withAcceptHeader(req *http.Request) { - req.Header.Set("Accept", "application/json") -} - -func NewNitricApiClient(apiUrl *url.URL, transformers ...transformer.RequestTransformer) *NitricApiClient { - defaultTransformers := []transformer.RequestTransformer{ - withAcceptHeader, - } - +func NewNitricApiClient(apiUrl *url.URL) *NitricApiClient { return &NitricApiClient{ - apiUrl: apiUrl, - transformers: append(defaultTransformers, transformers...), + apiUrl: apiUrl, } } -func (c *NitricApiClient) get(path string) (*http.Response, error) { +func (c *NitricApiClient) get(path string, transformers ...transformer.RequestTransformer) (*http.Response, error) { apiUrl, err := url.JoinPath(c.apiUrl.String(), path) if err != nil { return nil, err @@ -38,8 +28,13 @@ func (c *NitricApiClient) get(path string) (*http.Response, error) { return nil, err } - for _, transformer := range c.transformers { - transformer(req) + req.Header.Set("Accept", "application/json") + + for _, transformer := range transformers { + err := transformer(req) + if err != nil { + return nil, err + } } return http.DefaultClient.Do(req) diff --git a/cli/internal/api/tokens.go b/cli/internal/api/tokens.go new file mode 100644 index 000000000..02f1d2245 --- /dev/null +++ b/cli/internal/api/tokens.go @@ -0,0 +1,12 @@ +package api + +type TokenProvider interface { + GetTokens() (*Tokens, error) + SaveTokens(*Tokens) error +} + +type Tokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt int64 `json:"expires_at"` +} diff --git a/cli/internal/api/transformer/transformer.go b/cli/internal/api/transformer/transformer.go index 16e7fabff..968c4ec03 100644 --- a/cli/internal/api/transformer/transformer.go +++ b/cli/internal/api/transformer/transformer.go @@ -2,4 +2,4 @@ package transformer import "net/http" -type RequestTransformer func(req *http.Request) +type RequestTransformer func(req *http.Request) error diff --git a/cli/internal/auth/auth.go b/cli/internal/auth/auth.go deleted file mode 100644 index fea4d57cb..000000000 --- a/cli/internal/auth/auth.go +++ /dev/null @@ -1,152 +0,0 @@ -package auth - -import ( - "context" - _ "embed" - "fmt" - "net" - "net/http" - "strings" - - "github.com/nitrictech/nitric/cli/internal/api" - "github.com/nitrictech/nitric/cli/internal/config" - "github.com/nitrictech/nitric/cli/internal/style" - "github.com/nitrictech/nitric/cli/internal/style/icons" - "github.com/nitrictech/nitric/cli/internal/workos" - "github.com/pkg/browser" -) - -//go:embed login_success.html -var loginSuccessPage []byte - -// The port that the local auth callback server will listen on. -// This is used to handle the callback from the WorkOS auth provider. -var LOCAL_AUTH_CALLBACK_PORT = 48321 - -func WithAuthHeader(req *http.Request) { - token, err := GetOrRefreshWorkosToken() - if err != nil { - return - } - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) -} - -// ErrPortNotAvailable is returned when the local auth callback port is not available. -var ErrPortNotAvailable = fmt.Errorf("port %d is not available, unable to start local auth callback server", LOCAL_AUTH_CALLBACK_PORT) - -func newCallbackHandler(callbackResult chan error, client *workos.HttpClient, pkceChallenge *workos.CodeVerifier) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - code := r.URL.Query().Get("code") - - if code == "" { - w.WriteHeader(http.StatusBadRequest) - - callbackResult <- fmt.Errorf("login code was not provided with login callback") - return - } - - res, err := client.AuthenticateWithCode(code, pkceChallenge.Verifier) - if err != nil { - // TODO: make this pretty - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - - callbackResult <- err - return - } - - err = StoreWorkosToken(res) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - callbackResult <- err - return - } - - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - w.Write(loginSuccessPage) - - fmt.Printf("\n%s Login successful, welcome %s\n", style.Green(icons.Check), style.Teal(res.User.FirstName)) - - callbackResult <- nil - } -} - -func PerformPKCEFlow() error { - // Check if the callback port is available and create a listener - listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", LOCAL_AUTH_CALLBACK_PORT)) - if err != nil { - return ErrPortNotAvailable - } - defer listener.Close() - - client, err := getWorkOSClient() - if err != nil { - return err - } - - pkceChallenge, err := workos.CreatePkceChallenge() - if err != nil { - return err - } - - callbackResult := make(chan error) - - callbackServer := &http.Server{ - // We only bind to loopback for security - // The users own browser is the only client that should connect to this server, during a redirect - Handler: http.HandlerFunc(newCallbackHandler(callbackResult, client, pkceChallenge)), - } - - go callbackServer.Serve(listener) - defer callbackServer.Shutdown(context.Background()) - - authUrl, err := client.GetAuthorizationUrl(workos.GetAuthorizationUrlOptions{ - Provider: "authkit", - RedirectURI: fmt.Sprintf("http://127.0.0.1:%d/callback", LOCAL_AUTH_CALLBACK_PORT), - CodeChallenge: pkceChallenge.Challenge, - CodeChallengeMethod: "S256", - }) - if err != nil { - return err - } - - fmt.Printf("\nOpening browser to %s\n", style.Gray(authUrl)) - - // Open the browser - err = browser.OpenURL(authUrl) - if err != nil { - return err - } - - // Wait for the callback to be received or the server to shutdown - err = <-callbackResult - if err != nil { - fmt.Printf("\n%s Login failed due to an error: %s\n", style.Red(icons.Cross), err) - } - - return nil -} - -var workosClient *workos.HttpClient - -func getWorkOSClient() (*workos.HttpClient, error) { - if workosClient != nil { - return workosClient, nil - } - - nitricApiClient := api.NewNitricApiClient(config.GetNitricServerUrl()) - workosDetails, err := nitricApiClient.GetWorkOSPublicDetails() - if err != nil { - if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset by peer") { - return nil, fmt.Errorf("unable to connect to the Nitric API. Please check your connection and try again. If the problem persists, please contact support.") - } - - return nil, err - } - - workosClient = workos.NewHttpClient(workosDetails.ClientID, workos.WithHostname(workosDetails.ApiHostname)) - return workosClient, nil -} diff --git a/cli/internal/auth/token.go b/cli/internal/auth/token.go deleted file mode 100644 index cb950a1a7..000000000 --- a/cli/internal/auth/token.go +++ /dev/null @@ -1,155 +0,0 @@ -package auth - -import ( - "crypto/rsa" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "math/big" - "time" - - "github.com/golang-jwt/jwt/v4" - "github.com/nitrictech/nitric/cli/internal/config" - "github.com/nitrictech/nitric/cli/internal/workos" - "github.com/zalando/go-keyring" -) - -var ErrNotFound = errors.New("no token found") - -const KEYRING_SERVICE = "nitric.v2.cli" - -// Store 1 token per API -var WORKOS_TOKEN_KEY = getWorkosTokenKey(config.GetNitricServerUrl().String()) - -func getWorkosTokenKey(apiUrl string) string { - // Hash the API URL for a consistent length. We don't use the scheme or host, just the path - hash := sha256.Sum256([]byte(apiUrl + ".workos")) - return fmt.Sprintf("%x", hash) -} - -// StoreToken saves the authentication token to the keyring -func StoreWorkosToken(token *workos.AuthenticationResponse) error { - json, err := json.Marshal(token) - if err != nil { - return fmt.Errorf("failed to marshal token: %w", err) - } - - err = keyring.Set(KEYRING_SERVICE, WORKOS_TOKEN_KEY, string(json)) - if err != nil { - return fmt.Errorf("failed to store token in keyring: %w", err) - } - return nil -} - -func RefreshWorkosToken(workosToken *workos.AuthenticationResponse) (*workos.AuthenticationResponse, error) { - client, err := getWorkOSClient() - if err != nil { - return nil, err - } - - workosToken, err = client.AuthenticateWithRefreshToken(workosToken.RefreshToken, nil) - if err != nil { - return nil, err - } - - err = StoreWorkosToken(workosToken) - if err != nil { - return nil, err - } - - return workosToken, nil -} - -// GetToken retrieves the authentication token from the keyring, use GetOrRefreshWorkosToken instead -func GetWorkosToken() (*workos.AuthenticationResponse, error) { - token, err := keyring.Get(KEYRING_SERVICE, WORKOS_TOKEN_KEY) - if err != nil { - if err == keyring.ErrNotFound { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to retrieve token from keyring: %w", err) - } - - var workosToken workos.AuthenticationResponse - err = json.Unmarshal([]byte(token), &workosToken) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal token: %w", err) - } - - return &workosToken, nil -} - -// GetOrRefreshWorkosToken retrieves the authentication token from the keyring, and refreshes it if it's expired -func GetOrRefreshWorkosToken() (*workos.AuthenticationResponse, error) { - workosToken, err := GetWorkosToken() - if err != nil { - return nil, err - } - - // Decode the JWT to check if it's expired - claims := jwt.RegisteredClaims{} - parsedToken, err := jwt.ParseWithClaims(workosToken.AccessToken, &claims, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - - kid, ok := token.Header["kid"].(string) - if !ok { - return nil, fmt.Errorf("kid not found in token header") - } - - client, err := getWorkOSClient() - if err != nil { - return nil, err - } - - jwk, err := client.GetJWK(kid) - if err != nil { - return nil, fmt.Errorf("failed to get token validation key: %v", err) - } - - return jwkToRSAPublicKey(jwk) - }, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})) - - // Add a 1 second buffer to the expiry time to account for a slight delay in the token being sent to the server - // i.e. the token must remain valid for at least another second, or we'll refresh it early for good measure - if err != nil || !parsedToken.Valid || claims.ExpiresAt.Before(time.Now().Add(1+time.Second)) { - return RefreshWorkosToken(workosToken) - } - - return workosToken, nil -} - -func jwkToRSAPublicKey(jwk workos.JWK) (*rsa.PublicKey, error) { - // Decode the modulus (n) - nBytes, err := base64.RawURLEncoding.DecodeString(jwk.N) - if err != nil { - return nil, err - } - - // Decode the exponent (e) - eBytes, err := base64.RawURLEncoding.DecodeString(jwk.E) - if err != nil { - return nil, err - } - - // Create the RSA public key - return &rsa.PublicKey{ - N: new(big.Int).SetBytes(nBytes), - E: int(new(big.Int).SetBytes(eBytes).Int64()), - }, nil -} - -// DeleteToken removes the authentication token from the keyring -func DeleteWorkosToken() error { - err := keyring.Delete(KEYRING_SERVICE, WORKOS_TOKEN_KEY) - if err != nil { - if err == keyring.ErrNotFound { - return ErrNotFound - } - return fmt.Errorf("failed to delete token from keyring: %w", err) - } - return nil -} diff --git a/cli/internal/plugins/repository.go b/cli/internal/plugins/repository.go index 3dd1dfa80..24fd3ff6a 100644 --- a/cli/internal/plugins/repository.go +++ b/cli/internal/plugins/repository.go @@ -4,8 +4,6 @@ import ( "fmt" "github.com/nitrictech/nitric/cli/internal/api" - "github.com/nitrictech/nitric/cli/internal/auth" - "github.com/nitrictech/nitric/cli/internal/config" "github.com/nitrictech/nitric/engines/terraform" ) @@ -41,8 +39,8 @@ func (r *PluginRepository) GetIdentityPlugin(team, libname, version, name string return identityPluginManifest, nil } -func NewPluginRepository() *PluginRepository { +func NewPluginRepository(client *api.NitricApiClient) *PluginRepository { return &PluginRepository{ - apiClient: api.NewNitricApiClient(config.GetNitricServerUrl(), auth.WithAuthHeader), + apiClient: client, } } diff --git a/cli/internal/workos/code.go b/cli/internal/workos/code.go deleted file mode 100644 index dbfdc7c5d..000000000 --- a/cli/internal/workos/code.go +++ /dev/null @@ -1,57 +0,0 @@ -package workos - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "strings" -) - -type CodeVerifier struct { - Verifier string - Challenge string -} - -// CreatePkceChallenge generates both a code verifier and code challenge for PKCE -func CreatePkceChallenge() (*CodeVerifier, error) { - codeVerifier, err := createCodeVerifier() - if err != nil { - return nil, err - } - codeChallenge, err := createCodeChallenge(codeVerifier) - if err != nil { - return nil, err - } - return &CodeVerifier{ - Verifier: codeVerifier, - Challenge: codeChallenge, - }, nil -} - -// createCodeVerifier generates a random code verifier -func createCodeVerifier() (string, error) { - // Generate 96 bytes (equivalent to 96 * 4 = 384 bits from Uint32Array(96)) - randomBytes := make([]byte, 96) - _, err := rand.Read(randomBytes) - if err != nil { - return "", err - } - return base64urlEncode(randomBytes), nil -} - -// createCodeChallenge creates a SHA-256 hash of the code verifier -func createCodeChallenge(codeVerifier string) (string, error) { - hashed := sha256.Sum256([]byte(codeVerifier)) - return base64urlEncode(hashed[:]), nil -} - -// base64urlEncode encodes bytes to URL-safe base64 -func base64urlEncode(data []byte) string { - encoded := base64.StdEncoding.EncodeToString(data) - // Replace characters for URL-safe base64 - encoded = strings.ReplaceAll(encoded, "+", "-") - encoded = strings.ReplaceAll(encoded, "/", "_") - // Remove padding - encoded = strings.TrimRight(encoded, "=") - return encoded -} diff --git a/cli/internal/workos/client.go b/cli/internal/workos/http/client.go similarity index 92% rename from cli/internal/workos/client.go rename to cli/internal/workos/http/client.go index 665770d31..fa2b7f422 100644 --- a/cli/internal/workos/client.go +++ b/cli/internal/workos/http/client.go @@ -1,10 +1,13 @@ -package workos +package http import ( "bytes" + "crypto/rsa" + "encoding/base64" "encoding/json" "fmt" "io" + "math/big" "net/http" "net/url" "path" @@ -147,6 +150,35 @@ func WithScheme(scheme string) ClientOption { } } +func jwkToRSAPublicKey(jwk JWK) (*rsa.PublicKey, error) { + // Decode the modulus (n) + nBytes, err := base64.RawURLEncoding.DecodeString(jwk.N) + if err != nil { + return nil, err + } + + // Decode the exponent (e) + eBytes, err := base64.RawURLEncoding.DecodeString(jwk.E) + if err != nil { + return nil, err + } + + // Create the RSA public key + return &rsa.PublicKey{ + N: new(big.Int).SetBytes(nBytes), + E: int(new(big.Int).SetBytes(eBytes).Int64()), + }, nil +} + +// GetRSAPublicKey gets the RSA public key for a given JWT kid +func (h *HttpClient) GetRSAPublicKey(kid string) (*rsa.PublicKey, error) { + jwk, err := h.GetJWK(kid) + if err != nil { + return nil, err + } + return jwkToRSAPublicKey(jwk) +} + func (h *HttpClient) GetJWK(kid string) (JWK, error) { jwks, err := h.GetJWKs() if err != nil { diff --git a/cli/internal/workos/keyring.go b/cli/internal/workos/keyring.go new file mode 100644 index 000000000..3898e9688 --- /dev/null +++ b/cli/internal/workos/keyring.go @@ -0,0 +1,65 @@ +package workos + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + + "github.com/nitrictech/nitric/cli/internal/config" + "github.com/zalando/go-keyring" +) + +// Store 1 token per API +var WORKOS_TOKEN_KEY = getWorkosTokenKey(config.GetNitricServerUrl().String()) + +func getWorkosTokenKey(apiUrl string) string { + // Hash the API URL for a consistent length. We don't use the scheme or host, just the path + hash := sha256.Sum256([]byte(apiUrl + ".workos")) + return fmt.Sprintf("%x", hash) +} + +type KeyringTokenStore struct { + service string +} + +func NewKeyringTokenStore(service string) *KeyringTokenStore { + return &KeyringTokenStore{service: service} +} + +func (s *KeyringTokenStore) GetTokens() (*Tokens, error) { + token, err := keyring.Get(s.service, WORKOS_TOKEN_KEY) + if err != nil { + if err == keyring.ErrNotFound { + return nil, ErrNotFound + } + return nil, fmt.Errorf("failed to retrieve token from keyring: %w", err) + } + + var tokens Tokens + err = json.Unmarshal([]byte(token), &tokens) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal token: %w", err) + } + + return &tokens, nil +} + +func (s *KeyringTokenStore) SaveTokens(tokens *Tokens) error { + json, err := json.Marshal(tokens) + if err != nil { + return fmt.Errorf("failed to marshal token: %w", err) + } + + return keyring.Set(s.service, WORKOS_TOKEN_KEY, string(json)) +} + +func (s *KeyringTokenStore) Clear() error { + err := keyring.Delete(s.service, WORKOS_TOKEN_KEY) + if err != nil { + if err == keyring.ErrNotFound { + return ErrNotFound + } + return fmt.Errorf("failed to delete token from keyring: %w", err) + } + return nil +} diff --git a/cli/internal/auth/login_success.html b/cli/internal/workos/login_success.html similarity index 100% rename from cli/internal/auth/login_success.html rename to cli/internal/workos/login_success.html diff --git a/cli/internal/workos/pkce.go b/cli/internal/workos/pkce.go new file mode 100644 index 000000000..6917f10d0 --- /dev/null +++ b/cli/internal/workos/pkce.go @@ -0,0 +1,186 @@ +package workos + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "net" + "net/http" + "strings" + "time" + + _ "embed" + + "github.com/nitrictech/nitric/cli/internal/style" + "github.com/nitrictech/nitric/cli/internal/style/icons" + workos_http "github.com/nitrictech/nitric/cli/internal/workos/http" + "github.com/pkg/browser" +) + +//go:embed login_success.html +var loginSuccessPage []byte + +// The port that the local auth callback server will listen on. +// This is used to handle the callback from the WorkOS auth provider. +var LOCAL_PKCE_CALLBACK_PORT = 48321 + +// ErrPortNotAvailable is returned when the local auth callback port is not available. +var ErrPortNotAvailable = fmt.Errorf("port %d is not available, unable to start local auth callback server", LOCAL_PKCE_CALLBACK_PORT) + +func (a *WorkOSAuth) performPKCE() error { + // Check if the callback port is available and create a listener + listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", LOCAL_PKCE_CALLBACK_PORT)) + if err != nil { + return ErrPortNotAvailable + } + defer listener.Close() + + pkceChallenge, err := createPkceChallenge() + if err != nil { + return err + } + + callbackResult := make(chan CallbackResult) + + callbackServer := &http.Server{ + // We only bind to loopback for security + // The users own browser is the only client that should connect to this server, during a redirect + Handler: http.HandlerFunc(newCallbackHandler(callbackResult, a.httpClient, pkceChallenge)), + } + + go callbackServer.Serve(listener) + defer func() { + shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancelShutdown() + + callbackServer.Shutdown(shutdownCtx) + }() + + authUrl, err := a.httpClient.GetAuthorizationUrl(workos_http.GetAuthorizationUrlOptions{ + Provider: "authkit", + RedirectURI: fmt.Sprintf("http://127.0.0.1:%d/callback", LOCAL_PKCE_CALLBACK_PORT), + CodeChallenge: pkceChallenge.Challenge, + CodeChallengeMethod: "S256", + }) + if err != nil { + return err + } + + fmt.Printf("\nOpening browser to %s\n", style.Gray(authUrl)) + + // Open the browser + err = browser.OpenURL(authUrl) + if err != nil { + return err + } + + // Wait for the callback to be received or the server to shutdown + result := <-callbackResult + if result.Error != nil { + fmt.Printf("\n%s Login failed due to an error: %s\n", style.Red(icons.Cross), result.Error) + return result.Error + } + + close(callbackResult) + + a.tokens = result.Tokens + err = a.tokenStore.SaveTokens(a.tokens) + if err != nil { + fmt.Printf("\n%s Error saving tokens: %s\n", style.Red(icons.Cross), err) + return err + } + + return nil +} + +func newCallbackHandler(callbackResult chan CallbackResult, client *workos_http.HttpClient, pkceChallenge *CodeVerifier) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + + if code == "" { + // TODO: Handle favicon.ico requests + w.WriteHeader(http.StatusBadRequest) + return + } + + res, err := client.AuthenticateWithCode(code, pkceChallenge.Verifier) + if err != nil { + // TODO: make this pretty + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + + callbackResult <- CallbackResult{Error: err} + return + } + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write(loginSuccessPage) + + go func() { + callbackResult <- CallbackResult{Tokens: &Tokens{ + AccessToken: res.AccessToken, + RefreshToken: res.RefreshToken, + User: &res.User, + }} + }() + + fmt.Println("Callback received") + } +} + +type CallbackResult struct { + Tokens *Tokens + Error error +} + +type CodeVerifier struct { + Verifier string + Challenge string +} + +// CreatePkceChallenge generates both a code verifier and code challenge for PKCE +func createPkceChallenge() (*CodeVerifier, error) { + codeVerifier, err := createCodeVerifier() + if err != nil { + return nil, err + } + codeChallenge, err := createCodeChallenge(codeVerifier) + if err != nil { + return nil, err + } + return &CodeVerifier{ + Verifier: codeVerifier, + Challenge: codeChallenge, + }, nil +} + +// createCodeVerifier generates a random code verifier +func createCodeVerifier() (string, error) { + // Generate 96 bytes (equivalent to 96 * 4 = 384 bits from Uint32Array(96)) + randomBytes := make([]byte, 96) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + return base64urlEncode(randomBytes), nil +} + +// createCodeChallenge creates a SHA-256 hash of the code verifier +func createCodeChallenge(codeVerifier string) (string, error) { + hashed := sha256.Sum256([]byte(codeVerifier)) + return base64urlEncode(hashed[:]), nil +} + +// base64urlEncode encodes bytes to URL-safe base64 +func base64urlEncode(data []byte) string { + encoded := base64.StdEncoding.EncodeToString(data) + // Replace characters for URL-safe base64 + encoded = strings.ReplaceAll(encoded, "+", "-") + encoded = strings.ReplaceAll(encoded, "/", "_") + // Remove padding + encoded = strings.TrimRight(encoded, "=") + return encoded +} diff --git a/cli/internal/workos/workos.go b/cli/internal/workos/workos.go new file mode 100644 index 000000000..b7fe1d810 --- /dev/null +++ b/cli/internal/workos/workos.go @@ -0,0 +1,113 @@ +package workos + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/nitrictech/nitric/cli/internal/workos/http" +) + +var ( + ErrNotFound = errors.New("no token found") + ErrUnauthenticated = errors.New("unauthenticated") +) + +type TokenStore interface { + GetTokens() (*Tokens, error) + SaveTokens(*Tokens) error + Clear() error +} + +type Tokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + User *http.User `json:"user"` +} + +type WorkOSAuth struct { + tokenStore TokenStore + tokens *Tokens + httpClient *http.HttpClient +} + +func NewWorkOSAuth(tokenStore TokenStore, clientID string, endpoint string) *WorkOSAuth { + httpClient := http.NewHttpClient(clientID, http.WithHostname(endpoint)) + + return &WorkOSAuth{tokenStore: tokenStore, httpClient: httpClient} +} + +func (a *WorkOSAuth) Login() (*http.User, error) { + err := a.performPKCE() + if err != nil { + return nil, err + } + + return a.tokens.User, nil +} + +func (a *WorkOSAuth) GetAccessToken() (string, error) { + + if a.tokens == nil { + tokens, err := a.tokenStore.GetTokens() + if err != nil { + return "", fmt.Errorf("no stored tokens found, please login: %w", err) + } + a.tokens = tokens + } + + // Decode the JWT to check if it's expired + claims := jwt.RegisteredClaims{} + parsedToken, err := jwt.ParseWithClaims(a.tokens.AccessToken, &claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, fmt.Errorf("kid not found in token header") + } + + return a.httpClient.GetRSAPublicKey(kid) + }, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})) + + // Add a 1 second buffer to the expiry time to account for a slight delay in the token being sent to the server + // i.e. the token must remain valid for at least another second, or we'll refresh it early for good measure + if err != nil || !parsedToken.Valid || claims.ExpiresAt.Before(time.Now().Add(1+time.Second)) { + if err := a.refreshToken(); err != nil { + return "", fmt.Errorf("token refresh failed: %w", err) + } + } + + return a.tokens.AccessToken, nil +} + +func (a *WorkOSAuth) refreshToken() error { + if a.tokens.RefreshToken == "" { + return fmt.Errorf("no refresh token", ErrUnauthenticated) + } + + workosToken, err := a.httpClient.AuthenticateWithRefreshToken(a.tokens.RefreshToken, nil) + if err != nil { + return err + } + + a.tokens = &Tokens{ + AccessToken: workosToken.AccessToken, + RefreshToken: workosToken.RefreshToken, + User: &workosToken.User, + } + + err = a.tokenStore.SaveTokens(a.tokens) + if err != nil { + return err + } + + return nil +} + +func (a *WorkOSAuth) Logout() error { + a.tokens = nil + return a.tokenStore.Clear() +} From 13708d6f9ee2d6289aee7dd9e6e815351defb58e Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Wed, 9 Jul 2025 18:03:22 +1000 Subject: [PATCH 3/8] bring back login styled output --- cli/cmd/login.go | 4 +++- cli/internal/workos/workos.go | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cli/cmd/login.go b/cli/cmd/login.go index 5eeda11b8..2053115f6 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -14,13 +14,15 @@ func NewLoginCmd(deps *Dependencies) *cobra.Command { Short: "Login to Nitric", Long: `Login to the Nitric CLI.`, Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("\n%s Logging in...\n", style.Purple(icons.Lightning+" Nitric")) + user, err := deps.WorkOSAuth.Login() if err != nil { fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) return } - fmt.Printf("\n%s Login successful, welcome %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) + fmt.Printf("\n%s Logged in as %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) }, } diff --git a/cli/internal/workos/workos.go b/cli/internal/workos/workos.go index b7fe1d810..056b42678 100644 --- a/cli/internal/workos/workos.go +++ b/cli/internal/workos/workos.go @@ -15,8 +15,11 @@ var ( ) type TokenStore interface { + // GetTokens returns the tokens from the store, or nil if no tokens are found GetTokens() (*Tokens, error) + // SaveTokens saves the tokens to the store SaveTokens(*Tokens) error + // Clear clears the tokens from the store Clear() error } @@ -35,10 +38,16 @@ type WorkOSAuth struct { func NewWorkOSAuth(tokenStore TokenStore, clientID string, endpoint string) *WorkOSAuth { httpClient := http.NewHttpClient(clientID, http.WithHostname(endpoint)) - return &WorkOSAuth{tokenStore: tokenStore, httpClient: httpClient} + tokens, _ := tokenStore.GetTokens() + + return &WorkOSAuth{tokenStore: tokenStore, httpClient: httpClient, tokens: tokens} } func (a *WorkOSAuth) Login() (*http.User, error) { + if a.tokens != nil { + return a.tokens.User, nil + } + err := a.performPKCE() if err != nil { return nil, err From 955405decebe19c45ec6271d8497c86d9b642681 Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Wed, 9 Jul 2025 18:17:13 +1000 Subject: [PATCH 4/8] handle unauthenticated requests for templates --- cli/cmd/new.go | 26 +++++++++------- cli/cmd/root.go | 4 ++- cli/cmd/templates.go | 9 +++++- cli/internal/api/api.go | 33 ++++++++++++++++----- cli/internal/api/auth.go | 2 +- cli/internal/api/errors.go | 6 ++++ cli/internal/api/plugin.go | 2 +- cli/internal/api/templates.go | 8 ++--- cli/internal/api/tokens.go | 12 -------- cli/internal/api/transformer/transformer.go | 5 ---- 10 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 cli/internal/api/errors.go delete mode 100644 cli/internal/api/tokens.go delete mode 100644 cli/internal/api/transformer/transformer.go diff --git a/cli/cmd/new.go b/cli/cmd/new.go index 1689c53b2..4a75b1c65 100644 --- a/cli/cmd/new.go +++ b/cli/cmd/new.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hashicorp/go-getter" + "github.com/nitrictech/nitric/cli/internal/api" "github.com/nitrictech/nitric/cli/internal/style/colors" "github.com/nitrictech/nitric/cli/pkg/files" "github.com/nitrictech/nitric/cli/pkg/schema" @@ -46,6 +47,17 @@ func NewNewCmd(deps *Dependencies) *cobra.Command { return } + templates, err := deps.NitricApiClient.GetTemplates() + if err != nil { + if errors.Is(err, api.ErrUnauthenticated) { + fmt.Println("Please login first, using the `login` command") + return + } + + fmt.Printf("Failed to get templates: %v", err) + return + } + fs := afero.NewOsFs() projectName := "" @@ -88,19 +100,13 @@ func NewNewCmd(deps *Dependencies) *cobra.Command { } } - resp, err := deps.NitricApiClient.GetTemplates() - if err != nil { - fmt.Printf("Failed to get templates: %v", err) - return - } - - if len(resp) == 0 { + if len(templates) == 0 { fmt.Println("No templates found") return } - templateNames := make([]string, len(resp)) - for i, template := range resp { + templateNames := make([]string, len(templates)) + for i, template := range templates { templateNames[i] = template.String() } @@ -111,7 +117,7 @@ func NewNewCmd(deps *Dependencies) *cobra.Command { return } - template, err := deps.NitricApiClient.GetTemplate(resp[index].TeamSlug, resp[index].Slug, "") + template, err := deps.NitricApiClient.GetTemplate(templates[index].TeamSlug, templates[index].Slug, "") cobra.CheckErr(err) // Find home directory. diff --git a/cli/cmd/root.go b/cli/cmd/root.go index bc3b737f8..2978d210d 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -44,11 +44,12 @@ func init() { rootCmd.AddCommand(NewAccessTokenCmd(deps)) rootCmd.AddCommand(NewTemplatesCmd(deps)) + rootCmd.AddCommand(NewNewCmd(deps)) rootCmd.AddCommand(NewBuildCmd(deps)) } func initDependencies() { - deps.NitricApiClient = api.NewNitricApiClient(config.GetNitricServerUrl()) + deps.NitricApiClient = api.NewNitricApiClient(config.GetNitricServerUrl(), nil) workosDetails, err := deps.NitricApiClient.GetWorkOSPublicDetails() if err != nil { @@ -60,6 +61,7 @@ func initDependencies() { } deps.WorkOSAuth = workos.NewWorkOSAuth(workos.NewKeyringTokenStore("nitric.v2.cli"), workosDetails.ClientID, workosDetails.ApiHostname) + deps.NitricApiClient.SetTokenProvider(deps.WorkOSAuth) } // initConfig reads in config file and ENV variables if set. diff --git a/cli/cmd/templates.go b/cli/cmd/templates.go index 7632c1e0c..7846957ff 100644 --- a/cli/cmd/templates.go +++ b/cli/cmd/templates.go @@ -1,9 +1,11 @@ package cmd import ( + "errors" "fmt" "github.com/charmbracelet/lipgloss" + "github.com/nitrictech/nitric/cli/internal/api" "github.com/nitrictech/nitric/cli/internal/style/colors" "github.com/spf13/cobra" ) @@ -22,6 +24,11 @@ func NewTemplatesCmd(deps *Dependencies) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { resp, err := deps.NitricApiClient.GetTemplates() if err != nil { + if errors.Is(err, api.ErrUnauthenticated) { + fmt.Println("Please login first, using the `login` command") + return + } + fmt.Printf("Failed to get templates: %v", err) return } @@ -36,7 +43,7 @@ func NewTemplatesCmd(deps *Dependencies) *cobra.Command { fmt.Println(templateStyle.Render("\nAvailable templates:")) for _, template := range resp { - fmt.Printf(" %s\n", template) + fmt.Printf(" %s\n", template.String()) } }, } diff --git a/cli/internal/api/api.go b/cli/internal/api/api.go index 36ebc41f4..0f02b51dc 100644 --- a/cli/internal/api/api.go +++ b/cli/internal/api/api.go @@ -1,23 +1,35 @@ package api import ( + "fmt" "net/http" "net/url" - "github.com/nitrictech/nitric/cli/internal/api/transformer" + "github.com/pkg/errors" ) +type TokenProvider interface { + // GetAccessToken returns the access token for the user + GetAccessToken() (string, error) +} + type NitricApiClient struct { - apiUrl *url.URL + tokenProvider TokenProvider + apiUrl *url.URL } -func NewNitricApiClient(apiUrl *url.URL) *NitricApiClient { +func NewNitricApiClient(apiUrl *url.URL, tokenProvider TokenProvider) *NitricApiClient { return &NitricApiClient{ - apiUrl: apiUrl, + apiUrl: apiUrl, + tokenProvider: tokenProvider, } } -func (c *NitricApiClient) get(path string, transformers ...transformer.RequestTransformer) (*http.Response, error) { +func (c *NitricApiClient) SetTokenProvider(tokenProvider TokenProvider) { + c.tokenProvider = tokenProvider +} + +func (c *NitricApiClient) get(path string, requiresAuth bool) (*http.Response, error) { apiUrl, err := url.JoinPath(c.apiUrl.String(), path) if err != nil { return nil, err @@ -30,11 +42,16 @@ func (c *NitricApiClient) get(path string, transformers ...transformer.RequestTr req.Header.Set("Accept", "application/json") - for _, transformer := range transformers { - err := transformer(req) + if requiresAuth { + if c.tokenProvider == nil { + return nil, errors.Wrap(ErrPreconditionFailed, "no token provider provided") + } + + token, err := c.tokenProvider.GetAccessToken() if err != nil { - return nil, err + return nil, errors.Wrap(ErrUnauthenticated, err.Error()) } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } return http.DefaultClient.Do(req) diff --git a/cli/internal/api/auth.go b/cli/internal/api/auth.go index 8b3aff3fc..25a56c191 100644 --- a/cli/internal/api/auth.go +++ b/cli/internal/api/auth.go @@ -16,7 +16,7 @@ type WorkOSDetails struct { } func (c *NitricApiClient) GetWorkOSPublicDetails() (*WorkOSDetails, error) { - response, err := c.get("/auth/details") + response, err := c.get("/auth/details", false) if err != nil { return nil, fmt.Errorf("failed to connect to nitric auth details endpoint: %v", err) } diff --git a/cli/internal/api/errors.go b/cli/internal/api/errors.go new file mode 100644 index 000000000..c38c5130d --- /dev/null +++ b/cli/internal/api/errors.go @@ -0,0 +1,6 @@ +package api + +import "errors" + +var ErrUnauthenticated = errors.New("unauthenticated") +var ErrPreconditionFailed = errors.New("precondition failed") diff --git a/cli/internal/api/plugin.go b/cli/internal/api/plugin.go index 73e7fbb77..09b48de3b 100644 --- a/cli/internal/api/plugin.go +++ b/cli/internal/api/plugin.go @@ -11,7 +11,7 @@ import ( // FIXME: Because of the difference in fields between identity and resource plugins we need to return an interface func (c *NitricApiClient) GetPluginManifest(team, lib, version, name string) (interface{}, error) { fmt.Println("Getting plugin manifest for", team, lib, version, name) - response, err := c.get(fmt.Sprintf("/api/plugin_libraries/%s/%s/versions/%s/plugins/%s", team, lib, version, name)) + response, err := c.get(fmt.Sprintf("/api/plugin_libraries/%s/%s/versions/%s/plugins/%s", team, lib, version, name), true) if err != nil { return nil, fmt.Errorf("failed to connect to nitric auth details endpoint: %v", err) } diff --git a/cli/internal/api/templates.go b/cli/internal/api/templates.go index 77bb10e94..96d4a6625 100644 --- a/cli/internal/api/templates.go +++ b/cli/internal/api/templates.go @@ -7,7 +7,7 @@ import ( ) func (c *NitricApiClient) GetTemplates() ([]Template, error) { - response, err := c.get("/api/templates") + response, err := c.get("/api/templates", true) if err != nil { return nil, err } @@ -37,13 +37,13 @@ func (c *NitricApiClient) GetTemplate(teamSlug string, templateName string, vers // latest version URL is /api/templates/{teamSlug}/{templateName} // specific version URL is /api/templates/{teamSlug}/{templateName}/v/{version} - url := fmt.Sprintf("/api/templates/%s/%s", teamSlug, templateName) + templatePath := fmt.Sprintf("/api/templates/%s/%s", teamSlug, templateName) if version != "" { - url = fmt.Sprintf("%s/v/%s", url, version) + templatePath = fmt.Sprintf("%s/v/%s", templatePath, version) } - response, err := c.get(url) + response, err := c.get(templatePath, true) if err != nil { return nil, err } diff --git a/cli/internal/api/tokens.go b/cli/internal/api/tokens.go deleted file mode 100644 index 02f1d2245..000000000 --- a/cli/internal/api/tokens.go +++ /dev/null @@ -1,12 +0,0 @@ -package api - -type TokenProvider interface { - GetTokens() (*Tokens, error) - SaveTokens(*Tokens) error -} - -type Tokens struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresAt int64 `json:"expires_at"` -} diff --git a/cli/internal/api/transformer/transformer.go b/cli/internal/api/transformer/transformer.go deleted file mode 100644 index 968c4ec03..000000000 --- a/cli/internal/api/transformer/transformer.go +++ /dev/null @@ -1,5 +0,0 @@ -package transformer - -import "net/http" - -type RequestTransformer func(req *http.Request) error From 646601386c3032b6894a13e184c141ddc1a35acf Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 10 Jul 2025 08:11:28 +1000 Subject: [PATCH 5/8] remove debug log --- cli/internal/workos/pkce.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cli/internal/workos/pkce.go b/cli/internal/workos/pkce.go index 6917f10d0..7e114f6d5 100644 --- a/cli/internal/workos/pkce.go +++ b/cli/internal/workos/pkce.go @@ -126,8 +126,6 @@ func newCallbackHandler(callbackResult chan CallbackResult, client *workos_http. User: &res.User, }} }() - - fmt.Println("Callback received") } } From e66baaddc43a2291228925cc6db9340983d49fba Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 10 Jul 2025 14:44:34 +1000 Subject: [PATCH 6/8] refactor to DI To enable more granular testing --- cli/cmd/accesstoken.go | 26 -- cli/cmd/auth.go | 46 ++++ cli/cmd/build.go | 61 ----- cli/cmd/config.go | 83 +++--- cli/cmd/dev.go | 70 ------ cli/cmd/edit.go | 78 ------ cli/cmd/generate.go | 90 ------- cli/cmd/login.go | 30 --- cli/cmd/logout.go | 28 --- cli/cmd/new.go | 197 --------------- cli/cmd/nitric.go | 115 +++++++++ cli/cmd/root.go | 89 +++---- cli/cmd/templates.go | 54 ---- cli/cmd/version.go | 25 -- cli/go.mod | 1 + cli/go.sum | 42 ++++ cli/internal/api/api.go | 26 +- cli/internal/config/config.go | 122 +++++---- cli/internal/workos/keyring.go | 33 ++- cli/main.go | 44 +++- cli/pkg/cli/cli.go | 448 +++++++++++++++++++++++++++++++++ 21 files changed, 869 insertions(+), 839 deletions(-) delete mode 100644 cli/cmd/accesstoken.go create mode 100644 cli/cmd/auth.go delete mode 100644 cli/cmd/build.go delete mode 100644 cli/cmd/dev.go delete mode 100644 cli/cmd/edit.go delete mode 100644 cli/cmd/generate.go delete mode 100644 cli/cmd/login.go delete mode 100644 cli/cmd/logout.go delete mode 100644 cli/cmd/new.go create mode 100644 cli/cmd/nitric.go delete mode 100644 cli/cmd/templates.go delete mode 100644 cli/cmd/version.go create mode 100644 cli/pkg/cli/cli.go diff --git a/cli/cmd/accesstoken.go b/cli/cmd/accesstoken.go deleted file mode 100644 index ce219b065..000000000 --- a/cli/cmd/accesstoken.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -func NewAccessTokenCmd(deps *Dependencies) *cobra.Command { - var accessTokenCmd = &cobra.Command{ - Use: "access-token", - Short: "Print an access token for the Nitric Platform", - Long: `Print an access token for the Nitric Platform, using the current login session.`, - Run: func(cmd *cobra.Command, args []string) { - token, err := deps.WorkOSAuth.GetAccessToken() - if err != nil { - fmt.Printf("\n Not currently logged in, run `nitric login` to login") - return - } - - fmt.Println(token) - }, - } - - return accessTokenCmd -} diff --git a/cli/cmd/auth.go b/cli/cmd/auth.go new file mode 100644 index 000000000..61cf19ec9 --- /dev/null +++ b/cli/cmd/auth.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "github.com/nitrictech/nitric/cli/pkg/cli" + "github.com/samber/do" + "github.com/spf13/cobra" +) + +// NewLoginCmd creates the login command +func NewLoginCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "login", + Short: "Login to Nitric", + Long: `Login to the Nitric CLI.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Login()) + }, + } +} + +// NewLogoutCmd creates the logout command +func NewLogoutCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "logout", + Short: "Logout from Nitric", + Long: `Logout from the Nitric CLI.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Logout()) + }, + } +} + +// NewAccessTokenCmd creates the access token command +func NewAccessTokenCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "access-token", + Short: "Get access token", + Long: `Get the current access token.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.AccessToken()) + }, + } +} diff --git a/cli/cmd/build.go b/cli/cmd/build.go deleted file mode 100644 index 08bb0116f..000000000 --- a/cli/cmd/build.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/nitrictech/nitric/cli/internal/plugins" - "github.com/nitrictech/nitric/cli/pkg/schema" - "github.com/nitrictech/nitric/engines/terraform" - "github.com/spf13/afero" - "github.com/spf13/cobra" -) - -type MockTerraformPluginRepository struct { - plugins map[string]*terraform.PluginManifest -} - -func (r *MockTerraformPluginRepository) GetPlugin(name string) (*terraform.PluginManifest, error) { - return r.plugins[name], nil -} - -func NewBuildCmd(deps *Dependencies) *cobra.Command { - var buildCmd = &cobra.Command{ - Use: "build", - Short: "Builds the nitric application", - Long: `Builds an application using the nitric.yaml application spec and referenced platform.`, - Run: func(cmd *cobra.Command, args []string) { - - // Read the nitric.yaml file - fs := afero.NewOsFs() - - appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) - cobra.CheckErr(err) - - mockPlatformRepository := terraform.NewMockPlatformRepository() - - // TODO:prompt for platform selection if multiple targets are specified - targetPlatform := appSpec.Targets[0] - - platform, err := terraform.PlatformFromId(fs, targetPlatform, mockPlatformRepository) - cobra.CheckErr(err) - - repo := plugins.NewPluginRepository(deps.NitricApiClient) - engine := terraform.New(platform, terraform.WithRepository(repo)) - // Parse the application spec - // Validate the application spec - // Build the application using the specified platform - // Handle any errors that occur during the build process - - err = engine.Apply(appSpec) - if err != nil { - fmt.Print("Error applying platform: ", err) - return - } - - fmt.Println("Build completed successfully.") - - }, - } - - return buildCmd -} diff --git a/cli/cmd/config.go b/cli/cmd/config.go index 2e949cec4..3676f0886 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -3,56 +3,55 @@ package cmd import ( "fmt" + "github.com/charmbracelet/lipgloss" "github.com/nitrictech/nitric/cli/internal/config" "github.com/nitrictech/nitric/cli/internal/style" + "github.com/nitrictech/nitric/cli/internal/style/colors" + "github.com/samber/do" "github.com/spf13/cobra" - "github.com/spf13/viper" ) -var configSetCmd = &cobra.Command{ - Use: "set ", - Short: "Set the CLI configuration", - Long: `Set the CLI configuration.`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - key := args[0] - value := args[1] - - if err := config.SetValue(key, value); err != nil { - fmt.Printf("Error setting config: %v\n", err) - } - - if err := config.Save(); err != nil { - fmt.Printf("Error saving config: %v\n", err) - } - - fmt.Printf("Config set: %s: %s\n", key, value) - }, -} - -var configCmd = &cobra.Command{ - Use: "config", - Short: "Manage CLI configuration", - Long: `Manage the CLI configuration.`, - Run: func(cmd *cobra.Command, args []string) { - if viper.ConfigFileUsed() != "" { - fmt.Printf("using config file: %s\n", style.Teal(viper.ConfigFileUsed())) - items := config.GetAllConfigItems() - - if len(items) > 0 { - fmt.Println("") +// NewConfigCmd creates the config command +func NewConfigCmd(injector *do.Injector) *cobra.Command { + configCmd := &cobra.Command{ + Use: "config", + Short: "Manage CLI configuration", + Long: `Manage the CLI configuration.`, + Run: func(cmd *cobra.Command, args []string) { + conf := do.MustInvoke[*config.Config](injector) + + if conf.FileUsed() != "" { + fmt.Printf("file: %s\n\n", style.Teal(conf.FileUsed())) + fmt.Println(lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true, false, false, false).BorderForeground(colors.Teal).Render(conf.Dump())) + } else { + fmt.Println("file: none (using defaults)") + } + }, + } + + configSetCmd := &cobra.Command{ + Use: "set ", + Short: "Set the CLI configuration", + Long: `Set the CLI configuration.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + key := args[0] + value := args[1] + + conf := do.MustInvoke[*config.Config](injector) + + if err := conf.SetValue(key, value); err != nil { + fmt.Printf("error setting config: %v", err) } - for key, value := range items { - fmt.Printf("%s: %s\n", style.Teal(key), value) + if err := conf.Save(); err != nil { + fmt.Printf("error saving config: %v", err) } - } else { - fmt.Println("config file: none (using defaults)") - } - }, -} -func init() { + fmt.Printf("Config set: %s: %s\n", key, value) + }, + } + configCmd.AddCommand(configSetCmd) - rootCmd.AddCommand(configCmd) + return configCmd } diff --git a/cli/cmd/dev.go b/cli/cmd/dev.go deleted file mode 100644 index 80571b9f1..000000000 --- a/cli/cmd/dev.go +++ /dev/null @@ -1,70 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "strings" - - "github.com/nitrictech/nitric/cli/internal/simulation" - "github.com/nitrictech/nitric/cli/pkg/schema" - "github.com/spf13/afero" - "github.com/spf13/cobra" -) - -type PrefixWriter struct { - writer io.Writer - prefix string -} - -func (p *PrefixWriter) Write(content []byte) (int, error) { - value := strings.TrimSuffix(string(content), "\n") - - split := strings.Split(value, "\n") - value = strings.Join(split, "\n"+p.prefix) + "\n" - - _, err := fmt.Fprintf(p.writer, "%s%s", p.prefix, value) - if err != nil { - return 0, err - } - - return len(content), nil -} - -func NewPrefixWriter(prefix string, writer io.Writer) *PrefixWriter { - return &PrefixWriter{ - prefix: prefix, - writer: writer, - } -} - -var dev = &cobra.Command{ - Use: "dev", - Short: "Run the Nitric application in development mode", - Long: `Run the Nitric application in development mode, allowing local testing of resources.`, - Run: func(cmd *cobra.Command, args []string) { - // TODO: Extract common loading logic into a separate function - // (see build.go for common loading logic) - - // 1. Load the App Spec - // Read the nitric.yaml file - fs := afero.NewOsFs() - - appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) - cobra.CheckErr(err) - - simserver := simulation.NewSimulationServer(fs, appSpec) - err = simserver.Start(os.Stdout) - cobra.CheckErr(err) - }, -} - -func init() { - // Add the dev command to the root command - rootCmd.AddCommand(dev) - - // Add flags for the dev command if needed - // e.g., dev.Flags().StringVarP(&flagName, "flag", "f", "defaultValue", "Description of the flag") -} - -// diff --git a/cli/cmd/edit.go b/cli/cmd/edit.go deleted file mode 100644 index 594ad4503..000000000 --- a/cli/cmd/edit.go +++ /dev/null @@ -1,78 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "net" - "strconv" - "time" - - "github.com/nitrictech/nitric/cli/internal/browser" - "github.com/nitrictech/nitric/cli/internal/config" - "github.com/nitrictech/nitric/cli/internal/devserver" - "github.com/nitrictech/nitric/cli/pkg/tui" - "github.com/spf13/cobra" -) - -const fileName = "nitric.yaml" - -var editCmd = &cobra.Command{ - Use: "edit", - Short: "Edit the nitric application", - Long: `Edits an application using the nitric.yaml application spec and referenced platform.`, - Run: func(cmd *cobra.Command, args []string) { - - listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - log.Printf("Error listening: %v", err) - } - - devwsServer := devserver.NewDevWebsocketServer(devserver.WithListener(listener)) - fileSync, err := devserver.NewFileSync(fileName, devwsServer.Broadcast, devserver.WithDebounce(time.Millisecond*100)) - cobra.CheckErr(err) - defer fileSync.Close() - - // subscribe the file sync to the websocket server - devwsServer.Subscribe(fileSync) - - port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) - - // Open browser tab to the dashboard - devUrl := config.GetNitricServerUrl().JoinPath("dev") - q := devUrl.Query() - q.Add("port", port) - devUrl.RawQuery = q.Encode() - - fmt.Println(tui.NitricIntro("Sync Port", port, "Dashboard", devUrl.String())) - - // Start the WebSocket server - errChan := make(chan error) - go func(errChan chan error) { - err := devwsServer.Start() - if err != nil { - errChan <- err - } - }(errChan) - - go func() { - err = fileSync.Start() - if err != nil { - log.Printf("Error starting file sync: %v", err) - } - }() - - fmt.Println("Opening browser to the editor") - - err = browser.Open(devUrl.String()) - if err != nil { - log.Printf("Error opening browser: %v", err) - } - - // Wait for the file watcher to fail/return - cobra.CheckErr(<-errChan) - }, -} - -func init() { - rootCmd.AddCommand(editCmd) -} diff --git a/cli/cmd/generate.go b/cli/cmd/generate.go deleted file mode 100644 index 82035edf0..000000000 --- a/cli/cmd/generate.go +++ /dev/null @@ -1,90 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/nitrictech/nitric/cli/pkg/client" - "github.com/nitrictech/nitric/cli/pkg/schema" - "github.com/spf13/afero" - "github.com/spf13/cobra" -) - -var ( - goFlag bool - pythonFlag bool - javascriptFlag bool - typescriptFlag bool - - goOutputDir string - goPackageName string - pythonOutputDir string - javascriptOutputDir string - typescriptOutputDir string -) - -var generateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate client libraries", - Long: `Generate client libraries for different programming languages based on the Nitric application specification.`, - Run: func(cmd *cobra.Command, args []string) { - // Check if at least one language flag is provided - if !goFlag && !pythonFlag && !javascriptFlag && !typescriptFlag { - fmt.Println("Error: at least one language flag must be specified") - cmd.Help() - return - } - - fs := afero.NewOsFs() - - appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) - if err != nil { - fmt.Println(err) - return - } - - if !client.SpecHasClientResources(*appSpec) { - fmt.Println("No client compatible resources found in application, skipping client generation") - return - } - - // check if the go language flag is provided - if goFlag { - fmt.Println("Generating Go client...") - // TODO: add flags for output directory and package name - err = client.GenerateGo(fs, *appSpec, goOutputDir, goPackageName) - cobra.CheckErr(err) - } - - if pythonFlag { - fmt.Println("Generating Python client...") - err = client.GeneratePython(fs, *appSpec, pythonOutputDir) - cobra.CheckErr(err) - } - - if typescriptFlag { - fmt.Println("Generating NodeJS client...") - err = client.GenerateTypeScript(fs, *appSpec, typescriptOutputDir) - cobra.CheckErr(err) - } - - fmt.Println("Clients generated successfully.") - }, -} - -func init() { - rootCmd.AddCommand(generateCmd) - - // Add language flags - generateCmd.Flags().BoolVar(&goFlag, "go", false, "Generate Go client") - generateCmd.Flags().StringVar(&goOutputDir, "go-out", "", "Output directory for Go client") - generateCmd.Flags().StringVar(&goPackageName, "go-package-name", "", "Package name for Go client") - - generateCmd.Flags().BoolVar(&pythonFlag, "python", false, "Generate Python client") - generateCmd.Flags().StringVar(&pythonOutputDir, "python-out", "", "Output directory for Python client") - - generateCmd.Flags().BoolVar(&javascriptFlag, "js", false, "Generate JavaScript client") - generateCmd.Flags().StringVar(&javascriptOutputDir, "js-out", "", "Output directory for JavaScript client") - - generateCmd.Flags().BoolVar(&typescriptFlag, "ts", false, "Generate TypeScript client") - generateCmd.Flags().StringVar(&typescriptOutputDir, "ts-out", "", "Output directory for TypeScript client") -} diff --git a/cli/cmd/login.go b/cli/cmd/login.go deleted file mode 100644 index 2053115f6..000000000 --- a/cli/cmd/login.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/nitrictech/nitric/cli/internal/style" - "github.com/nitrictech/nitric/cli/internal/style/icons" - "github.com/spf13/cobra" -) - -func NewLoginCmd(deps *Dependencies) *cobra.Command { - var loginCmd = &cobra.Command{ - Use: "login", - Short: "Login to Nitric", - Long: `Login to the Nitric CLI.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("\n%s Logging in...\n", style.Purple(icons.Lightning+" Nitric")) - - user, err := deps.WorkOSAuth.Login() - if err != nil { - fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) - return - } - - fmt.Printf("\n%s Logged in as %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) - }, - } - - return loginCmd -} diff --git a/cli/cmd/logout.go b/cli/cmd/logout.go deleted file mode 100644 index f130781e8..000000000 --- a/cli/cmd/logout.go +++ /dev/null @@ -1,28 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/nitrictech/nitric/cli/internal/style" - "github.com/nitrictech/nitric/cli/internal/style/icons" - "github.com/spf13/cobra" -) - -func NewLogoutCmd(deps *Dependencies) *cobra.Command { - var logoutCmd = &cobra.Command{ - Use: "logout", - Short: "Logout from Nitric", - Long: `Logout from the Nitric CLI.`, - Run: func(cmd *cobra.Command, args []string) { - err := deps.WorkOSAuth.Logout() - if err != nil { - fmt.Printf("\n%s Error logging out: %s\n", style.Red(icons.Cross), err) - return - } - - fmt.Printf("\n%s Logged out\n", style.Green(icons.Check)) - }, - } - - return logoutCmd -} diff --git a/cli/cmd/new.go b/cli/cmd/new.go deleted file mode 100644 index 4a75b1c65..000000000 --- a/cli/cmd/new.go +++ /dev/null @@ -1,197 +0,0 @@ -package cmd - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/charmbracelet/lipgloss" - "github.com/hashicorp/go-getter" - "github.com/nitrictech/nitric/cli/internal/api" - "github.com/nitrictech/nitric/cli/internal/style/colors" - "github.com/nitrictech/nitric/cli/pkg/files" - "github.com/nitrictech/nitric/cli/pkg/schema" - "github.com/nitrictech/nitric/cli/pkg/tui" - "github.com/spf13/afero" - "github.com/spf13/cobra" -) - -func projectExists(fs afero.Fs, projectDir string) (bool, error) { - projectExists, err := afero.Exists(fs, projectDir) - if err != nil { - return false, fmt.Errorf("failed to read intended project directory: %v", err) - } - if projectExists { - // Check if the directory is empty - files, err := afero.ReadDir(fs, projectDir) - if err != nil { - return false, fmt.Errorf("failed to read project directory: %v", err) - } - return len(files) > 0, nil - } - return false, nil -} - -func NewNewCmd(deps *Dependencies) *cobra.Command { - var newCmd = &cobra.Command{ - Use: "new", - Short: "Create a new Nitric project", - Run: func(cmd *cobra.Command, args []string) { - force, err := cmd.Flags().GetBool("force") - if err != nil { - fmt.Printf("Failed to get force flag: %v", err) - return - } - - templates, err := deps.NitricApiClient.GetTemplates() - if err != nil { - if errors.Is(err, api.ErrUnauthenticated) { - fmt.Println("Please login first, using the `login` command") - return - } - - fmt.Printf("Failed to get templates: %v", err) - return - } - - fs := afero.NewOsFs() - - projectName := "" - if len(args) > 0 { - projectName = args[0] - } - - if projectName == "" { - fmt.Println() - var err error - projectName, err = tui.RunTextInput("Project name:", func(input string) error { - if input == "" { - return errors.New("project name is required") - } - - // Must be kebab-case - if !regexp.MustCompile(`^[a-z][a-z0-9-]*$`).MatchString(input) { - return errors.New("project name must start with a letter and be lower kebab-case") - } - - return nil - }) - if err != nil || projectName == "" { - fmt.Println(err) - fmt.Println("+" + projectName + "+") - return - } - } - - projectDir := filepath.Join(".", projectName) - if !force { - projectExists, err := projectExists(fs, projectDir) - if err != nil { - fmt.Println(err.Error()) - return - } - if projectExists { - fmt.Printf("\nDirectory ./%s already exists and is not empty\n", projectDir) - return - } - } - - if len(templates) == 0 { - fmt.Println("No templates found") - return - } - - templateNames := make([]string, len(templates)) - for i, template := range templates { - templateNames[i] = template.String() - } - - // Prompt the user to select one of the templates - fmt.Println("") - _, index, err := tui.RunSelect(templateNames, "Template:") - if err != nil || index == -1 { - return - } - - template, err := deps.NitricApiClient.GetTemplate(templates[index].TeamSlug, templates[index].Slug, "") - cobra.CheckErr(err) - - // Find home directory. - home, err := os.UserHomeDir() - if err != nil { - fmt.Println(err) - return - } - - templateDir := filepath.Join(home, ".nitric", "templates", template.TeamSlug, template.TemplateSlug, template.Version) - - templateCached, err := afero.Exists(fs, filepath.Join(templateDir, "nitric.yaml")) - if err != nil { - fmt.Printf("Failed read template cache directory: %v", err) - return - } - - if !templateCached { - goGetter := &getter.Client{ - Ctx: context.Background(), - Dst: templateDir, - Src: template.GitSource, - Mode: getter.ClientModeAny, - DisableSymlinks: true, - } - - err = goGetter.Get() - if err != nil { - fmt.Printf("Failed to get template: %v", err) - return - } - } - - // Copy the template dir contents into a new project dir - err = os.MkdirAll(projectDir, 0755) - if err != nil { - fmt.Printf("Failed to create project directory: %v", err) - return - } - - err = files.CopyDir(fs, templateDir, projectDir) - if err != nil { - fmt.Printf("Failed to copy template directory: %v", err) - return - } - - nitricYamlPath := filepath.Join(projectDir, "nitric.yaml") - - appSpec, err := schema.LoadFromFile(fs, nitricYamlPath, false) - cobra.CheckErr(err) - - appSpec.Name = projectName - - err = schema.SaveToYaml(fs, nitricYamlPath, appSpec) - cobra.CheckErr(err) - - successStyle := lipgloss.NewStyle().MarginLeft(3) - highlight := lipgloss.NewStyle().Foreground(colors.Teal).Bold(true) - - var b strings.Builder - - b.WriteString("\n") - b.WriteString("Project created!") - b.WriteString("\n\n") - b.WriteString("Navigate to your project with ") - b.WriteString(highlight.Render("cd ./" + projectDir)) - b.WriteString("\n") - b.WriteString("Install dependencies and you're ready to rock! 🪨") - - fmt.Println(successStyle.Render(b.String())) - }, - } - - newCmd.Flags().BoolP("force", "f", false, "Force overwrite existing project directory") - - return newCmd -} diff --git a/cli/cmd/nitric.go b/cli/cmd/nitric.go new file mode 100644 index 000000000..32cf16b44 --- /dev/null +++ b/cli/cmd/nitric.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "github.com/nitrictech/nitric/cli/pkg/cli" + "github.com/samber/do" + "github.com/spf13/cobra" +) + +// NewTemplatesCmd creates the templates command +func NewTemplatesCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "templates", + Short: "List available templates", + Long: `List all available templates for creating new projects.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Templates()) + }, + } +} + +// NewNewCmd creates the new command +func NewNewCmd(injector *do.Injector) *cobra.Command { + var force bool + + cmd := &cobra.Command{ + Use: "new [project-name]", + Short: "Create a new Nitric project", + Long: `Create a new Nitric project from a template.`, + Run: func(cmd *cobra.Command, args []string) { + projectName := "" + if len(args) > 0 { + projectName = args[0] + } + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.New(projectName, force)) + }, + } + + cmd.Flags().BoolP("force", "f", false, "Force overwrite existing project directory") + return cmd +} + +// NewBuildCmd creates the build command +func NewBuildCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "build", + Short: "Builds the nitric application", + Long: `Builds an application using the nitric.yaml application spec and referenced platform.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Build()) + }, + } +} + +// NewGenerateCmd creates the generate command +func NewGenerateCmd(injector *do.Injector) *cobra.Command { + var ( + goFlag, pythonFlag, javascriptFlag, typescriptFlag bool + goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir string + ) + + cmd := &cobra.Command{ + Use: "generate", + Short: "Generate client libraries", + Long: `Generate client libraries for different programming languages based on the Nitric application specification.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag, goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir)) + }, + } + + // Add language flags + cmd.Flags().BoolVar(&goFlag, "go", false, "Generate Go client") + cmd.Flags().StringVar(&goOutputDir, "go-out", "", "Output directory for Go client") + cmd.Flags().StringVar(&goPackageName, "go-package-name", "", "Package name for Go client") + + cmd.Flags().BoolVar(&pythonFlag, "python", false, "Generate Python client") + cmd.Flags().StringVar(&pythonOutputDir, "python-out", "", "Output directory for Python client") + + cmd.Flags().BoolVar(&javascriptFlag, "js", false, "Generate JavaScript client") + cmd.Flags().StringVar(&javascriptOutputDir, "js-out", "", "Output directory for JavaScript client") + + cmd.Flags().BoolVar(&typescriptFlag, "ts", false, "Generate TypeScript client") + cmd.Flags().StringVar(&typescriptOutputDir, "ts-out", "", "Output directory for TypeScript client") + + return cmd +} + +// NewEditCmd creates the edit command +func NewEditCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "edit", + Short: "Edit the nitric application", + Long: `Edits an application using the nitric.yaml application spec and referenced platform.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Edit()) + }, + } +} + +// NewDevCmd creates the dev command +func NewDevCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "dev", + Short: "Run the Nitric application in development mode", + Long: `Run the Nitric application in development mode, allowing local testing of resources.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Dev()) + }, + } +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2978d210d..bb25c6ab6 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -1,70 +1,55 @@ package cmd import ( - "log" - "strings" - - "github.com/nitrictech/nitric/cli/internal/api" "github.com/nitrictech/nitric/cli/internal/config" - "github.com/nitrictech/nitric/cli/internal/workos" + "github.com/nitrictech/nitric/cli/pkg/cli" + "github.com/samber/do" "github.com/spf13/cobra" ) -var ( - cfgFile string - rootCmd = &cobra.Command{ +func NewRootCmd(injector *do.Injector) *cobra.Command { + rootCmd := &cobra.Command{ Use: "nitric", Short: "Nitric CLI - The command line interface for Nitric", Long: `Nitric CLI is a command line interface for managing and deploying Nitric applications. It provides a set of commands to help you develop, test, and deploy your Nitric applications.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + conf, err := config.Load(cmd) + if err != nil { + return err + } + + do.ProvideValue(injector, conf) + return nil + }, } -) - -type Dependencies struct { - WorkOSAuth *workos.WorkOSAuth - NitricApiClient *api.NitricApiClient -} -var deps *Dependencies = &Dependencies{} - -// Execute adds all child commands to the root command and sets flags appropriately. -func Execute() error { - return rootCmd.Execute() + // Add commands that use the CLI struct methods + rootCmd.AddCommand(NewLoginCmd(injector)) + rootCmd.AddCommand(NewLogoutCmd(injector)) + rootCmd.AddCommand(NewAccessTokenCmd(injector)) + rootCmd.AddCommand(NewVersionCmd(injector)) + rootCmd.AddCommand(NewTemplatesCmd(injector)) + rootCmd.AddCommand(NewNewCmd(injector)) + rootCmd.AddCommand(NewBuildCmd(injector)) + rootCmd.AddCommand(NewGenerateCmd(injector)) + rootCmd.AddCommand(NewEditCmd(injector)) + rootCmd.AddCommand(NewDevCmd(injector)) + rootCmd.AddCommand(NewConfigCmd(injector)) + + return rootCmd } -func init() { - cobra.OnInitialize(initConfig, initDependencies) - - // Global flags - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.nitric.yaml)") - - rootCmd.AddCommand(NewLoginCmd(deps)) - rootCmd.AddCommand(NewLogoutCmd(deps)) - rootCmd.AddCommand(NewAccessTokenCmd(deps)) - - rootCmd.AddCommand(NewTemplatesCmd(deps)) - rootCmd.AddCommand(NewNewCmd(deps)) - rootCmd.AddCommand(NewBuildCmd(deps)) -} - -func initDependencies() { - deps.NitricApiClient = api.NewNitricApiClient(config.GetNitricServerUrl(), nil) - - workosDetails, err := deps.NitricApiClient.GetWorkOSPublicDetails() - if err != nil { - if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset by peer") { - log.Fatal("unable to connect to the Nitric API. Please check your connection and try again. If the problem persists, please contact support.") - } - - log.Fatal(err) +// NewVersionCmd creates the version command +func NewVersionCmd(injector *do.Injector) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the CLI version", + Long: `Display the version number of the Nitric CLI.`, + Run: func(cmd *cobra.Command, args []string) { + app := do.MustInvoke[*cli.CLI](injector) + cobra.CheckErr(app.Version()) + }, } - - deps.WorkOSAuth = workos.NewWorkOSAuth(workos.NewKeyringTokenStore("nitric.v2.cli"), workosDetails.ClientID, workosDetails.ApiHostname) - deps.NitricApiClient.SetTokenProvider(deps.WorkOSAuth) -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - config.Load(cfgFile) } diff --git a/cli/cmd/templates.go b/cli/cmd/templates.go deleted file mode 100644 index 7846957ff..000000000 --- a/cli/cmd/templates.go +++ /dev/null @@ -1,54 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - - "github.com/charmbracelet/lipgloss" - "github.com/nitrictech/nitric/cli/internal/api" - "github.com/nitrictech/nitric/cli/internal/style/colors" - "github.com/spf13/cobra" -) - -func NewTemplatesCmd(deps *Dependencies) *cobra.Command { - var templatesCmd = &cobra.Command{ - Use: "templates", - Short: "Manage Nitric templates", - Long: `Manage Nitric templates.`, - } - - var listCmd = &cobra.Command{ - Use: "list", - Short: "List available Nitric templates", - Long: `List available Nitric templates.`, - Run: func(cmd *cobra.Command, args []string) { - resp, err := deps.NitricApiClient.GetTemplates() - if err != nil { - if errors.Is(err, api.ErrUnauthenticated) { - fmt.Println("Please login first, using the `login` command") - return - } - - fmt.Printf("Failed to get templates: %v", err) - return - } - - if len(resp) == 0 { - fmt.Println("No templates found") - return - } - - templateStyle := lipgloss.NewStyle().Foreground(colors.Purple) - - fmt.Println(templateStyle.Render("\nAvailable templates:")) - - for _, template := range resp { - fmt.Printf(" %s\n", template.String()) - } - }, - } - - templatesCmd.AddCommand(listCmd) - - return templatesCmd -} diff --git a/cli/cmd/version.go b/cli/cmd/version.go deleted file mode 100644 index a5300d086..000000000 --- a/cli/cmd/version.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/charmbracelet/lipgloss" - "github.com/nitrictech/nitric/cli/internal/version" - "github.com/spf13/cobra" -) - -var highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("14")) - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the CLI version", - Long: `Display the version number of the Nitric CLI.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("nitric cli version %s\n", highlight.Render(version.Version)) - - }, -} - -func init() { - rootCmd.AddCommand(versionCmd) -} diff --git a/cli/go.mod b/cli/go.mod index e8aac3201..8c427866f 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -89,6 +89,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/samber/do v1.6.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect diff --git a/cli/go.sum b/cli/go.sum index aff529395..64539db1a 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,6 +1,7 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -103,7 +104,9 @@ cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEar cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= @@ -616,6 +619,7 @@ git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3p github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= @@ -633,8 +637,10 @@ github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0I github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.50.36 h1:PjWXHwZPuTLMR1NIb8nEjLucZBMzmf84TLoLbD8BZqk= +github.com/aws/aws-sdk-go v1.50.36/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -686,6 +692,7 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= @@ -708,12 +715,14 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -735,6 +744,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -750,6 +760,7 @@ github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -761,6 +772,7 @@ github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -836,6 +848,7 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -874,6 +887,7 @@ github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhE github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -898,6 +912,7 @@ github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -946,6 +961,7 @@ github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -964,6 +980,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -975,7 +992,10 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU= +github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -993,6 +1013,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -1035,6 +1056,7 @@ github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8u github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -1046,15 +1068,24 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -1074,6 +1105,7 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1199,6 +1231,7 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1229,6 +1262,7 @@ golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1323,6 +1357,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1379,6 +1414,7 @@ golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1516,6 +1552,7 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/api v0.223.0 h1:JUTaWEriXmEy5AhvdMgksGGPEFsYfUKaPEYXd4c3Wvc= +google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1658,7 +1695,9 @@ google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaL google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1701,6 +1740,7 @@ google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwS google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1721,6 +1761,7 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -1731,6 +1772,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/internal/api/api.go b/cli/internal/api/api.go index 0f02b51dc..56e4e7572 100644 --- a/cli/internal/api/api.go +++ b/cli/internal/api/api.go @@ -5,7 +5,9 @@ import ( "net/http" "net/url" + "github.com/nitrictech/nitric/cli/internal/config" "github.com/pkg/errors" + "github.com/samber/do" ) type TokenProvider interface { @@ -14,19 +16,22 @@ type TokenProvider interface { } type NitricApiClient struct { - tokenProvider TokenProvider + tokenProvider func() (TokenProvider, error) apiUrl *url.URL } -func NewNitricApiClient(apiUrl *url.URL, tokenProvider TokenProvider) *NitricApiClient { +func NewNitricApiClient(injector *do.Injector) (*NitricApiClient, error) { + config := do.MustInvoke[*config.Config](injector) + apiUrl := config.GetNitricServerUrl() + + tokenProvider := func() (TokenProvider, error) { + return do.Invoke[TokenProvider](injector) + } + return &NitricApiClient{ apiUrl: apiUrl, tokenProvider: tokenProvider, - } -} - -func (c *NitricApiClient) SetTokenProvider(tokenProvider TokenProvider) { - c.tokenProvider = tokenProvider + }, nil } func (c *NitricApiClient) get(path string, requiresAuth bool) (*http.Response, error) { @@ -47,7 +52,12 @@ func (c *NitricApiClient) get(path string, requiresAuth bool) (*http.Response, e return nil, errors.Wrap(ErrPreconditionFailed, "no token provider provided") } - token, err := c.tokenProvider.GetAccessToken() + tokenProvider, err := c.tokenProvider() + if err != nil { + return nil, errors.Wrap(ErrUnauthenticated, err.Error()) + } + + token, err := tokenProvider.GetAccessToken() if err != nil { return nil, errors.Wrap(ErrUnauthenticated, err.Error()) } diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index b4d30e507..cdeae78f5 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -5,46 +5,40 @@ import ( "net/url" "os" "path/filepath" - "slices" + "strings" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" "github.com/spf13/viper" ) -const ( - ConfigFile = "config" - NitricServerUrlKey = "nitric.url" - EnvPrefix = "NITRIC" -) - -var allConfigKeys = []string{NitricServerUrlKey} +type Config struct { + v *viper.Viper + NitricServerUrl string `mapstructure:"url"` +} -func GetAllConfigItems() map[string]string { - items := make(map[string]string) - for _, key := range allConfigKeys { - items[key] = viper.GetString(key) - } - return items +func (c *Config) FileUsed() string { + return c.v.ConfigFileUsed() } -func GetValue(key string) (string, error) { - if slices.Contains(allConfigKeys, key) { - return viper.GetString(key), nil - } +func (c *Config) Dump() string { + all := c.v.AllSettings() - return "", fmt.Errorf("invalid config option %s", key) -} + allLines := []string{} -func SetValue(key string, value string) error { - if slices.Contains(allConfigKeys, key) { - viper.Set(key, value) - return nil + for key, value := range all { + allLines = append(allLines, fmt.Sprintf("%s: %v", key, value)) } - return fmt.Errorf("invalid config option %s", key) + return strings.Join(allLines, "\n") } -func GetNitricServerUrl() *url.URL { - nitricUrl, err := url.Parse(viper.GetString(NitricServerUrlKey)) +func (c *Config) SetValue(key, value string) error { + return mapstructure.Decode(map[string]interface{}{key: value}, c) +} + +func (c *Config) GetNitricServerUrl() *url.URL { + nitricUrl, err := url.Parse(c.NitricServerUrl) if err != nil { fmt.Printf("Error parsing nitric server url from config, using default: %v\n", err) return &url.URL{ @@ -56,62 +50,62 @@ func GetNitricServerUrl() *url.URL { return nitricUrl } -func SetNitricServerUrl(newUrl string) error { +func (c *Config) SetNitricServerUrl(newUrl string) error { nitricUrl, err := url.Parse(newUrl) if err != nil { return err } - viper.Set(NitricServerUrlKey, nitricUrl.String()) + c.NitricServerUrl = nitricUrl.String() return nil } -func Save() error { - if err := viper.WriteConfig(); err != nil { - // If config file doesn't exist, create it - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - if err := viper.SafeWriteConfig(); err != nil { - return fmt.Errorf("error creating config file: %v", err) - } - } else { - return fmt.Errorf("error writing config: %v", err) - } +func (c *Config) Save() error { + var configMap map[string]interface{} + err := mapstructure.Decode(c, &configMap) + if err != nil { + return fmt.Errorf("failed to decode config struct: %w", err) + } + + for key, value := range configMap { + c.v.Set(key, value) + } + + err = c.v.WriteConfig() + if err != nil { + return fmt.Errorf("failed to write config: %w", err) } return nil } -func setDefaults() { - viper.SetDefault(NitricServerUrlKey, "https://app.nitric.io/") -} +func Load(cmd *cobra.Command) (*Config, error) { + v := viper.New() + v.SetDefault("url", "https://app.nitric.io/") + + v.SetConfigType("yaml") -// Load loads the config from the file or the home directory -func Load(file string) error { - if file != "" { - // Use config file from the flag. - viper.SetConfigFile(file) + home, err := os.UserHomeDir() + if err != nil { + return nil, err } else { - // Find home directory. - home, err := os.UserHomeDir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } + v.AddConfigPath(filepath.Join(home, ".nitric")) + } - configDir := filepath.Join(home, ".nitric") + // Search the current .nitric directory first + v.AddConfigPath(".nitric") - // Search config in home directory with name ".nitric" (without extension). - viper.AddConfigPath(configDir) - // Search config in the current directory - viper.AddConfigPath(".") - viper.SetConfigType("yaml") - viper.SetConfigName(ConfigFile) + err = v.ReadInConfig() + if err != nil { + return nil, err } - setDefaults() + c := &Config{v: v} - viper.SetEnvPrefix(EnvPrefix) - viper.AutomaticEnv() // read in environment variables that match + err = v.Unmarshal(c) + if err != nil { + return nil, err + } - return viper.ReadInConfig() + return c, nil } diff --git a/cli/internal/workos/keyring.go b/cli/internal/workos/keyring.go index 3898e9688..42cb47d9a 100644 --- a/cli/internal/workos/keyring.go +++ b/cli/internal/workos/keyring.go @@ -5,29 +5,36 @@ import ( "encoding/json" "fmt" - "github.com/nitrictech/nitric/cli/internal/config" "github.com/zalando/go-keyring" ) -// Store 1 token per API -var WORKOS_TOKEN_KEY = getWorkosTokenKey(config.GetNitricServerUrl().String()) - -func getWorkosTokenKey(apiUrl string) string { - // Hash the API URL for a consistent length. We don't use the scheme or host, just the path - hash := sha256.Sum256([]byte(apiUrl + ".workos")) +func hashTokenKey(tokenKey string) string { + // Hash the token key for a consistent length. + hash := sha256.Sum256([]byte(tokenKey)) return fmt.Sprintf("%x", hash) } type KeyringTokenStore struct { - service string + service string + tokenKey string } -func NewKeyringTokenStore(service string) *KeyringTokenStore { - return &KeyringTokenStore{service: service} +func NewKeyringTokenStore(serviceName, tokenKey string) (*KeyringTokenStore, error) { + if serviceName == "" { + return nil, fmt.Errorf("service name is required") + } + + if tokenKey == "" { + return nil, fmt.Errorf("token key is required") + } + + hashedTokenKey := hashTokenKey(tokenKey) + + return &KeyringTokenStore{service: serviceName, tokenKey: hashedTokenKey}, nil } func (s *KeyringTokenStore) GetTokens() (*Tokens, error) { - token, err := keyring.Get(s.service, WORKOS_TOKEN_KEY) + token, err := keyring.Get(s.service, s.tokenKey) if err != nil { if err == keyring.ErrNotFound { return nil, ErrNotFound @@ -50,11 +57,11 @@ func (s *KeyringTokenStore) SaveTokens(tokens *Tokens) error { return fmt.Errorf("failed to marshal token: %w", err) } - return keyring.Set(s.service, WORKOS_TOKEN_KEY, string(json)) + return keyring.Set(s.service, s.tokenKey, string(json)) } func (s *KeyringTokenStore) Clear() error { - err := keyring.Delete(s.service, WORKOS_TOKEN_KEY) + err := keyring.Delete(s.service, s.tokenKey) if err != nil { if err == keyring.ErrNotFound { return ErrNotFound diff --git a/cli/main.go b/cli/main.go index 7dacf43db..4945fd5ac 100644 --- a/cli/main.go +++ b/cli/main.go @@ -2,12 +2,54 @@ package main import ( "os" + "strings" "github.com/nitrictech/nitric/cli/cmd" + "github.com/nitrictech/nitric/cli/internal/api" + "github.com/nitrictech/nitric/cli/internal/config" + "github.com/nitrictech/nitric/cli/internal/workos" + "github.com/nitrictech/nitric/cli/pkg/cli" + "github.com/samber/do" + "github.com/spf13/cobra" ) func main() { - if err := cmd.Execute(); err != nil { + + injector := do.New() + + do.Provide(injector, func(inj *do.Injector) (workos.TokenStore, error) { + config := do.MustInvoke[*config.Config](inj) + apiUrl := config.GetNitricServerUrl() + + tokenStore, err := workos.NewKeyringTokenStore("nitric.v2.cli", apiUrl.String()) + if err != nil { + return nil, err + } + return tokenStore, nil + }) + + do.Provide(injector, api.NewNitricApiClient) + + do.Provide(injector, func(inj *do.Injector) (*workos.WorkOSAuth, error) { + tokenStore := do.MustInvoke[workos.TokenStore](inj) + + apiClient := do.MustInvoke[*api.NitricApiClient](inj) + workosDetails, err := apiClient.GetWorkOSPublicDetails() + if err != nil { + if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset by peer") { + cobra.CheckErr("failed to connect to the Nitric API. Please check your connection and try again. If the problem persists, please contact support.") + } + + cobra.CheckErr(err) + } + return workos.NewWorkOSAuth(tokenStore, workosDetails.ClientID, workosDetails.ApiHostname), nil + }) + + do.Provide(injector, cli.NewCLI) + + rootCmd := cmd.NewRootCmd(injector) + + if err := rootCmd.Execute(); err != nil { os.Exit(1) } } diff --git a/cli/pkg/cli/cli.go b/cli/pkg/cli/cli.go new file mode 100644 index 000000000..8a0a241c6 --- /dev/null +++ b/cli/pkg/cli/cli.go @@ -0,0 +1,448 @@ +package cli + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/lipgloss" + "github.com/hashicorp/go-getter" + "github.com/nitrictech/nitric/cli/internal/api" + "github.com/nitrictech/nitric/cli/internal/browser" + "github.com/nitrictech/nitric/cli/internal/config" + "github.com/nitrictech/nitric/cli/internal/devserver" + "github.com/nitrictech/nitric/cli/internal/plugins" + "github.com/nitrictech/nitric/cli/internal/simulation" + "github.com/nitrictech/nitric/cli/internal/style" + "github.com/nitrictech/nitric/cli/internal/style/colors" + "github.com/nitrictech/nitric/cli/internal/style/icons" + "github.com/nitrictech/nitric/cli/internal/version" + "github.com/nitrictech/nitric/cli/internal/workos" + "github.com/nitrictech/nitric/cli/pkg/client" + "github.com/nitrictech/nitric/cli/pkg/files" + "github.com/nitrictech/nitric/cli/pkg/schema" + "github.com/nitrictech/nitric/cli/pkg/tui" + "github.com/nitrictech/nitric/engines/terraform" + "github.com/samber/do" + "github.com/spf13/afero" +) + +type CLI struct { + config *config.Config + apiClient *api.NitricApiClient + auth *workos.WorkOSAuth +} + +func NewCLI(injector *do.Injector) (*CLI, error) { + config := do.MustInvoke[*config.Config](injector) + apiClient := do.MustInvoke[*api.NitricApiClient](injector) + auth := do.MustInvoke[*workos.WorkOSAuth](injector) + + return &CLI{config: config, apiClient: apiClient, auth: auth}, nil +} + +// Login handles the login command logic +func (c *CLI) Login() error { + fmt.Printf("\n%s Logging in...\n", style.Purple(icons.Lightning+" Nitric")) + + user, err := c.auth.Login() + if err != nil { + fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) + return err + } + + fmt.Printf("\n%s Logged in as %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) + return nil +} + +// Logout handles the logout command logic +func (c *CLI) Logout() error { + fmt.Printf("\n%s Logging out...\n", style.Purple(icons.Lightning+" Nitric")) + + err := c.auth.Logout() + if err != nil { + fmt.Printf("\n%s Error logging out: %s\n", style.Red(icons.Cross), err) + return err + } + + fmt.Printf("\n%s Logged out successfully\n", style.Green(icons.Check)) + return nil +} + +// AccessToken handles the access token command logic +func (c *CLI) AccessToken() error { + token, err := c.auth.GetAccessToken() + if err != nil { + fmt.Printf("\n%s Error getting access token: %s\n", style.Red(icons.Cross), err) + return err + } + + fmt.Printf("\n%s Access token: %s\n", style.Green(icons.Check), token) + return nil +} + +// Version handles the version command logic +func (c *CLI) Version() error { + highlight := lipgloss.NewStyle().Foreground(lipgloss.Color("14")) + fmt.Printf("nitric cli version %s\n", highlight.Render(version.Version)) + return nil +} + +// Templates handles the templates command logic +func (c *CLI) Templates() error { + templates, err := c.apiClient.GetTemplates() + if err != nil { + if errors.Is(err, api.ErrUnauthenticated) { + fmt.Println("Please login first, using the `login` command") + return nil + } + + fmt.Printf("Failed to get templates: %v\n", err) + return nil + } + + if len(templates) == 0 { + fmt.Println("No templates found") + return nil + } + + fmt.Println("Available templates:") + for _, template := range templates { + fmt.Printf(" %s\n", template.String()) + } + + return nil +} + +// New handles the new project creation command logic +func (c *CLI) New(projectName string, force bool) error { + templates, err := c.apiClient.GetTemplates() + if err != nil { + if errors.Is(err, api.ErrUnauthenticated) { + fmt.Println("Please login first, using the `login` command") + return nil + } + + fmt.Printf("Failed to get templates: %v\n", err) + return nil + } + + fs := afero.NewOsFs() + + if projectName == "" { + fmt.Println() + var err error + projectName, err = tui.RunTextInput("Project name:", func(input string) error { + if input == "" { + return errors.New("project name is required") + } + + // Must be kebab-case + if !regexp.MustCompile(`^[a-z][a-z0-9-]*$`).MatchString(input) { + return errors.New("project name must start with a letter and be lower kebab-case") + } + + return nil + }) + if err != nil || projectName == "" { + fmt.Println(err) + fmt.Println("+" + projectName + "+") + return nil + } + } + + projectDir := filepath.Join(".", projectName) + if !force { + projectExists, err := projectExists(fs, projectDir) + if err != nil { + fmt.Println(err.Error()) + return nil + } + if projectExists { + fmt.Printf("\nDirectory ./%s already exists and is not empty\n", projectDir) + return errors.New("project directory already exists") + } + } + + if len(templates) == 0 { + fmt.Println("No templates found") + return errors.New("no templates available") + } + + templateNames := make([]string, len(templates)) + for i, template := range templates { + templateNames[i] = template.String() + } + + // Prompt the user to select one of the templates + fmt.Println("") + _, index, err := tui.RunSelect(templateNames, "Template:") + if err != nil || index == -1 { + return err + } + + template, err := c.apiClient.GetTemplate(templates[index].TeamSlug, templates[index].Slug, "") + if err != nil { + return err + } + + // Find home directory. + home, err := os.UserHomeDir() + if err != nil { + fmt.Println(err) + return err + } + + templateDir := filepath.Join(home, ".nitric", "templates", template.TeamSlug, template.TemplateSlug, template.Version) + + templateCached, err := afero.Exists(fs, filepath.Join(templateDir, "nitric.yaml")) + if err != nil { + fmt.Printf("Failed read template cache directory: %v\n", err) + return err + } + + if !templateCached { + goGetter := &getter.Client{ + Ctx: context.Background(), + Dst: templateDir, + Src: template.GitSource, + Mode: getter.ClientModeAny, + DisableSymlinks: true, + } + + err = goGetter.Get() + if err != nil { + fmt.Printf("Failed to get template: %v\n", err) + return err + } + } + + // Copy the template dir contents into a new project dir + err = os.MkdirAll(projectDir, 0755) + if err != nil { + fmt.Printf("Failed to create project directory: %v\n", err) + return err + } + + err = files.CopyDir(fs, templateDir, projectDir) + if err != nil { + fmt.Printf("Failed to copy template directory: %v\n", err) + return err + } + + nitricYamlPath := filepath.Join(projectDir, "nitric.yaml") + + appSpec, err := schema.LoadFromFile(fs, nitricYamlPath, false) + if err != nil { + return err + } + + appSpec.Name = projectName + + err = schema.SaveToYaml(fs, nitricYamlPath, appSpec) + if err != nil { + return err + } + + successStyle := lipgloss.NewStyle().MarginLeft(3) + highlight := lipgloss.NewStyle().Foreground(colors.Teal).Bold(true) + + var b strings.Builder + + b.WriteString("\n") + b.WriteString("Project created!") + b.WriteString("\n\n") + b.WriteString("Navigate to your project with ") + b.WriteString(highlight.Render("cd ./" + projectDir)) + b.WriteString("\n") + b.WriteString("Install dependencies and you're ready to rock! 🪨") + + fmt.Println(successStyle.Render(b.String())) + return nil +} + +// Build handles the build command logic +func (c *CLI) Build() error { + // Read the nitric.yaml file + fs := afero.NewOsFs() + + appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) + if err != nil { + return err + } + + mockPlatformRepository := terraform.NewMockPlatformRepository() + + // TODO:prompt for platform selection if multiple targets are specified + targetPlatform := appSpec.Targets[0] + + platform, err := terraform.PlatformFromId(fs, targetPlatform, mockPlatformRepository) + if err != nil { + return err + } + + repo := plugins.NewPluginRepository(c.apiClient) + engine := terraform.New(platform, terraform.WithRepository(repo)) + // Parse the application spec + // Validate the application spec + // Build the application using the specified platform + // Handle any errors that occur during the build process + + err = engine.Apply(appSpec) + if err != nil { + fmt.Print("Error applying platform: ", err) + return err + } + + fmt.Println("Build completed successfully.") + return nil +} + +// Generate handles the generate command logic +func (c *CLI) Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag bool, goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir string) error { + // Check if at least one language flag is provided + if !goFlag && !pythonFlag && !javascriptFlag && !typescriptFlag { + return fmt.Errorf("at least one language flag must be specified") + } + + fs := afero.NewOsFs() + + appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) + if err != nil { + return err + } + + if !client.SpecHasClientResources(*appSpec) { + fmt.Println("No client compatible resources found in application, skipping client generation") + return nil + } + + // check if the go language flag is provided + if goFlag { + fmt.Println("Generating Go client...") + // TODO: add flags for output directory and package name + err = client.GenerateGo(fs, *appSpec, goOutputDir, goPackageName) + if err != nil { + return err + } + } + + if pythonFlag { + fmt.Println("Generating Python client...") + err = client.GeneratePython(fs, *appSpec, pythonOutputDir) + if err != nil { + return err + } + } + + if typescriptFlag { + fmt.Println("Generating NodeJS client...") + err = client.GenerateTypeScript(fs, *appSpec, typescriptOutputDir) + if err != nil { + return err + } + } + + fmt.Println("Clients generated successfully.") + return nil +} + +// Edit handles the edit command logic +func (c *CLI) Edit() error { + const fileName = "nitric.yaml" + + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return fmt.Errorf("error listening: %v", err) + } + + devwsServer := devserver.NewDevWebsocketServer(devserver.WithListener(listener)) + fileSync, err := devserver.NewFileSync(fileName, devwsServer.Broadcast, devserver.WithDebounce(time.Millisecond*100)) + if err != nil { + return err + } + defer fileSync.Close() + + // subscribe the file sync to the websocket server + devwsServer.Subscribe(fileSync) + + port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) + + // Open browser tab to the dashboard + devUrl := c.config.GetNitricServerUrl().JoinPath("dev") + q := devUrl.Query() + q.Add("port", port) + devUrl.RawQuery = q.Encode() + + fmt.Println(tui.NitricIntro("Sync Port", port, "Dashboard", devUrl.String())) + + // Start the WebSocket server + errChan := make(chan error) + go func(errChan chan error) { + err := devwsServer.Start() + if err != nil { + errChan <- err + } + }(errChan) + + go func() { + err = fileSync.Start() + if err != nil { + fmt.Printf("Error starting file sync: %v\n", err) + } + }() + + fmt.Println("Opening browser to the editor") + + err = browser.Open(devUrl.String()) + if err != nil { + fmt.Printf("Error opening browser: %v\n", err) + } + + // Wait for the file watcher to fail/return + return <-errChan +} + +// Dev handles the dev command logic +func (c *CLI) Dev() error { + // 1. Load the App Spec + // Read the nitric.yaml file + fs := afero.NewOsFs() + + appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true) + if err != nil { + return err + } + + simserver := simulation.NewSimulationServer(fs, appSpec) + err = simserver.Start(os.Stdout) + if err != nil { + return err + } + + return nil +} + +// Helper function for checking if project exists +func projectExists(fs afero.Fs, projectDir string) (bool, error) { + projectExists, err := afero.Exists(fs, projectDir) + if err != nil { + return false, fmt.Errorf("failed to read intended project directory: %v", err) + } + if projectExists { + // Check if the directory is empty + files, err := afero.ReadDir(fs, projectDir) + if err != nil { + return false, fmt.Errorf("failed to read project directory: %v", err) + } + return len(files) > 0, nil + } + return false, nil +} From 84b660ffbd0b629eb9163f547a99a55b4c91231b Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 10 Jul 2025 15:23:00 +1000 Subject: [PATCH 7/8] fix auth/unauth coupling --- cli/cmd/auth.go | 12 ++++----- cli/main.go | 5 ++++ cli/pkg/cli/auth.go | 59 +++++++++++++++++++++++++++++++++++++++++++++ cli/pkg/cli/cli.go | 48 ++---------------------------------- 4 files changed, 72 insertions(+), 52 deletions(-) create mode 100644 cli/pkg/cli/auth.go diff --git a/cli/cmd/auth.go b/cli/cmd/auth.go index 61cf19ec9..a2e812688 100644 --- a/cli/cmd/auth.go +++ b/cli/cmd/auth.go @@ -13,8 +13,8 @@ func NewLoginCmd(injector *do.Injector) *cobra.Command { Short: "Login to Nitric", Long: `Login to the Nitric CLI.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) - cobra.CheckErr(app.Login()) + app := do.MustInvoke[*cli.AuthApp](injector) + app.Login() }, } } @@ -26,8 +26,8 @@ func NewLogoutCmd(injector *do.Injector) *cobra.Command { Short: "Logout from Nitric", Long: `Logout from the Nitric CLI.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) - cobra.CheckErr(app.Logout()) + app := do.MustInvoke[*cli.AuthApp](injector) + app.Logout() }, } } @@ -39,8 +39,8 @@ func NewAccessTokenCmd(injector *do.Injector) *cobra.Command { Short: "Get access token", Long: `Get the current access token.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) - cobra.CheckErr(app.AccessToken()) + app := do.MustInvoke[*cli.AuthApp](injector) + app.AccessToken() }, } } diff --git a/cli/main.go b/cli/main.go index 4945fd5ac..f347589a6 100644 --- a/cli/main.go +++ b/cli/main.go @@ -45,7 +45,12 @@ func main() { return workos.NewWorkOSAuth(tokenStore, workosDetails.ClientID, workosDetails.ApiHostname), nil }) + do.Provide(injector, func(inj *do.Injector) (api.TokenProvider, error) { + return do.Invoke[*workos.WorkOSAuth](inj) + }) + do.Provide(injector, cli.NewCLI) + do.Provide(injector, cli.NewAuthApp) rootCmd := cmd.NewRootCmd(injector) diff --git a/cli/pkg/cli/auth.go b/cli/pkg/cli/auth.go new file mode 100644 index 000000000..6e8a3a27e --- /dev/null +++ b/cli/pkg/cli/auth.go @@ -0,0 +1,59 @@ +package cli + +import ( + "errors" + "fmt" + + "github.com/nitrictech/nitric/cli/internal/style" + "github.com/nitrictech/nitric/cli/internal/style/icons" + "github.com/nitrictech/nitric/cli/internal/workos" + "github.com/samber/do" +) + +type AuthApp struct { + auth *workos.WorkOSAuth +} + +func NewAuthApp(injector *do.Injector) (*AuthApp, error) { + auth := do.MustInvoke[*workos.WorkOSAuth](injector) + return &AuthApp{auth: auth}, nil +} + +// Login handles the login command logic +func (c *AuthApp) Login() { + fmt.Printf("\n%s Logging in...\n", style.Purple(icons.Lightning+" Nitric")) + + user, err := c.auth.Login() + if err != nil { + fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) + return + } + + fmt.Printf("\n%s Logged in as %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) +} + +// Logout handles the logout command logic +func (c *AuthApp) Logout() { + fmt.Printf("\n%s Logging out...\n", style.Purple(icons.Lightning+" Nitric")) + + err := c.auth.Logout() + if err != nil { + if !errors.Is(err, workos.ErrNotFound) { + fmt.Printf("\n%s Error logging out: %s\n", style.Red(icons.Cross), err) + return + } + } + + fmt.Printf("\n%s Logged out successfully\n", style.Green(icons.Check)) +} + +// AccessToken handles the access token command logic +func (c *AuthApp) AccessToken() { + token, err := c.auth.GetAccessToken() + if err != nil { + fmt.Printf("\n%s Error getting access token: %s\n", style.Red(icons.Cross), err) + return + } + + fmt.Printf("\n%s Access token: %s\n", style.Green(icons.Check), token) +} diff --git a/cli/pkg/cli/cli.go b/cli/pkg/cli/cli.go index 8a0a241c6..01028f75c 100644 --- a/cli/pkg/cli/cli.go +++ b/cli/pkg/cli/cli.go @@ -20,11 +20,8 @@ import ( "github.com/nitrictech/nitric/cli/internal/devserver" "github.com/nitrictech/nitric/cli/internal/plugins" "github.com/nitrictech/nitric/cli/internal/simulation" - "github.com/nitrictech/nitric/cli/internal/style" "github.com/nitrictech/nitric/cli/internal/style/colors" - "github.com/nitrictech/nitric/cli/internal/style/icons" "github.com/nitrictech/nitric/cli/internal/version" - "github.com/nitrictech/nitric/cli/internal/workos" "github.com/nitrictech/nitric/cli/pkg/client" "github.com/nitrictech/nitric/cli/pkg/files" "github.com/nitrictech/nitric/cli/pkg/schema" @@ -37,55 +34,13 @@ import ( type CLI struct { config *config.Config apiClient *api.NitricApiClient - auth *workos.WorkOSAuth } func NewCLI(injector *do.Injector) (*CLI, error) { config := do.MustInvoke[*config.Config](injector) apiClient := do.MustInvoke[*api.NitricApiClient](injector) - auth := do.MustInvoke[*workos.WorkOSAuth](injector) - return &CLI{config: config, apiClient: apiClient, auth: auth}, nil -} - -// Login handles the login command logic -func (c *CLI) Login() error { - fmt.Printf("\n%s Logging in...\n", style.Purple(icons.Lightning+" Nitric")) - - user, err := c.auth.Login() - if err != nil { - fmt.Printf("\n%s Error logging in: %s\n", style.Red(icons.Cross), err) - return err - } - - fmt.Printf("\n%s Logged in as %s\n", style.Green(icons.Check), style.Teal(user.FirstName)) - return nil -} - -// Logout handles the logout command logic -func (c *CLI) Logout() error { - fmt.Printf("\n%s Logging out...\n", style.Purple(icons.Lightning+" Nitric")) - - err := c.auth.Logout() - if err != nil { - fmt.Printf("\n%s Error logging out: %s\n", style.Red(icons.Cross), err) - return err - } - - fmt.Printf("\n%s Logged out successfully\n", style.Green(icons.Check)) - return nil -} - -// AccessToken handles the access token command logic -func (c *CLI) AccessToken() error { - token, err := c.auth.GetAccessToken() - if err != nil { - fmt.Printf("\n%s Error getting access token: %s\n", style.Red(icons.Cross), err) - return err - } - - fmt.Printf("\n%s Access token: %s\n", style.Green(icons.Check), token) - return nil + return &CLI{config: config, apiClient: apiClient}, nil } // Version handles the version command logic @@ -101,6 +56,7 @@ func (c *CLI) Templates() error { if err != nil { if errors.Is(err, api.ErrUnauthenticated) { fmt.Println("Please login first, using the `login` command") + fmt.Printf("%+v\n", err) return nil } From 681aaada410d43316af17a5d07d17cd657e314f0 Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 10 Jul 2025 16:42:09 +1000 Subject: [PATCH 8/8] continue refactor further reduces coupling between cli services --- cli/cmd/auth.go | 25 +++++++--- cli/cmd/config.go | 4 +- cli/cmd/nitric.go | 42 ++++++++++------ cli/cmd/root.go | 15 +++--- cli/go.mod | 2 + cli/go.sum | 4 ++ cli/internal/api/api.go | 17 ++----- cli/internal/api/auth.go | 36 -------------- cli/internal/details/details.go | 10 ++++ cli/internal/details/service/details.go | 65 +++++++++++++++++++++++++ cli/internal/workos/workos.go | 22 ++++++--- cli/main.go | 56 ++++++++------------- cli/pkg/{cli => app}/auth.go | 6 +-- cli/pkg/{cli/cli.go => app/nitric.go} | 30 +++++------- 14 files changed, 191 insertions(+), 143 deletions(-) delete mode 100644 cli/internal/api/auth.go create mode 100644 cli/internal/details/details.go create mode 100644 cli/internal/details/service/details.go rename cli/pkg/{cli => app}/auth.go (93%) rename cli/pkg/{cli/cli.go => app/nitric.go} (92%) diff --git a/cli/cmd/auth.go b/cli/cmd/auth.go index a2e812688..ece7f1a8f 100644 --- a/cli/cmd/auth.go +++ b/cli/cmd/auth.go @@ -1,45 +1,54 @@ package cmd import ( - "github.com/nitrictech/nitric/cli/pkg/cli" - "github.com/samber/do" + "github.com/nitrictech/nitric/cli/pkg/app" + "github.com/samber/do/v2" "github.com/spf13/cobra" ) // NewLoginCmd creates the login command -func NewLoginCmd(injector *do.Injector) *cobra.Command { +func NewLoginCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "login", Short: "Login to Nitric", Long: `Login to the Nitric CLI.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.AuthApp](injector) + app, err := do.Invoke[*app.AuthApp](injector) + if err != nil { + cobra.CheckErr(err) + } app.Login() }, } } // NewLogoutCmd creates the logout command -func NewLogoutCmd(injector *do.Injector) *cobra.Command { +func NewLogoutCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "logout", Short: "Logout from Nitric", Long: `Logout from the Nitric CLI.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.AuthApp](injector) + app, err := do.Invoke[*app.AuthApp](injector) + if err != nil { + cobra.CheckErr(err) + } app.Logout() }, } } // NewAccessTokenCmd creates the access token command -func NewAccessTokenCmd(injector *do.Injector) *cobra.Command { +func NewAccessTokenCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "access-token", Short: "Get access token", Long: `Get the current access token.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.AuthApp](injector) + app, err := do.Invoke[*app.AuthApp](injector) + if err != nil { + cobra.CheckErr(err) + } app.AccessToken() }, } diff --git a/cli/cmd/config.go b/cli/cmd/config.go index 3676f0886..f8a851cfc 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -7,12 +7,12 @@ import ( "github.com/nitrictech/nitric/cli/internal/config" "github.com/nitrictech/nitric/cli/internal/style" "github.com/nitrictech/nitric/cli/internal/style/colors" - "github.com/samber/do" + "github.com/samber/do/v2" "github.com/spf13/cobra" ) // NewConfigCmd creates the config command -func NewConfigCmd(injector *do.Injector) *cobra.Command { +func NewConfigCmd(injector do.Injector) *cobra.Command { configCmd := &cobra.Command{ Use: "config", Short: "Manage CLI configuration", diff --git a/cli/cmd/nitric.go b/cli/cmd/nitric.go index 32cf16b44..25b24d0f7 100644 --- a/cli/cmd/nitric.go +++ b/cli/cmd/nitric.go @@ -1,26 +1,29 @@ package cmd import ( - "github.com/nitrictech/nitric/cli/pkg/cli" - "github.com/samber/do" + "github.com/nitrictech/nitric/cli/pkg/app" + "github.com/samber/do/v2" "github.com/spf13/cobra" ) // NewTemplatesCmd creates the templates command -func NewTemplatesCmd(injector *do.Injector) *cobra.Command { +func NewTemplatesCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "templates", Short: "List available templates", Long: `List all available templates for creating new projects.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) + app, err := do.Invoke[*app.NitricApp](injector) + if err != nil { + cobra.CheckErr(err) + } cobra.CheckErr(app.Templates()) }, } } // NewNewCmd creates the new command -func NewNewCmd(injector *do.Injector) *cobra.Command { +func NewNewCmd(injector do.Injector) *cobra.Command { var force bool cmd := &cobra.Command{ @@ -32,7 +35,10 @@ func NewNewCmd(injector *do.Injector) *cobra.Command { if len(args) > 0 { projectName = args[0] } - app := do.MustInvoke[*cli.CLI](injector) + app, err := do.Invoke[*app.NitricApp](injector) + if err != nil { + cobra.CheckErr(err) + } cobra.CheckErr(app.New(projectName, force)) }, } @@ -42,20 +48,23 @@ func NewNewCmd(injector *do.Injector) *cobra.Command { } // NewBuildCmd creates the build command -func NewBuildCmd(injector *do.Injector) *cobra.Command { +func NewBuildCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "build", Short: "Builds the nitric application", Long: `Builds an application using the nitric.yaml application spec and referenced platform.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) + app, err := do.Invoke[*app.NitricApp](injector) + if err != nil { + cobra.CheckErr(err) + } cobra.CheckErr(app.Build()) }, } } // NewGenerateCmd creates the generate command -func NewGenerateCmd(injector *do.Injector) *cobra.Command { +func NewGenerateCmd(injector do.Injector) *cobra.Command { var ( goFlag, pythonFlag, javascriptFlag, typescriptFlag bool goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir string @@ -66,7 +75,10 @@ func NewGenerateCmd(injector *do.Injector) *cobra.Command { Short: "Generate client libraries", Long: `Generate client libraries for different programming languages based on the Nitric application specification.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) + app, err := do.Invoke[*app.NitricApp](injector) + if err != nil { + cobra.CheckErr(err) + } cobra.CheckErr(app.Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag, goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir)) }, } @@ -89,26 +101,28 @@ func NewGenerateCmd(injector *do.Injector) *cobra.Command { } // NewEditCmd creates the edit command -func NewEditCmd(injector *do.Injector) *cobra.Command { +func NewEditCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "edit", Short: "Edit the nitric application", Long: `Edits an application using the nitric.yaml application spec and referenced platform.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) + app, err := do.Invoke[*app.NitricApp](injector) + if err != nil { + cobra.CheckErr(err) + } cobra.CheckErr(app.Edit()) }, } } // NewDevCmd creates the dev command -func NewDevCmd(injector *do.Injector) *cobra.Command { +func NewDevCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "dev", Short: "Run the Nitric application in development mode", Long: `Run the Nitric application in development mode, allowing local testing of resources.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) cobra.CheckErr(app.Dev()) }, } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index bb25c6ab6..0ff06c442 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -1,13 +1,16 @@ package cmd import ( + "fmt" + + "github.com/charmbracelet/lipgloss" "github.com/nitrictech/nitric/cli/internal/config" - "github.com/nitrictech/nitric/cli/pkg/cli" - "github.com/samber/do" + "github.com/nitrictech/nitric/cli/internal/version" + "github.com/samber/do/v2" "github.com/spf13/cobra" ) -func NewRootCmd(injector *do.Injector) *cobra.Command { +func NewRootCmd(injector do.Injector) *cobra.Command { rootCmd := &cobra.Command{ Use: "nitric", Short: "Nitric CLI - The command line interface for Nitric", @@ -42,14 +45,14 @@ test, and deploy your Nitric applications.`, } // NewVersionCmd creates the version command -func NewVersionCmd(injector *do.Injector) *cobra.Command { +func NewVersionCmd(injector do.Injector) *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print the CLI version", Long: `Display the version number of the Nitric CLI.`, Run: func(cmd *cobra.Command, args []string) { - app := do.MustInvoke[*cli.CLI](injector) - cobra.CheckErr(app.Version()) + highlight := lipgloss.NewStyle().Foreground(lipgloss.Color("14")) + fmt.Printf("nitric cli version %s\n", highlight.Render(version.Version)) }, } } diff --git a/cli/go.mod b/cli/go.mod index 8c427866f..430efcdb2 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -90,6 +90,8 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/samber/do v1.6.0 // indirect + github.com/samber/do/v2 v2.0.0-beta.7 // indirect + github.com/samber/go-type-to-string v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect diff --git a/cli/go.sum b/cli/go.sum index 64539db1a..5abe5eef0 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -994,6 +994,10 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU= github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k= +github.com/samber/do/v2 v2.0.0-beta.7 h1:tmdLOVSCbTA6uGWLU5poi/nZvMRh5QxXFJ9vHytU+Jk= +github.com/samber/do/v2 v2.0.0-beta.7/go.mod h1:+LpV3vu4L81Q1JMZNSkMvSkW9lt4e5eJoXoZHkeBS4c= +github.com/samber/go-type-to-string v1.4.0 h1:KXphToZgiFdnJQxryU25brhlh/CqY/cwJVeX2rfmow0= +github.com/samber/go-type-to-string v1.4.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= diff --git a/cli/internal/api/api.go b/cli/internal/api/api.go index 56e4e7572..ba3b25362 100644 --- a/cli/internal/api/api.go +++ b/cli/internal/api/api.go @@ -7,7 +7,7 @@ import ( "github.com/nitrictech/nitric/cli/internal/config" "github.com/pkg/errors" - "github.com/samber/do" + "github.com/samber/do/v2" ) type TokenProvider interface { @@ -16,17 +16,15 @@ type TokenProvider interface { } type NitricApiClient struct { - tokenProvider func() (TokenProvider, error) + tokenProvider TokenProvider apiUrl *url.URL } -func NewNitricApiClient(injector *do.Injector) (*NitricApiClient, error) { +func NewNitricApiClient(injector do.Injector) (*NitricApiClient, error) { config := do.MustInvoke[*config.Config](injector) apiUrl := config.GetNitricServerUrl() - tokenProvider := func() (TokenProvider, error) { - return do.Invoke[TokenProvider](injector) - } + tokenProvider := do.MustInvokeAs[TokenProvider](injector) return &NitricApiClient{ apiUrl: apiUrl, @@ -52,12 +50,7 @@ func (c *NitricApiClient) get(path string, requiresAuth bool) (*http.Response, e return nil, errors.Wrap(ErrPreconditionFailed, "no token provider provided") } - tokenProvider, err := c.tokenProvider() - if err != nil { - return nil, errors.Wrap(ErrUnauthenticated, err.Error()) - } - - token, err := tokenProvider.GetAccessToken() + token, err := c.tokenProvider.GetAccessToken() if err != nil { return nil, errors.Wrap(ErrUnauthenticated, err.Error()) } diff --git a/cli/internal/api/auth.go b/cli/internal/api/auth.go deleted file mode 100644 index 25a56c191..000000000 --- a/cli/internal/api/auth.go +++ /dev/null @@ -1,36 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" -) - -type AuthDetails struct { - WorkOS WorkOSDetails `json:"workos"` -} - -type WorkOSDetails struct { - ClientID string `json:"client_id"` - ApiHostname string `json:"api_hostname"` -} - -func (c *NitricApiClient) GetWorkOSPublicDetails() (*WorkOSDetails, error) { - response, err := c.get("/auth/details", false) - if err != nil { - return nil, fmt.Errorf("failed to connect to nitric auth details endpoint: %v", err) - } - - body, err := io.ReadAll(response.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response from nitric auth details endpoint: %v", err) - } - - var authDetails AuthDetails - err = json.Unmarshal(body, &authDetails) - if err != nil { - return nil, fmt.Errorf("unexpected response from nitric auth details endpoint: %v", err) - } - - return &authDetails.WorkOS, nil -} diff --git a/cli/internal/details/details.go b/cli/internal/details/details.go new file mode 100644 index 000000000..04251564e --- /dev/null +++ b/cli/internal/details/details.go @@ -0,0 +1,10 @@ +package details + +type AuthDetailsService interface { + GetWorkOSDetails() (*WorkOSDetails, error) +} + +type WorkOSDetails struct { + ClientID string `json:"client_id"` + ApiHostname string `json:"api_hostname"` +} diff --git a/cli/internal/details/service/details.go b/cli/internal/details/service/details.go new file mode 100644 index 000000000..45574c55b --- /dev/null +++ b/cli/internal/details/service/details.go @@ -0,0 +1,65 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/nitrictech/nitric/cli/internal/config" + detail "github.com/nitrictech/nitric/cli/internal/details" + "github.com/samber/do/v2" +) + +type AuthDetails struct { + WorkOS detail.WorkOSDetails `json:"workos"` +} + +type Service struct { + nitricBackendUrl *url.URL +} + +var _ detail.AuthDetailsService = &Service{} + +func NewService(inj do.Injector) (*Service, error) { + conf := do.MustInvoke[*config.Config](inj) + + return &Service{nitricBackendUrl: conf.GetNitricServerUrl()}, nil +} + +func (s *Service) GetWorkOSDetails() (*detail.WorkOSDetails, error) { + apiUrl, err := url.JoinPath(s.nitricBackendUrl.String(), "/auth/details") + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", apiUrl, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + + response, err := http.DefaultClient.Do(req) + if err != nil { + if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset by peer") { + return nil, fmt.Errorf("failed to connect to the Nitric API. Please check your connection and try again. If the problem persists, please contact support.") + } + return nil, fmt.Errorf("failed to connect to nitric auth details endpoint: %v", err) + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response from nitric auth details endpoint: %v", err) + } + + var authDetails AuthDetails + err = json.Unmarshal(body, &authDetails) + if err != nil { + return nil, fmt.Errorf("unexpected response from nitric auth details endpoint: %v", err) + } + + return &authDetails.WorkOS, nil +} diff --git a/cli/internal/workos/workos.go b/cli/internal/workos/workos.go index 056b42678..3fa1f2df6 100644 --- a/cli/internal/workos/workos.go +++ b/cli/internal/workos/workos.go @@ -3,10 +3,13 @@ package workos import ( "errors" "fmt" + "net/url" "time" "github.com/golang-jwt/jwt/v4" + "github.com/nitrictech/nitric/cli/internal/details" "github.com/nitrictech/nitric/cli/internal/workos/http" + "github.com/samber/do/v2" ) var ( @@ -30,17 +33,24 @@ type Tokens struct { } type WorkOSAuth struct { - tokenStore TokenStore - tokens *Tokens - httpClient *http.HttpClient + nitricBackendUrl *url.URL + tokenStore TokenStore + tokens *Tokens + httpClient *http.HttpClient } -func NewWorkOSAuth(tokenStore TokenStore, clientID string, endpoint string) *WorkOSAuth { - httpClient := http.NewHttpClient(clientID, http.WithHostname(endpoint)) +func NewWorkOSAuth(inj do.Injector) (*WorkOSAuth, error) { + details, err := do.MustInvokeAs[details.AuthDetailsService](inj).GetWorkOSDetails() + if err != nil { + return nil, err + } + + httpClient := http.NewHttpClient(details.ClientID, http.WithHostname(details.ApiHostname)) + tokenStore := do.MustInvokeAs[TokenStore](inj) tokens, _ := tokenStore.GetTokens() - return &WorkOSAuth{tokenStore: tokenStore, httpClient: httpClient, tokens: tokens} + return &WorkOSAuth{tokenStore: tokenStore, httpClient: httpClient, tokens: tokens}, nil } func (a *WorkOSAuth) Login() (*http.User, error) { diff --git a/cli/main.go b/cli/main.go index f347589a6..1809cde51 100644 --- a/cli/main.go +++ b/cli/main.go @@ -2,55 +2,37 @@ package main import ( "os" - "strings" "github.com/nitrictech/nitric/cli/cmd" "github.com/nitrictech/nitric/cli/internal/api" "github.com/nitrictech/nitric/cli/internal/config" + details_service "github.com/nitrictech/nitric/cli/internal/details/service" "github.com/nitrictech/nitric/cli/internal/workos" - "github.com/nitrictech/nitric/cli/pkg/cli" - "github.com/samber/do" - "github.com/spf13/cobra" + "github.com/nitrictech/nitric/cli/pkg/app" + "github.com/samber/do/v2" ) -func main() { +func createTokenStore(inj do.Injector) (*workos.KeyringTokenStore, error) { + config := do.MustInvoke[*config.Config](inj) + apiUrl := config.GetNitricServerUrl() - injector := do.New() + tokenStore, err := workos.NewKeyringTokenStore("nitric.v2.cli", apiUrl.String()) + if err != nil { + return nil, err + } + return tokenStore, nil +} - do.Provide(injector, func(inj *do.Injector) (workos.TokenStore, error) { - config := do.MustInvoke[*config.Config](inj) - apiUrl := config.GetNitricServerUrl() +func main() { - tokenStore, err := workos.NewKeyringTokenStore("nitric.v2.cli", apiUrl.String()) - if err != nil { - return nil, err - } - return tokenStore, nil - }) + injector := do.New() + do.Provide(injector, createTokenStore) do.Provide(injector, api.NewNitricApiClient) - - do.Provide(injector, func(inj *do.Injector) (*workos.WorkOSAuth, error) { - tokenStore := do.MustInvoke[workos.TokenStore](inj) - - apiClient := do.MustInvoke[*api.NitricApiClient](inj) - workosDetails, err := apiClient.GetWorkOSPublicDetails() - if err != nil { - if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset by peer") { - cobra.CheckErr("failed to connect to the Nitric API. Please check your connection and try again. If the problem persists, please contact support.") - } - - cobra.CheckErr(err) - } - return workos.NewWorkOSAuth(tokenStore, workosDetails.ClientID, workosDetails.ApiHostname), nil - }) - - do.Provide(injector, func(inj *do.Injector) (api.TokenProvider, error) { - return do.Invoke[*workos.WorkOSAuth](inj) - }) - - do.Provide(injector, cli.NewCLI) - do.Provide(injector, cli.NewAuthApp) + do.Provide(injector, details_service.NewService) + do.Provide(injector, workos.NewWorkOSAuth) + do.Provide(injector, app.NewNitricApp) + do.Provide(injector, app.NewAuthApp) rootCmd := cmd.NewRootCmd(injector) diff --git a/cli/pkg/cli/auth.go b/cli/pkg/app/auth.go similarity index 93% rename from cli/pkg/cli/auth.go rename to cli/pkg/app/auth.go index 6e8a3a27e..fcbae3178 100644 --- a/cli/pkg/cli/auth.go +++ b/cli/pkg/app/auth.go @@ -1,4 +1,4 @@ -package cli +package app import ( "errors" @@ -7,14 +7,14 @@ import ( "github.com/nitrictech/nitric/cli/internal/style" "github.com/nitrictech/nitric/cli/internal/style/icons" "github.com/nitrictech/nitric/cli/internal/workos" - "github.com/samber/do" + "github.com/samber/do/v2" ) type AuthApp struct { auth *workos.WorkOSAuth } -func NewAuthApp(injector *do.Injector) (*AuthApp, error) { +func NewAuthApp(injector do.Injector) (*AuthApp, error) { auth := do.MustInvoke[*workos.WorkOSAuth](injector) return &AuthApp{auth: auth}, nil } diff --git a/cli/pkg/cli/cli.go b/cli/pkg/app/nitric.go similarity index 92% rename from cli/pkg/cli/cli.go rename to cli/pkg/app/nitric.go index 01028f75c..eaeeb67e1 100644 --- a/cli/pkg/cli/cli.go +++ b/cli/pkg/app/nitric.go @@ -1,4 +1,4 @@ -package cli +package app import ( "context" @@ -21,37 +21,29 @@ import ( "github.com/nitrictech/nitric/cli/internal/plugins" "github.com/nitrictech/nitric/cli/internal/simulation" "github.com/nitrictech/nitric/cli/internal/style/colors" - "github.com/nitrictech/nitric/cli/internal/version" "github.com/nitrictech/nitric/cli/pkg/client" "github.com/nitrictech/nitric/cli/pkg/files" "github.com/nitrictech/nitric/cli/pkg/schema" "github.com/nitrictech/nitric/cli/pkg/tui" "github.com/nitrictech/nitric/engines/terraform" - "github.com/samber/do" + "github.com/samber/do/v2" "github.com/spf13/afero" ) -type CLI struct { +type NitricApp struct { config *config.Config apiClient *api.NitricApiClient } -func NewCLI(injector *do.Injector) (*CLI, error) { +func NewNitricApp(injector do.Injector) (*NitricApp, error) { config := do.MustInvoke[*config.Config](injector) apiClient := do.MustInvoke[*api.NitricApiClient](injector) - return &CLI{config: config, apiClient: apiClient}, nil -} - -// Version handles the version command logic -func (c *CLI) Version() error { - highlight := lipgloss.NewStyle().Foreground(lipgloss.Color("14")) - fmt.Printf("nitric cli version %s\n", highlight.Render(version.Version)) - return nil + return &NitricApp{config: config, apiClient: apiClient}, nil } // Templates handles the templates command logic -func (c *CLI) Templates() error { +func (c *NitricApp) Templates() error { templates, err := c.apiClient.GetTemplates() if err != nil { if errors.Is(err, api.ErrUnauthenticated) { @@ -78,7 +70,7 @@ func (c *CLI) Templates() error { } // New handles the new project creation command logic -func (c *CLI) New(projectName string, force bool) error { +func (c *NitricApp) New(projectName string, force bool) error { templates, err := c.apiClient.GetTemplates() if err != nil { if errors.Is(err, api.ErrUnauthenticated) { @@ -225,7 +217,7 @@ func (c *CLI) New(projectName string, force bool) error { } // Build handles the build command logic -func (c *CLI) Build() error { +func (c *NitricApp) Build() error { // Read the nitric.yaml file fs := afero.NewOsFs() @@ -262,7 +254,7 @@ func (c *CLI) Build() error { } // Generate handles the generate command logic -func (c *CLI) Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag bool, goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir string) error { +func (c *NitricApp) Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag bool, goOutputDir, goPackageName, pythonOutputDir, javascriptOutputDir, typescriptOutputDir string) error { // Check if at least one language flag is provided if !goFlag && !pythonFlag && !javascriptFlag && !typescriptFlag { return fmt.Errorf("at least one language flag must be specified") @@ -311,7 +303,7 @@ func (c *CLI) Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag bool, } // Edit handles the edit command logic -func (c *CLI) Edit() error { +func (c *NitricApp) Edit() error { const fileName = "nitric.yaml" listener, err := net.Listen("tcp", "localhost:0") @@ -367,7 +359,7 @@ func (c *CLI) Edit() error { } // Dev handles the dev command logic -func (c *CLI) Dev() error { +func Dev() error { // 1. Load the App Spec // Read the nitric.yaml file fs := afero.NewOsFs()