ianedwards 2 лет назад
Родитель
Сommit
f13a45d55c

+ 83 - 0
cli/cmd/commands/app.go

@@ -37,6 +37,7 @@ import (
 )
 
 var (
+	appDeployMethod      string
 	appContainerName     string
 	appCpuMilli          int
 	appExistingPod       bool
@@ -71,6 +72,45 @@ func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
 		"the name of the deployment target for the app",
 	)
 
+	appCreateCommand := &cobra.Command{
+		Use:   "create",
+		Args:  cobra.NoArgs,
+		Short: "Creates and deploys a new app in your project.",
+		Long: fmt.Sprintf(`
+	%s
+Creates a new app in your project. You can specify the name of the app using the --name flag:
+	%s
+If no flags are specified, you will be directed to a series of required prompts to configure the app.
+`,
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter app create\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter app create --name example-app"),
+		),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return checkLoginAndRunWithConfig(cmd, cliConf, args, appCreate)
+		},
+	}
+
+	appCreateCommand.PersistentFlags().StringP(
+		flags.App_Name,
+		"n",
+		"",
+		"the name of the app",
+	)
+	appCreateCommand.PersistentFlags().StringVarP(
+		&appDeployMethod,
+		"deploy-method",
+		"m",
+		"",
+		"the deployment method for the app (docker, repo)",
+	)
+	appCreateCommand.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
+
+	flags.UseAppConfigFlags(appCreateCommand)
+	flags.UseAppBuildFlags(appCreateCommand)
+	flags.UseAppImageFlags(appCreateCommand)
+
+	appCmd.AddCommand(appCreateCommand)
+
 	appBuildCommand := &cobra.Command{
 		Use:   "build [application]",
 		Args:  cobra.MinimumNArgs(1),
@@ -316,6 +356,49 @@ func appRunFlags(appRunCmd *cobra.Command) {
 	)
 }
 
