diff --git a/cmd/cloud.go b/cmd/cloud.go index feb8da39..d212ba09 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -16,6 +16,7 @@ import ( "github.com/agentuity/cli/internal/agent" "github.com/agentuity/cli/internal/deployer" + "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/ignore" "github.com/agentuity/cli/internal/project" @@ -23,7 +24,6 @@ import ( "github.com/agentuity/go-common/crypto" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/logger" - cstr "github.com/agentuity/go-common/string" "github.com/agentuity/go-common/tui" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -124,58 +124,6 @@ func ShowNewProjectImport(ctx context.Context, logger logger.Logger, cmd *cobra. var envTemplateFileNames = []string{".env.example", ".env.template"} -func readPossibleEnvTemplateFiles(baseDir string) map[string][]env.EnvLineComment { - var results map[string][]env.EnvLineComment - keys := make(map[string]bool) - for _, file := range envTemplateFileNames { - filename := filepath.Join(baseDir, file) - if !util.Exists(filename) { - continue - } - efc, err := env.ParseEnvFileWithComments(filename) - if err == nil { - if results == nil { - results = make(map[string][]env.EnvLineComment) - } - for _, ev := range efc { - if _, ok := keys[ev.Key]; !ok { - if isAgentuityEnv.MatchString(ev.Key) { - continue - } - keys[ev.Key] = true - results[file] = append(results[file], ev) - } - } - } - } - return results -} - -func appendToEnvFile(envfile string, envs []env.EnvLineComment) ([]env.EnvLineComment, error) { - le, err := env.ParseEnvFileWithComments(envfile) - if err != nil { - return nil, err - } - var buf strings.Builder - for _, ev := range le { - if ev.Comment != "" { - buf.WriteString(fmt.Sprintf("# %s\n", ev.Comment)) - } - buf.WriteString(fmt.Sprintf("%s=%s\n", ev.Key, ev.Raw)) - } - for _, ev := range envs { - if ev.Comment != "" { - buf.WriteString(fmt.Sprintf("# %s\n", ev.Comment)) - } - buf.WriteString(fmt.Sprintf("%s=%s\n", ev.Key, ev.Raw)) - le = append(le, ev) - } - if err := os.WriteFile(envfile, []byte(buf.String()), 0644); err != nil { - return nil, err - } - return le, nil -} - var border = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1).BorderForeground(lipgloss.AdaptiveColor{Light: "#999999", Dark: "#999999"}) var redDiff = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#990000", Dark: "#EE0000"}) @@ -251,13 +199,11 @@ Examples: } var err error - var le []env.EnvLineComment var projectExists bool var action func() if !context.NewProject { action = func() { - var err error projectData, err = theproject.GetProject(ctx, logger, apiUrl, token) if err != nil { if err == project.ErrProjectNotFound { @@ -282,148 +228,12 @@ Examples: ShowNewProjectImport(ctx, logger, cmd, apiUrl, token, projectId, theproject, dir, false) } - // check to see if we have any env vars that are not in the project - envfilename := filepath.Join(dir, ".env") - if tui.HasTTY && util.Exists(envfilename) { - - // attempt to see if we have any template files - templateEnvs := readPossibleEnvTemplateFiles(dir) - - le, err = env.ParseEnvFileWithComments(envfilename) - if err != nil { - errsystem.New(errsystem.ErrParseEnvironmentFile, err, - errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() - } - envFile = &deployer.EnvFile{Filepath: envfilename, Env: le} - - if len(templateEnvs) > 0 { - kvmap := make(map[string]env.EnvLineComment) - for _, ev := range le { - if isAgentuityEnv.MatchString(ev.Key) { - continue - } - kvmap[ev.Key] = ev - } - var osenv map[string]string - var addtoenvfile []env.EnvLineComment - // look to see if we have any template environment variables that are not in the .env file - for filename, evs := range templateEnvs { - for _, ev := range evs { - if _, ok := kvmap[ev.Key]; !ok { - isSecret := looksLikeSecret.MatchString(ev.Key) - if !isSecret && descriptionLookingLikeASecret(ev.Comment) { - isSecret = true - } - _ = filename - var content string - var para []string - para = append(para, tui.Warning("Missing Environment Variable\n")) - para = append(para, fmt.Sprintf("The variable %s was found in %s but not in your %s file:\n", tui.Bold(ev.Key), tui.Bold(filename), tui.Bold(".env"))) - if ev.Comment != "" { - para = append(para, tui.Muted(fmt.Sprintf("# %s", ev.Comment))) - } - if isSecret { - para = append(para, redDiff.Render(fmt.Sprintf("+ %s=%s\n", ev.Key, cstr.Mask(ev.Val)))) - } else { - para = append(para, redDiff.Render(fmt.Sprintf("+ %s=%s\n", ev.Key, ev.Val))) - } - content = lipgloss.JoinVertical(lipgloss.Left, para...) - fmt.Println(border.Render(content)) - if !tui.Ask(logger, "Would you like to add it to your .env file?", true) { - fmt.Println() - tui.ShowWarning("cancelled") - continue - } - if osenv == nil { - osenv = loadOSEnv() - } - val := promptForEnv(logger, ev.Key, isSecret, nil, osenv, ev.Val, ev.Comment) - addtoenvfile = append(addtoenvfile, env.EnvLineComment{ - EnvLine: env.EnvLine{ - Key: ev.Key, - Val: val, - Raw: val, - }, - Comment: ev.Comment, - }) - } - } - } - if len(addtoenvfile) > 0 { - le, err = appendToEnvFile(envfilename, addtoenvfile) - if err != nil { - errsystem.New(errsystem.ErrParseEnvironmentFile, err, - errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() - } - tui.ShowSuccess("added %s to your .env file", util.Pluralize(len(addtoenvfile), "environment variable", "environment variables")) - fmt.Println() - } - } - - var foundkeys []string - for _, ev := range le { - if isAgentuityEnv.MatchString(ev.Key) { - continue - } - if projectData != nil && projectData.Env != nil && projectData.Env[ev.Key] == ev.Val { - continue - } - if projectData != nil && projectData.Secrets != nil && projectData.Secrets[ev.Key] == cstr.Mask(ev.Val) { - continue - } - foundkeys = append(foundkeys, ev.Key) - } - if len(foundkeys) > 0 { - var title string - var suffix string - switch { - case len(foundkeys) < 3 && len(foundkeys) > 1: - suffix = "it" - var colorized []string - for _, key := range foundkeys { - colorized = append(colorized, tui.Bold(key)) - } - title = fmt.Sprintf("The environment variables %s from %s are not been set in the project.", strings.Join(colorized, ", "), tui.Bold(".env")) - case len(foundkeys) == 1: - suffix = "it" - title = fmt.Sprintf("The environment variable %s from %s has not been set in the project.", tui.Bold(foundkeys[0]), tui.Bold(".env")) - default: - suffix = "them" - title = fmt.Sprintf("There are %d environment variables from %s that are not set in the project.", len(foundkeys), tui.Bold(".env")) - } - fmt.Println(title) - if !tui.Ask(logger, "Would you like to set "+suffix+"?", true) { - fmt.Println() - tui.ShowWarning("cancelled") - return - } - envs, secrets := loadEnvFile(le, false) - pd, err := theproject.SetProjectEnv(ctx, logger, apiUrl, token, envs, secrets) - if err != nil { - if isCancelled(ctx) { - os.Exit(1) - } - errsystem.New(errsystem.ErrEnvironmentVariablesNotSet, err, - errsystem.WithContextMessage("Failed to set project environment variables")).ShowErrorAndExit() - } - fmt.Println() - fmt.Println() - projectData = pd // overwrite with the new version - switch { - case len(envs) > 0 && len(secrets) > 0: - tui.ShowSuccess("Environment variables and secrets added") - case len(envs) == 1: - tui.ShowSuccess("Environment variable added") - case len(envs) > 1: - tui.ShowSuccess("Environment variables added") - case len(secrets) == 1: - tui.ShowSuccess("Secret added") - case len(secrets) > 1: - tui.ShowSuccess("Secrets added") - } - fmt.Println() - } + force, _ := cmd.Flags().GetBool("force") + if !tui.HasTTY { + force = true } + // check to see if we have any env vars that are not in the project + envFile, projectData = envutil.ProcessEnvFiles(ctx, logger, dir, theproject, projectData, apiUrl, token, force) if tui.HasTTY { _, localIssues, remoteIssues, err := buildAgentTree(keys, state, context) @@ -827,6 +637,7 @@ func init() { cloudDeployCmd.Flags().StringArray("tag", nil, "Tag(s) to associate with this deployment (can be specified multiple times)") cloudDeployCmd.Flags().String("description", "", "Description for the deployment") cloudDeployCmd.Flags().String("message", "", "A shorter description for the deployment") + cloudDeployCmd.Flags().Bool("force", false, "Force the processing of environment files") cloudDeployCmd.Flags().MarkHidden("deploymentId") cloudDeployCmd.Flags().MarkHidden("ci") diff --git a/cmd/dev.go b/cmd/dev.go index 430c74fb..3e00f1bf 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -12,6 +12,7 @@ import ( "github.com/agentuity/cli/internal/bundler" "github.com/agentuity/cli/internal/dev" + "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/project" "github.com/agentuity/cli/internal/util" @@ -66,6 +67,12 @@ Examples: errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithUserMessage("Failed to validate project (%s) using the provided API key from the .env file in %s. This is most likely due to the API key being invalid or the project has been deleted.\n\nYou can import this project using the following command:\n\n"+tui.Command("project import"), theproject.Project.ProjectId, dir), errsystem.WithContextMessage(fmt.Sprintf("Failed to get project: %s", err))).ShowErrorAndExit() } + force, _ := cmd.Flags().GetBool("force") + if !tui.HasTTY { + force = true + } + _, project = envutil.ProcessEnvFiles(ctx, log, dir, theproject.Project, project, theproject.APIURL, apiKey, force) + orgId := project.OrgId port, _ := cmd.Flags().GetInt("port") @@ -275,4 +282,5 @@ func init() { devCmd.Flags().Int("port", 0, "The port to run the development server on (uses project default if not provided)") devCmd.Flags().String("server", "echo.agentuity.cloud", "the echo server to connect to") devCmd.Flags().MarkHidden("server") + devCmd.Flags().Bool("force", false, "Force the processing of environment files") } diff --git a/cmd/env.go b/cmd/env.go index a0077ab1..5859a9f6 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -12,10 +12,11 @@ import ( "strings" "syscall" + "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/project" + "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" - "github.com/agentuity/go-common/logger" cstr "github.com/agentuity/go-common/string" "github.com/agentuity/go-common/sys" "github.com/agentuity/go-common/tui" @@ -74,40 +75,6 @@ func loadEnvFile(le []env.EnvLineComment, forceSecret bool) (map[string]string, return envs, secrets } -func promptForEnv(logger logger.Logger, key string, isSecret bool, localenv map[string]string, osenv map[string]string, defaultValue string, placeholder string) string { - prompt := "Enter the value for " + key - var help string - var value string - if isSecret { - prompt = "Enter the secret value for " + key - if val, ok := localenv[key]; ok { - help = "Press enter to set as " + maxString(cstr.Mask(val), 30) + " from your .env file" - if defaultValue == "" { - defaultValue = val - } - } else if val, ok := osenv[key]; ok { - help = "Press enter to set as " + maxString(cstr.Mask(val), 30) + " from your environment" - if defaultValue == "" { - defaultValue = val - } - } else { - help = "Your input will be masked" - } - value = tui.Password(logger, prompt, help) - } else { - if placeholder == "" { - value = tui.InputWithPlaceholder(logger, prompt, placeholder, defaultValue) - } else { - value = tui.InputWithPlaceholder(logger, prompt, help, defaultValue) - } - } - - if value == "" && defaultValue != "" { - value = defaultValue - } - return value -} - func loadOSEnv() map[string]string { osenv := make(map[string]string) for _, line := range os.Environ() { @@ -251,16 +218,16 @@ Examples: } } if value == "" { - value = promptForEnv(logger, key, isSecret, localenv, osenv, "", "") + value = envutil.PromptForEnv(logger, key, isSecret, localenv, osenv, "", "") } } if key != "" && value != "" { if isSecret { secrets[key] = value - tui.ShowSuccess("%s=%s", key, maxString(cstr.Mask(value), 40)) + tui.ShowSuccess("%s=%s", key, util.MaxString(cstr.Mask(value), 40)) } else { envs[key] = value - tui.ShowSuccess("%s=%s", key, maxString(value, 40)) + tui.ShowSuccess("%s=%s", key, util.MaxString(value, 40)) } } if askMore { @@ -426,7 +393,7 @@ Examples: if !hasTTY { fmt.Printf("%s=%s\n", key, value) } else { - fmt.Printf("%s=%s\n", tui.Title(key), tui.Muted(maxString(value, 40))) + fmt.Printf("%s=%s\n", tui.Title(key), tui.Muted(util.MaxString(value, 40))) } } if len(projectData.Env) == 0 && len(projectData.Secrets) == 0 { diff --git a/cmd/project.go b/cmd/project.go index 67cd1fb6..12fd2a8e 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -11,6 +11,7 @@ import ( "sort" "syscall" + "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/mcp" "github.com/agentuity/cli/internal/organization" @@ -820,6 +821,11 @@ Examples: } ShowNewProjectImport(ctx, logger, cmd, context.APIURL, context.Token, "", context.Project, context.Dir, true) + force, _ := cmd.Flags().GetBool("force") + if !tui.HasTTY { + force = true + } + _, _ = envutil.ProcessEnvFiles(ctx, logger, context.Dir, context.Project, nil, context.APIURL, context.Token, force) }, } @@ -868,6 +874,7 @@ func init() { projectImportCmd.Flags().String("apikey", "", "The API key to use for the project") projectImportCmd.Flags().String("name", "", "The name of the project to import") projectImportCmd.Flags().String("description", "", "The description of the project to import") + projectImportCmd.Flags().Bool("force", false, "Force the processing of environment files") projectImportCmd.Flags().MarkHidden("apikey") projectImportCmd.Flags().MarkHidden("name") diff --git a/cmd/root.go b/cmd/root.go index edf1ade3..91972a53 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( "time" "github.com/agentuity/cli/internal/deployer" + "github.com/agentuity/cli/internal/envutil" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/templates" "github.com/agentuity/cli/internal/util" @@ -168,13 +169,6 @@ func initConfig() { } } -func maxString(val string, max int) string { - if len(val) > max { - return val[:max] + "..." - } - return val -} - func initScreenWithLogo() { tui.ClearScreen() tui.Logo() @@ -189,7 +183,7 @@ func createPromptHelper() deployer.PromptHelpers { PrintLock: tui.ShowLock, PrintWarning: tui.ShowWarning, Ask: tui.Ask, - PromptForEnv: promptForEnv, + PromptForEnv: envutil.PromptForEnv, } } diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go new file mode 100644 index 00000000..065e81b6 --- /dev/null +++ b/internal/envutil/envutil.go @@ -0,0 +1,325 @@ +package envutil + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/agentuity/cli/internal/deployer" + "github.com/agentuity/cli/internal/errsystem" + "github.com/agentuity/cli/internal/project" + util "github.com/agentuity/cli/internal/util" + "github.com/agentuity/go-common/env" + "github.com/agentuity/go-common/logger" + cstr "github.com/agentuity/go-common/string" + "github.com/agentuity/go-common/tui" + "github.com/charmbracelet/lipgloss" +) + +var EnvTemplateFileNames = []string{".env.example", ".env.template"} + +var border = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1).BorderForeground(lipgloss.AdaptiveColor{Light: "#999999", Dark: "#999999"}) +var redDiff = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#990000", Dark: "#EE0000"}) + +var LooksLikeSecret = looksLikeSecret +var IsAgentuityEnv = isAgentuityEnv + +var looksLikeSecret = regexp.MustCompile(`(?i)KEY|SECRET|TOKEN|PASSWORD|sk_`) +var isAgentuityEnv = regexp.MustCompile(`(?i)AGENTUITY_`) + +// ProcessEnvFiles handles .env and template env processing +func ProcessEnvFiles(ctx context.Context, logger logger.Logger, dir string, theproject *project.Project, projectData *project.ProjectData, apiUrl, token string, force bool) (*deployer.EnvFile, *project.ProjectData) { + envfilename := filepath.Join(dir, ".env") + var envFile *deployer.EnvFile + if (tui.HasTTY || force) && util.Exists(envfilename) { + // attempt to see if we have any template files + templateEnvs := ReadPossibleEnvTemplateFiles(dir) + + le, err := env.ParseEnvFileWithComments(envfilename) + if err != nil { + errsystem.New(errsystem.ErrParseEnvironmentFile, err, + errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() + } + envFile = &deployer.EnvFile{Filepath: envfilename, Env: le} + + le, err = HandleMissingTemplateEnvs(logger, dir, envfilename, le, templateEnvs, force) + if err != nil { + errsystem.New(errsystem.ErrParseEnvironmentFile, err, + errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() + } + + projectData = HandleMissingProjectEnvs(ctx, logger, le, projectData, theproject, apiUrl, token, force) + envFile.Env = le + return envFile, projectData + } + return envFile, projectData +} + +// HandleMissingTemplateEnvs handles missing envs from template files +func HandleMissingTemplateEnvs(logger logger.Logger, dir, envfilename string, le []env.EnvLineComment, templateEnvs map[string][]env.EnvLineComment, force bool) ([]env.EnvLineComment, error) { + if len(templateEnvs) == 0 { + return le, nil + } + kvmap := make(map[string]env.EnvLineComment) + for _, ev := range le { + if isAgentuityEnv.MatchString(ev.Key) { + continue + } + kvmap[ev.Key] = ev + } + var osenv map[string]string + var addtoenvfile []env.EnvLineComment + // look to see if we have any template environment variables that are not in the .env file + for filename, evs := range templateEnvs { + for _, ev := range evs { + if _, ok := kvmap[ev.Key]; !ok { + isSecret := looksLikeSecret.MatchString(ev.Key) + if !isSecret && DescriptionLookingLikeASecret(ev.Comment) { + isSecret = true + } + if !force { + var content string + var para []string + para = append(para, tui.Warning("Missing Environment Variable\n")) + para = append(para, fmt.Sprintf("The variable %s was found in %s but not in your %s file:\n", tui.Bold(ev.Key), tui.Bold(filename), tui.Bold(".env"))) + if ev.Comment != "" { + para = append(para, tui.Muted(fmt.Sprintf("# %s", ev.Comment))) + } + if isSecret { + para = append(para, redDiff.Render(fmt.Sprintf("+ %s=%s\n", ev.Key, cstr.Mask(ev.Val)))) + } else { + para = append(para, redDiff.Render(fmt.Sprintf("+ %s=%s\n", ev.Key, ev.Val))) + } + content = lipgloss.JoinVertical(lipgloss.Left, para...) + fmt.Println(border.Render(content)) + if !tui.Ask(logger, "Would you like to add it to your .env file?", true) { + fmt.Println() + tui.ShowWarning("cancelled") + continue + } + if osenv == nil { + osenv = LoadOSEnv() + } + if ev.Val == "" { + ev.Val = PromptForEnv(logger, ev.Key, isSecret, nil, osenv, ev.Val, ev.Comment) + } + ev.Raw = ev.Val + } + addtoenvfile = append(addtoenvfile, env.EnvLineComment{ + EnvLine: env.EnvLine{ + Key: ev.Key, + Val: ev.Val, + Raw: ev.Raw, + }, + Comment: ev.Comment, + }) + } + } + } + if len(addtoenvfile) > 0 { + var err error + le, err = AppendToEnvFile(envfilename, addtoenvfile) + if err != nil { + return le, err + } + if tui.HasTTY { + tui.ShowSuccess("added %s to your .env file", util.Pluralize(len(addtoenvfile), "environment variable", "environment variables")) + fmt.Println() + } + } + return le, nil +} + +// HandleMissingProjectEnvs handles missing envs in project +func HandleMissingProjectEnvs(ctx context.Context, logger logger.Logger, le []env.EnvLineComment, projectData *project.ProjectData, theproject *project.Project, apiUrl, token string, force bool) *project.ProjectData { + + if projectData == nil { + projectData = &project.ProjectData{} + } + keyvalue := map[string]string{} + for _, ev := range le { + if isAgentuityEnv.MatchString(ev.Key) { + continue + } + if projectData.Env != nil && projectData.Env[ev.Key] == ev.Val { + continue + } + if projectData.Secrets != nil && projectData.Secrets[ev.Key] == cstr.Mask(ev.Val) { + continue + } + keyvalue[ev.Key] = ev.Val + } + if len(keyvalue) > 0 { + if !force { + var title string + var suffix string + switch { + case len(keyvalue) < 3 && len(keyvalue) > 1: + suffix = "it" + var colorized []string + for key := range keyvalue { + colorized = append(colorized, tui.Bold(key)) + } + title = fmt.Sprintf("The environment variables %s from %s are not been set in the project.", strings.Join(colorized, ", "), tui.Bold(".env")) + case len(keyvalue) == 1: + var key string + for _key := range keyvalue { + key = _key + break + } + suffix = "it" + title = fmt.Sprintf("The environment variable %s from %s has not been set in the project.", tui.Bold(key), tui.Bold(".env")) + default: + suffix = "them" + title = fmt.Sprintf("There are %d environment variables from %s that are not set in the project.", len(keyvalue), tui.Bold(".env")) + } + force = tui.Ask(logger, title+"\nWould you like to set "+suffix+" now?", true) + } + if force { + for key, val := range keyvalue { + if projectData.Env == nil { + projectData.Env = make(map[string]string) + } + projectData.Env[key] = val + } + _, err := theproject.SetProjectEnv(ctx, logger, apiUrl, token, projectData.Env, projectData.Secrets) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithUserMessage("Failed to save project settings")).ShowErrorAndExit() + } + } + } + return projectData +} + +// ReadPossibleEnvTemplateFiles reads .env.example and .env.template files +func ReadPossibleEnvTemplateFiles(baseDir string) map[string][]env.EnvLineComment { + var results map[string][]env.EnvLineComment + keys := make(map[string]bool) + for _, file := range EnvTemplateFileNames { + filename := filepath.Join(baseDir, file) + if !util.Exists(filename) { + continue + } + if efc, err := env.ParseEnvFileWithComments(filename); err == nil { + if results == nil { + results = make(map[string][]env.EnvLineComment) + } + for _, ev := range efc { + if _, ok := keys[ev.Key]; !ok { + if isAgentuityEnv.MatchString(ev.Key) { + continue + } + keys[ev.Key] = true + results[file] = append(results[file], ev) + } + } + } else { + errsystem.New(errsystem.ErrParseEnvironmentFile, err, + errsystem.WithContextMessage("Error parsing .env file")).ShowErrorAndExit() + } + } + return results +} + +// AppendToEnvFile appends envs to the .env file +func AppendToEnvFile(envfile string, envs []env.EnvLineComment) ([]env.EnvLineComment, error) { + le, err := env.ParseEnvFileWithComments(envfile) + if err != nil { + return nil, err + } + var buf strings.Builder + for _, ev := range le { + if ev.Comment != "" { + buf.WriteString(fmt.Sprintf("# %s\n", ev.Comment)) + } + raw := ev.Raw + if raw == "" { + raw = ev.Val + } + buf.WriteString(fmt.Sprintf("%s=%s\n", ev.Key, raw)) + } + for _, ev := range envs { + if ev.Comment != "" { + buf.WriteString(fmt.Sprintf("# %s\n", ev.Comment)) + } + raw := ev.Raw + if raw == "" { + raw = ev.Val + } + buf.WriteString(fmt.Sprintf("%s=%s\n", ev.Key, raw)) + le = append(le, ev) + } + if err := os.WriteFile(envfile, []byte(buf.String()), 0600); err != nil { + return nil, err + } + return le, nil +} + +// Utility functions needed for env processing + +// DescriptionLookingLikeASecret checks if a description looks like a secret +func DescriptionLookingLikeASecret(description string) bool { + if description == "" { + return false + } + val := strings.ToLower(description) + if strings.Contains(val, "secret") { + return true + } + if strings.Contains(val, "password") { + return true + } + if strings.Contains(val, "key") { + return true + } + return false +} + +// LoadOSEnv loads the OS environment variables +func LoadOSEnv() map[string]string { + osenv := make(map[string]string) + for _, line := range os.Environ() { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 && !isAgentuityEnv.MatchString(parts[0]) { + osenv[parts[0]] = parts[1] + } + } + return osenv +} + +func PromptForEnv(logger logger.Logger, key string, isSecret bool, localenv map[string]string, osenv map[string]string, defaultValue string, placeholder string) string { + prompt := "Enter the value for " + key + var help string + var value string + if isSecret { + prompt = "Enter the secret value for " + key + if val, ok := localenv[key]; ok { + help = "Press enter to set as " + cstr.Mask(util.MaxString(val, 30)) + " from your .env file" + if defaultValue == "" { + defaultValue = val + } + } else if val, ok := osenv[key]; ok { + help = "Press enter to set as " + cstr.Mask(util.MaxString(val, 30)) + " from your environment" + if defaultValue == "" { + defaultValue = val + } + } else { + help = "Your input will be masked" + } + value = tui.Password(logger, prompt, help) + } else { + if placeholder == "" { + value = tui.InputWithPlaceholder(logger, prompt, help, defaultValue) + } else { + value = tui.InputWithPlaceholder(logger, prompt, placeholder, defaultValue) + } + } + + if value == "" && defaultValue != "" { + value = defaultValue + } + return value +} diff --git a/internal/util/api.go b/internal/util/api.go index 313537f7..45bfcb0c 100644 --- a/internal/util/api.go +++ b/internal/util/api.go @@ -155,6 +155,7 @@ func (c *APIClient) Do(method, pathParam string, payload interface{}, response i if err != nil { return NewAPIError(u.String(), method, 0, "", fmt.Errorf("error creating request: %w", err), traceID) } + req.Header.Set("User-Agent", UserAgent()) req.Header.Set("Content-Type", "application/json") if c.token != "" { diff --git a/internal/util/strings.go b/internal/util/strings.go index 89067dc5..de497b50 100644 --- a/internal/util/strings.go +++ b/internal/util/strings.go @@ -38,3 +38,10 @@ func Pluralize(count int, singular string, plural string) string { } return fmt.Sprintf("%d %s", count, plural) } + +func MaxString(val string, max int) string { + if len(val) > max { + return val[:max] + "..." + } + return val +}