diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 901c33c4f..186a48167 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -155,7 +155,17 @@ func makeComposeUpCmd() *cobra.Command { deploy, project, err := cli.ComposeUp(ctx, project, client, provider, upload, mode.Value()) if err != nil { - return handleComposeUpErr(ctx, err, project, provider) + retry, err2 := handleComposeUpErr(ctx, err, project, provider) + if err2 != nil { + return err2 + } + + if retry { + deploy, project, err = cli.ComposeUp(ctx, project, client, provider, upload, mode.Value()) + if err != nil { + return err + } + } } if len(deploy.Services) == 0 { @@ -218,34 +228,67 @@ func makeComposeUpCmd() *cobra.Command { return composeUpCmd } -func handleComposeUpErr(ctx context.Context, err error, project *compose.Project, provider cliClient.Provider) error { +func handleComposeUpErr(ctx context.Context, err error, project *compose.Project, provider cliClient.Provider) (bool, error) { if errors.Is(err, types.ErrComposeFileNotFound) { // TODO: generate a compose file based on the current project printDefangHint("To start a new project, do:", "new") + return false, err } + term.Error("Error:", cliClient.PrettyError(err)) if nonInteractive { - return err + return false, err } if strings.Contains(err.Error(), "maximum number of projects") { - if projectName, err2 := provider.RemoteProjectName(ctx); err2 == nil { - term.Error("Error:", cliClient.PrettyError(err)) - if _, err := cli.InteractiveComposeDown(ctx, provider, projectName); err != nil { - term.Debug("ComposeDown failed:", err) - printDefangHint("To deactivate a project, do:", "compose down --project-name "+projectName) - } else { - // TODO: actually do the "compose up" (because that's what the user intended in the first place) - printDefangHint("To try deployment again, do:", "compose up") - } - return nil + projectName, err2 := provider.RemoteProjectName(ctx) + if err2 != nil { + term.Warn("Failed to fetch remote project name for interactive down", err2) + return false, err2 + } + if _, err2 := cli.InteractiveComposeDown(ctx, provider, projectName); err2 != nil { + term.Debug("ComposeDown failed:", err2) + printDefangHint("To deactivate a project, do:", "compose down --project-name "+projectName) + return false, err2 + } + return true, nil + } + + var missingConfigErr *compose.ErrMissingConfig + if errors.As(err, &missingConfigErr) { + // printDefangHint("To fix the missing config, do:", "defang config set ") + missingNames := missingConfigErr.Names() + term.Warn("missing config names: ", missingNames) + err2 := InteractiveConfigSet(ctx, project, provider, missingNames) + if err2 != nil { + term.Warn("Failed to interactively set missing config", err2) + return true, err2 } - return err } - term.Error("Error:", cliClient.PrettyError(err)) track.Evt("Debug Prompted", P("composeErr", err)) - return cli.InteractiveDebugForClientError(ctx, client, project, err) + return false, cli.InteractiveDebugForClientError(ctx, client, project, err) +} + +func InteractiveConfigSet(ctx context.Context, project *compose.Project, provider cliClient.Provider, missingNames []string) error { + for _, name := range missingNames { + var value string + message := fmt.Sprintf("Enter value for %q: ", name) + prompt := &survey.Input{ + Message: message, + Default: os.Getenv(name), + } + err := survey.AskOne(prompt, &value) + if err != nil { + return err + } + err = cli.ConfigSet(ctx, project.Name, provider, name, value) + if err != nil { + return err + } + } + + return nil } func handleTailAndMonitorErr(ctx context.Context, err error, client *cliClient.GrpcClient, debugConfig cli.DebugConfig) { diff --git a/src/pkg/cli/compose/validation.go b/src/pkg/cli/compose/validation.go index 378cf241f..761f148c6 100644 --- a/src/pkg/cli/compose/validation.go +++ b/src/pkg/cli/compose/validation.go @@ -23,6 +23,10 @@ type ListConfigNamesFunc func(context.Context) ([]string, error) type ErrMissingConfig []string +func (e ErrMissingConfig) Names() []string { + return ([]string)(e) +} + func (e ErrMissingConfig) Error() string { return fmt.Sprintf("missing configs %q (https://docs.defang.io/docs/concepts/configuration)", ([]string)(e)) } diff --git a/src/pkg/mcp/tools/common.go b/src/pkg/mcp/tools/common.go index 30a8392e2..cae73a94c 100644 --- a/src/pkg/mcp/tools/common.go +++ b/src/pkg/mcp/tools/common.go @@ -3,6 +3,7 @@ package tools import ( "crypto/rand" "encoding/base64" + "errors" "regexp" "strings" @@ -53,7 +54,8 @@ func HandleTermsOfServiceError(err error) *mcp.CallToolResult { } func HandleConfigError(err error) *mcp.CallToolResult { - if strings.Contains(err.Error(), "missing configs") { + var missingConfigErr *compose.ErrMissingConfig + if errors.As(err, &missingConfigErr) { mcpResult := mcp.NewToolResultErrorFromErr("The operation failed due to missing configs not being set. Please use the Defang tool called set_config to set the variable.", err) term.Debugf("MCP output error: %v", mcpResult) return mcpResult