+func appCreate(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
+	name, err := cmd.Flags().GetString(flags.App_Name)
+	if err != nil {
+		return fmt.Errorf("error getting app name: %w", err)
+	}
+
+	buildValues, err := flags.AppBuildValuesFromCmd(cmd)
+	if err != nil {
+		return err
+	}
+
+	imageValues, err := flags.AppImageValuesFromCmd(cmd)
+	if err != nil {
+		return err
+	}
+
+	configValues, err := flags.AppConfigValuesFromCmd(cmd)
+	if err != nil {
+		return err
+	}
+
+	err = v2.CreateApp(ctx, v2.CreateAppInput{
+		CLIConfig:            cliConfig,
+		Client:               client,
+		AppName:              name,
+		PorterYamlPath:       porterYAML,
+		DeploymentTargetName: deploymentTargetName,
+		BuildMethod:          buildValues.BuildMethod,
+		Dockerfile:           buildValues.Dockerfile,
+		Builder:              buildValues.Builder,
+		Buildpacks:           buildValues.Buildpacks,
+		BuildContext:         buildValues.BuildContext,
+		ImageTag:             imageValues.Tag,
+		ImageRepo:            imageValues.Repository,
+		EnvGroups:            configValues.AttachEnvGroups,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to create app: %w", err)
+	}
+
+	return nil
+}
+
 func appBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, args []string) error {
 	appName := args[0]
 	if appName == "" {

+ 2 - 0
cli/cmd/commands/flags/app_config.go

@@ -7,6 +7,8 @@ import (
 )
 
 const (
+	// App_Name is the key for the app name flag
+	App_Name = "name"
 	// App_ConfigAttachEnvGroups is the key for the attach env groups flag
 	App_ConfigAttachEnvGroups = "attach-env-groups"
 )

+ 241 - 0
cli/cmd/v2/app_create.go

@@ -0,0 +1,241 @@
+package v2
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/charmbracelet/huh"
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/internal/porter_app/v2"
+)
+
+// AppDeployMethod is the deployment method for the app
+type AppDeployMethod string
+
+const (
+	// AppDeployMethod_Repo is the deployment method when source code is in a git repository and built on apply
+	AppDeployMethod_Repo AppDeployMethod = "repo"
+	// AppDeployMethod_Docker is the deployment method when app is sourced from a docker image
+	AppDeployMethod_Docker AppDeployMethod = "docker"
+)
+
+const herokuDefaultBuilder = "heroku/buildpacks:20"
+
+// CreateAppInput is the input for the CreateApp function
+type CreateAppInput struct {
+	CLIConfig config.CLIConfig
+	// Client is the Porter API client
+	Client api.Client
+	// AppName is the name of the app. If not provided, the user will be prompted to provide one
+	AppName string
+	// DeploymentMethod is the deployment method for the app, either 'repo' or 'docker'
+	DeploymentMethod string
+	// PorterYamlPath is the path to the porter.yaml file.
+	PorterYamlPath string
+	// DeploymentTargetName is the name of the deployment target, if provided
+	DeploymentTargetName string
+	// BuildMethod is the build method for the app on apply, either 'docker' or 'pack'
+	BuildMethod string
+	// Dockerfile is the path to the Dockerfile when build method is 'docker'
+	Dockerfile string
+	// Builder is the builder to use when build method is 'pack'
+	Builder string
+	// Buildpacks is the buildpacks to use when build method is 'pack'
+	Buildpacks []string
+	// BuildContext is the build context for the app, e.g. ./app
+	BuildContext string
+	// ImageTag is the image tag to use for the app build
+	ImageTag string
+	// ImageRepo is the image repository to use for the app build
+	ImageRepo string
+	// EnvGroups is a list of any env groups to attach to the app
+	EnvGroups []string
+}
+
+// CreateApp creates a new app in the Porter project, either from a Porter YAML file or through a form
+func CreateApp(ctx context.Context, inp CreateAppInput) error {
+	if inp.PorterYamlPath == "" {
+		err := createWithForm(&inp)
+		if err != nil {
+			if !errors.Is(err, huh.ErrUserAborted) {
+				return err
+			}
+
+			return nil
+		}
+	}
+
+	var builder string
+	if inp.BuildMethod == "pack" {
+		builder = herokuDefaultBuilder
+	}
+
+	patchOps := v2.PatchOperationsFromFlagValues(v2.PatchOperationsFromFlagValuesInput{
+		EnvGroups:       inp.EnvGroups,
+		BuildMethod:     inp.BuildMethod,
+		Builder:         builder,
+		BuildContext:    inp.BuildContext,
+		Buildpacks:      inp.Buildpacks,
+		Dockerfile:      inp.Dockerfile,
+		ImageRepository: inp.ImageRepo,
+		ImageTag:        inp.ImageTag,
+	})
+
+	err := Apply(ctx, ApplyInput{
+		CLIConfig:        inp.CLIConfig,
+		Client:           inp.Client,
+		PorterYamlPath:   inp.PorterYamlPath,
+		AppName:          inp.AppName,
+		ImageTagOverride: inp.ImageTag,
+		PatchOperations:  patchOps,
+	})
+	if err != nil {
+		return fmt.Errorf("error applying app: %w", err)
+	}
+
+	return nil
+}
+
+func createWithForm(inp *CreateAppInput) error {
+	color.New(color.FgGreen).Printf("Creating a new app\n\n")                                    // nolint:errcheck,gosec
+	color.New(color.FgBlue).Println("Get started by providing some information about your app.") // nolint:errcheck,gosec
+
+	var formGroups []*huh.Group
+	if inp.AppName == "" {
+		formGroups = append(formGroups, WithNameOption(inp))
+	}
+
+	var deployMethod AppDeployMethod
+	if inp.DeploymentMethod != "" {
+		method, err := validDeployMethod(inp.DeploymentMethod)
+		if err != nil {
+			return fmt.Errorf("error getting deployment method: %w", err)
+		}
+		deployMethod = method
+	}
+	if deployMethod == "" {
+		formGroups = append(formGroups, WithDeployMethodOption(inp))
+	}
+
+	if inp.BuildContext == "" {
+		formGroups = append(formGroups, WithBuildContextOption(inp))
+	}
+
+	if inp.BuildMethod == "" {
+		formGroups = append(formGroups, WithBuildMethodOption(inp))
+
+		if inp.Dockerfile == "" {
+			formGroups = append(formGroups, WithDockerfileOption(inp))
+		}
+		if len(inp.Buildpacks) == 0 {
+			formGroups = append(formGroups, WithBuildpackOptions(inp))
+		}
+	}
+
+	if inp.ImageRepo == "" || inp.ImageTag == "" {
+		formGroups = append(formGroups, WithImageOptions(inp))
+	}
+
+	if len(formGroups) > 0 {
+		form := huh.NewForm(formGroups...)
+		err := form.Run()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// CreateAppFormOption is a functional option for configuring the CreateAppInput through a form
+type CreateAppFormOption func(*CreateAppInput) *huh.Group
+
+// WithNameOption returns a form group for the app name
+func WithNameOption(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewInput().Title("App Name").CharLimit(31).Value(&inp.AppName),
+	)
+}
+
+// WithDeployMethodOption returns a form group for the deployment method
+func WithDeployMethodOption(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewSelect[string]().Title("Deployment Method").Options(
+			huh.NewOption("Docker", "docker"),
+			huh.NewOption("From Repository", "repo"),
+		).Value(&inp.DeploymentMethod),
+	)
+}
+
+// WithBuildContextOption returns a form group for the build context
+func WithBuildContextOption(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewInput().Title("Build Context").Value(&inp.BuildContext),
+	).WithHideFunc(func() bool {
+		return inp.DeploymentMethod != string(AppDeployMethod_Repo)
+	})
+}
+
+// WithBuildMethodOption returns a form group for the build method
+func WithBuildMethodOption(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewSelect[string]().Title("Build Method").Options(
+			huh.NewOption("Dockerfile", "docker"),
+			huh.NewOption("Buildpacks", "pack"),
+		).Value(&inp.BuildMethod),
+	).WithHideFunc(func() bool {
+		return inp.DeploymentMethod != string(AppDeployMethod_Repo)
+	})
+}
+
+// WithBuildpackOptions returns a form group for the buildpack options
+func WithBuildpackOptions(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewMultiSelect[string]().Title("Buildpacks").Options(
+			huh.NewOption("Node.js", "heroku/nodejs"),
+			huh.NewOption("Ruby", "heroku/ruby"),
+			huh.NewOption("Python", "heroku/python"),
+			huh.NewOption("Go", "heroku/go"),
+			huh.NewOption("Java", "heroku/java"),
+		).Value(&inp.Buildpacks),
+	).WithHideFunc(func() bool {
+		return inp.BuildMethod != "pack"
+	})
+}
+
+// WithDockerfileOption returns a form group for the Dockerfile path
+func WithDockerfileOption(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewInput().Title("Dockerfile Path").Value(&inp.Dockerfile),
+	).WithHideFunc(func() bool {
+		return inp.BuildMethod != "docker"
+	})
+}
+
+// WithImageOptions returns a form group for the image repository and tag
+func WithImageOptions(inp *CreateAppInput) *huh.Group {
+	return huh.NewGroup(
+		huh.NewInput().Title("Image Repository").Value(&inp.ImageRepo),
+		huh.NewInput().Title("Image Tag").Value(&inp.ImageTag),
+	).WithHideFunc(func() bool {
+		return inp.DeploymentMethod != string(AppDeployMethod_Docker)
+	})
+}
+
+func validDeployMethod(m string) (AppDeployMethod, error) {
+	var method AppDeployMethod
+
+	switch m {
+	case string(AppDeployMethod_Repo):
+		method = AppDeployMethod_Repo
+	case string(AppDeployMethod_Docker):
+		method = AppDeployMethod_Docker
+	default:
+		return method, fmt.Errorf("invalid deployment method: %s", method)
+	}
+
+	return method, nil
+}

+ 7 - 5
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -563,11 +563,13 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           </Banner>
           <Spacer y={1} />
         </AnimateHeight>
-        <GithubErrorBanner
-          appName={porterAppRecord.name}
-          workflowRerunError={workflowRerunError}
-          setWorkflowRerunError={setWorkflowRerunError}
-        />
+        {latestSource.type === "github" && (
+          <GithubErrorBanner
+            appName={porterAppRecord.name}
+            workflowRerunError={workflowRerunError}
+            setWorkflowRerunError={setWorkflowRerunError}
+          />
+        )}
         <TabSelector
           noBuffer
           options={tabs}

+ 33 - 19
dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx

@@ -230,24 +230,38 @@ const AppHeader: React.FC = () => {
         <Spacer y={0.5} />
         <NoShrink>
           {match(latestSource)
-            .with({ type: "github" }, () => (
-              <ImageTagContainer>
-                <Link
-                  to={gitCommitUrl}
-                  target="_blank"
-                  showTargetBlankIcon={false}
-                >
-                  <CommitIcon src={pull_request_icon} />
-                  <Code>{displayCommitSha}</Code>
-                </Link>
-              </ImageTagContainer>
-            ))
-            .with({ type: "local" }, () => (
-              <ImageTagContainer>
-                <CommitIcon src={pull_request_icon} />
-                <Code>{displayCommitSha}</Code>
-              </ImageTagContainer>
-            ))
+            .with({ type: "github" }, () =>
+              displayCommitSha ? (
+                <ImageTagContainer>
+                  <Link
+                    to={gitCommitUrl}
+                    target="_blank"
+                    showTargetBlankIcon={false}
+                  >
+                    <CommitIcon src={pull_request_icon} />
+                    <Code>{displayCommitSha}</Code>
+                  </Link>
+                </ImageTagContainer>
+              ) : latestProto.image?.tag ? (
+                renderTagBadge(latestProto.image.tag)
+              ) : null
+            )
+            .with({ type: "local" }, () =>
+              displayCommitSha ? (
+                <ImageTagContainer>
+                  <Link
+                    to={gitCommitUrl}
+                    target="_blank"
+                    showTargetBlankIcon={false}
+                  >
+                    <CommitIcon src={pull_request_icon} />
+                    <Code>{displayCommitSha}</Code>
+                  </Link>
+                </ImageTagContainer>
+              ) : latestProto.image?.tag ? (
+                renderTagBadge(latestProto.image.tag)
+              ) : null
+            )
             .with({ type: "docker-registry" }, (s) =>
               renderTagBadge(s.image.tag)
             )
@@ -256,7 +270,7 @@ const AppHeader: React.FC = () => {
         <Spacer y={0.5} />
       </LatestDeployContainer>
       <Spacer y={0.5} />
-      <GHStatusBanner />
+      {latestSource.type === "github" && <GHStatusBanner />}
       <Spacer y={0.5} />
     </>
   );

+ 18 - 5
go.mod

@@ -73,6 +73,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0
 	github.com/briandowns/spinner v1.18.1
+	github.com/charmbracelet/huh v0.3.0
 	github.com/cloudflare/cloudflare-go v0.76.0
 	github.com/evanphx/json-patch/v5 v5.9.0
 	github.com/glebarez/sqlite v1.6.0
@@ -112,6 +113,7 @@ require (
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
 	github.com/OneOfOne/xxhash v1.2.8 // indirect
 	github.com/agnivade/levenshtein v1.1.1 // indirect
+	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
 	github.com/aws/aws-sdk-go-v2/config v1.15.9 // indirect
 	github.com/aws/aws-sdk-go-v2/credentials v1.12.4 // indirect
@@ -126,8 +128,14 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 // indirect
 	github.com/aws/smithy-go v1.11.2 // indirect
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/catppuccin/go v0.2.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+	github.com/charmbracelet/bubbles v0.18.0 // indirect
+	github.com/charmbracelet/bubbletea v0.25.0 // indirect
+	github.com/charmbracelet/lipgloss v0.10.0 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
+	github.com/containerd/console v1.0.4 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
@@ -153,6 +161,11 @@ require (
 	github.com/launchdarkly/go-semver v1.0.2 // indirect
 	github.com/launchdarkly/go-server-sdk-evaluation/v2 v2.0.2 // indirect
 	github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
+	github.com/mattn/go-localereader v0.0.1 // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+	github.com/muesli/cancelreader v0.2.2 // indirect
+	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/muesli/termenv v0.15.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/nats-io/nats-server/v2 v2.9.15 // indirect
 	github.com/nats-io/nkeys v0.3.0 // indirect
@@ -293,7 +306,7 @@ require (
 	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/mattn/go-isatty v0.0.18 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@@ -323,7 +336,7 @@ require (
 	github.com/prometheus/common v0.39.0 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 // indirect
-	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/rubenv/sql-migrate v1.2.0 // indirect
 	github.com/russross/blackfriday v1.6.0 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
@@ -347,9 +360,9 @@ require (
 	go.opencensus.io v0.24.0 // indirect
 	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
 	golang.org/x/mod v0.8.0 // indirect
-	golang.org/x/sync v0.2.0
-	golang.org/x/sys v0.17.0 // indirect
-	golang.org/x/term v0.17.0 // indirect
+	golang.org/x/sync v0.6.0
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/term v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect

+ 37 - 9
go.sum

@@ -229,6 +229,8 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl
 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI=
 github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU=
+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.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -277,6 +279,8 @@ github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnw
 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 h1:p2I85zYI9z5/c/3Q0LiO3RtNXcmXHTtJfml/hV16zNg=
 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04/go.mod h1:Z+bXnIbhKJYSvxNwsNnwde7pDKxuqlEZCbUBoTwAqf0=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+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/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
 github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
@@ -321,6 +325,8 @@ github.com/buildpacks/pack v0.27.0 h1:diMvn/aR0wbfs0ke7DOBtrkFzJqDw+moJPTWLdUTuh
 github.com/buildpacks/pack v0.27.0/go.mod h1:ifPVxBoY2EKbSrA8Hkyy0YFfSGCzyYnzlyjrLsxxAIY=
 github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc=
 github.com/bytecodealliance/wasmtime-go v0.36.0 h1:B6thr7RMM9xQmouBtUqm1RpkJjuLS37m6nxX+iwsQSc=
+github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
+github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
 github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
@@ -333,7 +339,15 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
 github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
 github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg=
+github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
+github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
+github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
+github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
 github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw=
+github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
+github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
+github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
+github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
 github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU=
 github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
 github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
@@ -392,6 +406,8 @@ github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4g
 github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
 github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
+github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -1283,14 +1299,17 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
-github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -1393,8 +1412,16 @@ github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOA
 github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8=
 github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s=
 github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@@ -1596,8 +1623,9 @@ github.com/riandyrn/otelchi v0.5.1/go.mod h1:ZxVxNEl+jQ9uHseRYIxKWRb3OY8YXFEu+Ek
 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -2144,8 +2172,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2278,8 +2306,8 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2288,8 +2316,8 @@ golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9sn
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 2 - 2
go.work.sum

@@ -418,8 +418,6 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/ashanbrown/forbidigo v1.2.0 h1:RMlEFupPCxQ1IogYOQUnIQwGEUGK8g5vAPMRyJoSxbc=
 github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde h1:YOsoVXsZQPA9aOTy1g0lAJv5VzZUvwQuZqug8XPeqfM=
-github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
-github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns=
@@ -1034,6 +1032,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=