diff --git a/README.md b/README.md
index 0c73cd625..b3936ff56 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,8 @@ Documentation for all available commands:
   (alias: nitric down)
 - nitric stack list : List all stacks in the project
 - nitric stack new [stackName] [providerName] : Create a new Nitric stack
+- nitric stack preview [-s stack] : Preview the updates for a deployed stack
+  (alias: nitric preview)
 - nitric stack update [-s stack] : Create or update a deployed stack
   (alias: nitric up)
 - nitric start : Run nitric services locally for development and testing
diff --git a/cmd/stack.go b/cmd/stack.go
index 276394605..a151edbe7 100644
--- a/cmd/stack.go
+++ b/cmd/stack.go
@@ -41,9 +41,11 @@ import (
 	"github.com/nitrictech/cli/pkg/view/tui/commands/build"
 	stack_down "github.com/nitrictech/cli/pkg/view/tui/commands/stack/down"
 	stack_new "github.com/nitrictech/cli/pkg/view/tui/commands/stack/new"
+	stack_preview "github.com/nitrictech/cli/pkg/view/tui/commands/stack/preview"
 	stack_select "github.com/nitrictech/cli/pkg/view/tui/commands/stack/select"
 	stack_up "github.com/nitrictech/cli/pkg/view/tui/commands/stack/up"
 	"github.com/nitrictech/cli/pkg/view/tui/components/list"
+	"github.com/nitrictech/cli/pkg/view/tui/components/listprompt"
 	"github.com/nitrictech/cli/pkg/view/tui/components/view"
 	"github.com/nitrictech/cli/pkg/view/tui/teax"
 	deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1"
@@ -56,6 +58,7 @@ var (
 	noBuilder     bool
 	forceNewStack bool
 	envFile       string
+	skipPreview   bool
 )
 
 var stackCmd = &cobra.Command{
@@ -303,13 +306,39 @@ var stackUpdateCmd = &cobra.Command{
 		attributesStruct, err := structpb.NewStruct(attributes)
 		tui.CheckErr(err)
 
+		if !skipPreview && !isNonInteractive() {
+			eventChan, errorChan := deploymentClient.Preview(&deploymentspb.DeploymentPreviewRequest{
+				Spec:        spec,
+				Attributes:  attributesStruct,
+				Interactive: true,
+			})
+
+			stackPreview := stack_preview.New(stackConfig.Provider, stackConfig.Name, eventChan, providerStdout, errorChan)
+			_, err = teax.NewProgram(stackPreview).Run()
+
+			confirmModel := stack_preview.NewConfirmDeployment(listprompt.ListPromptArgs{
+				Items:  list.StringsToListItems([]string{"confirm deployment", "cancel"}),
+				Tag:    "deploy",
+				Prompt: "Would you like to perform the deployment?",
+			})
+
+			selection, err := teax.NewProgram(confirmModel).Run()
+			tui.CheckErr(err)
+
+			stackSelection = selection.(stack_preview.ConfirmDeploymentModel).Choice()
+			if stackSelection == "cancel" {
+				fmt.Println("Cancelled")
+				return
+			}
+		}
+
 		eventChan, errorChan := deploymentClient.Up(&deploymentspb.DeploymentUpRequest{
 			Spec:        spec,
 			Attributes:  attributesStruct,
 			Interactive: true,
 		})
 
-		// Step 5b. Communicate with server to share progress of ...
+		// Step 5d. Communicate with server to share progress of update
 		if isNonInteractive() {
 			providerErrorDetected := false
 
@@ -362,7 +391,7 @@ var stackUpdateCmd = &cobra.Command{
 			}
 		} else {
 			// interactive environment
-			// Step 5c. Start the stack up view
+			// Step 5e. Start the stack up view
 			stackUp := stack_up.New(stackConfig.Provider, stackConfig.Name, eventChan, providerStdout, errorChan)
 			_, err = teax.NewProgram(stackUp).Run()
 			tui.CheckErr(err)
@@ -562,6 +591,268 @@ nitric stack down -s aws -y`,
 	Args: cobra.ExactArgs(0),
 }
 
+var stackPreviewCommand = &cobra.Command{
+	Use:     "preview [-s stack]",
+	Short:   "Preview the updates for a deployed stack",
+	Long:    `Preview the updates a deployed stack`,
+	Example: `nitric stack preview -s aws`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fs := afero.NewOsFs()
+
+		stackFiles, err := stack.GetAllStackFiles(fs)
+		tui.CheckErr(err)
+
+		if len(stackFiles) == 0 {
+			tui.CheckErr(fmt.Errorf("no stacks found in project, to create a new one run `nitric stack new`"))
+		}
+
+		// Step 0. Get the stack file, or prompt if more than 1.
+		stackSelection := stackFlag
+
+		if isNonInteractive() {
+			if len(stackFiles) > 1 && stackSelection == "" {
+				tui.CheckErr(fmt.Errorf("multiple stacks found in project, please specify one with -s"))
+			}
+		}
+
+		if stackSelection == "" {
+			if len(stackFiles) > 1 {
+				stackList := make([]list.ListItem, len(stackFiles))
+
+				for i, stackFile := range stackFiles {
+					stackName, err := stack.GetStackNameFromFileName(stackFile)
+					tui.CheckErr(err)
+					stackConfig, err := stack.ConfigFromName[map[string]any](fs, stackName)
+					tui.CheckErr(err)
+					stackList[i] = stack_select.StackListItem{
+						Name:     stackConfig.Name,
+						Provider: stackConfig.Provider,
+					}
+				}
+
+				promptModel := stack_select.New(stack_select.Args{
+					Prompt:    "Which stack would you like to preview?",
+					StackList: stackList,
+				})
+
+				selection, err := teax.NewProgram(promptModel).Run()
+				tui.CheckErr(err)
+				stackSelection = selection.(stack_select.Model).Choice()
+				if stackSelection == "" {
+					return
+				}
+			} else {
+				stackSelection, err = stack.GetStackNameFromFileName(stackFiles[0])
+				tui.CheckErr(err)
+			}
+		}
+
+		stackConfig, err := stack.ConfigFromName[map[string]any](fs, stackSelection)
+		tui.CheckErr(err)
+
+		if !isNonInteractive() {
+			_ = pulumi.EnsurePulumiPassphrase(fs)
+		}
+
+		// print provider version check
+		update.PrintOutdatedProviderWarning(stackConfig.Provider)
+
+		proj, err := project.FromFile(fs, "")
+		tui.CheckErr(err)
+
+		// Step 0a. Locate/Download provider where applicable.
+		prov, err := provider.NewProvider(stackConfig.Provider, proj, fs)
+		tui.CheckErr(err)
+
+		err = prov.Install()
+		tui.CheckErr(err)
+
+		// Build the Project's Services (Containers)
+		buildUpdates, err := proj.BuildServices(fs, !noBuilder)
+		tui.CheckErr(err)
+
+		batchBuildUpdates, err := proj.BuildBatches(fs, !noBuilder)
+		tui.CheckErr(err)
+
+		allBuildUpdates := lo.FanIn(10, buildUpdates, batchBuildUpdates)
+
+		if isNonInteractive() {
+			fmt.Println("building project services")
+			for _, service := range proj.GetServices() {
+				fmt.Printf("service matched '%s', auto-naming this service '%s'\n", service.GetFilePath(), service.Name)
+			}
+
+			// non-interactive environment
+			for update := range allBuildUpdates {
+				for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") {
+					fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line)
+				}
+			}
+		} else {
+			prog := teax.NewProgram(build.NewModel(allBuildUpdates, "Building Services"))
+			// blocks but quits once the above updates channel is closed by the build process
+			buildModel, err := prog.Run()
+			tui.CheckErr(err)
+			if buildModel.(build.Model).Err != nil {
+				tui.CheckErr(fmt.Errorf("error building services"))
+			}
+		}
+
+		// Step 2. Start the collectors and containers (respectively in pairs)
+		// Step 3. Merge requirements from collectors into a specification
+		serviceRequirements, err := proj.CollectServicesRequirements()
+		tui.CheckErr(err)
+
+		batchRequirements, err := proj.CollectBatchRequirements()
+		tui.CheckErr(err)
+
+		additionalEnvFiles := []string{}
+
+		if envFile != "" {
+			additionalEnvFiles = append(additionalEnvFiles, envFile)
+		}
+
+		envVariables, err := env.ReadLocalEnv(additionalEnvFiles...)
+		if err != nil && os.IsNotExist(err) {
+			if !os.IsNotExist(err) {
+				tui.CheckErr(err)
+			}
+			// If it doesn't exist set blank
+			envVariables = map[string]string{}
+		}
+
+		// Allow Beta providers to be run if 'beta-providers' is enabled in preview flags
+		if slices.Contains(proj.Preview, preview.Feature_BetaProviders) {
+			envVariables["NITRIC_BETA_PROVIDERS"] = "true"
+		}
+
+		spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements)
+		tui.CheckErr(err)
+
+		migrationImageContexts, err := collector.GetMigrationImageBuildContexts(serviceRequirements, batchRequirements, fs)
+		tui.CheckErr(err)
+		// Build images from contexts and provide updates on the builds
+
+		if len(migrationImageContexts) > 0 {
+			migrationBuildUpdates, err := project.BuildMigrationImages(fs, migrationImageContexts, !noBuilder)
+			tui.CheckErr(err)
+
+			if isNonInteractive() {
+				fmt.Println("building project migration images")
+				// non-interactive environment
+				for update := range migrationBuildUpdates {
+					for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") {
+						fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line)
+					}
+				}
+			} else {
+				prog := teax.NewProgram(build.NewModel(migrationBuildUpdates, "Building Database Migrations"))
+				// blocks but quits once the above updates channel is closed by the build process
+				buildModel, err := prog.Run()
+				tui.CheckErr(err)
+				if buildModel.(build.Model).Err != nil {
+					tui.CheckErr(fmt.Errorf("error building services"))
+				}
+			}
+		}
+
+		providerStdout := make(chan string)
+
+		// Step 4. Start the deployment provider server
+		providerAddress, err := prov.Start(&provider.StartOptions{
+			Env:    envVariables,
+			StdOut: providerStdout,
+			StdErr: providerStdout,
+		})
+		tui.CheckErr(err)
+		defer func() {
+			err := prov.Stop()
+			tui.CheckErr(err)
+		}()
+
+		// Step 5a. Send specification to provider for deployment
+		deploymentClient := provider.NewDeploymentClient(providerAddress, true)
+
+		attributes := map[string]interface{}{}
+
+		attributes["stack"] = stackConfig.Name
+		attributes["project"] = proj.Name
+
+		for k, v := range stackConfig.Config {
+			attributes[k] = v
+		}
+
+		attributesStruct, err := structpb.NewStruct(attributes)
+		tui.CheckErr(err)
+
+		eventChan, errorChan := deploymentClient.Preview(&deploymentspb.DeploymentPreviewRequest{
+			Spec:        spec,
+			Attributes:  attributesStruct,
+			Interactive: true,
+		})
+
+		// Step 5b. Communicate with server to share progress of ...
+		if isNonInteractive() {
+			providerErrorDetected := false
+
+			fmt.Printf("Previewing %s stack with provider %s\n", stackConfig.Name, stackConfig.Provider)
+			go func() {
+				for update := range errorChan {
+					fmt.Printf("Error: %s\n", update)
+					providerErrorDetected = true
+				}
+			}()
+
+			go func() {
+				for outMessage := range providerStdout {
+					fmt.Printf("%s: %s\n", stackConfig.Provider, outMessage)
+				}
+			}()
+
+			// non-interactive environment
+			for update := range eventChan {
+				switch content := update.Content.(type) {
+				case *deploymentspb.DeploymentPreviewEvent_Message:
+					fmt.Printf("%s\n", content.Message)
+				case *deploymentspb.DeploymentPreviewEvent_Update:
+					updateResType := ""
+					updateResName := ""
+					if content.Update.Id != nil {
+						updateResType = content.Update.Id.Type.String()
+						updateResName = content.Update.Id.Name
+					}
+
+					if updateResType == "" {
+						updateResType = "Stack"
+					}
+					if updateResName == "" {
+						updateResName = stackConfig.Name
+					}
+					if content.Update.SubResource != "" {
+						updateResName = fmt.Sprintf("%s:%s", updateResName, content.Update.SubResource)
+					}
+
+					fmt.Printf("%s:%s [%s]:%s %s\n", updateResType, updateResName, content.Update.Action, content.Update.Status, content.Update.Message)
+				case *deploymentspb.DeploymentPreviewEvent_Result:
+					fmt.Printf("\nResult: %s\n", content.Result.GetText())
+				}
+			}
+
+			// ensure the process exits with a non-zero status code after all messages are processed
+			if providerErrorDetected {
+				os.Exit(1)
+			}
+		} else {
+			// interactive environment
+			// Step 5c. Start the stack preview view
+			stackPreview := stack_preview.New(stackConfig.Provider, stackConfig.Name, eventChan, providerStdout, errorChan)
+			_, err = teax.NewProgram(stackPreview).Run()
+		}
+	},
+	Args:    cobra.MinimumNArgs(0),
+	Aliases: []string{"preview"},
+}
+
 var stackListCmd = &cobra.Command{
 	Use:   "list",
 	Short: "List all stacks in the project",
@@ -635,6 +926,7 @@ func init() {
 	stackUpdateCmd.Flags().BoolVarP(&noBuilder, "no-builder", "", false, "don't create a buildx container")
 	stackUpdateCmd.Flags().StringVarP(&envFile, "env-file", "e", "", "--env-file config/.my-env")
 	stackUpdateCmd.Flags().BoolVarP(&forceStack, "force", "f", false, "force override previous deployment")
+	stackUpdateCmd.Flags().BoolVarP(&skipPreview, "skip-preview", "", false, "skips the preview step of the deployment")
 	tui.CheckErr(AddOptions(stackUpdateCmd, false))
 
 	// Delete Stack (Down)
@@ -642,6 +934,13 @@ func init() {
 	stackDeleteCmd.Flags().BoolVarP(&confirmDown, "yes", "y", false, "confirm the destruction of the stack")
 	tui.CheckErr(AddOptions(stackDeleteCmd, false))
 
+	// Preview Stack (Preview)
+	stackCmd.AddCommand(tui.AddDependencyCheck(stackPreviewCommand, tui.RequireContainerBuilder))
+	stackPreviewCommand.Flags().BoolVarP(&noBuilder, "no-builder", "", false, "don't create a buildx container")
+	stackPreviewCommand.Flags().StringVarP(&envFile, "env-file", "e", "", "--env-file config/.my-env")
+	stackPreviewCommand.Flags().BoolVarP(&forceStack, "force", "f", false, "force override previous deployment")
+	tui.CheckErr(AddOptions(stackPreviewCommand, false))
+
 	// List Stacks
 	stackCmd.AddCommand(stackListCmd)
 
@@ -650,4 +949,5 @@ func init() {
 
 	addAlias("stack update", "up", true)
 	addAlias("stack down", "down", true)
+	addAlias("stack preview", "preview", true)
 }
diff --git a/go.mod b/go.mod
index 35254abf2..06db5f08e 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,6 @@ replace github.com/mattn/go-ieproxy => github.com/darthShadow/go-ieproxy v0.0.0-
 require github.com/golangci/golangci-lint v1.61.0
 
 require (
-	github.com/AlecAivazis/survey/v2 v2.3.6
 	github.com/asdine/storm v2.1.2+incompatible
 	github.com/aws/aws-sdk-go v1.44.175 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -28,7 +27,7 @@ require (
 	github.com/spf13/cobra v1.8.1
 	github.com/valyala/fasthttp v1.55.0
 	golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
-	golang.org/x/mod v0.21.0 // indirect
+	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/oauth2 v0.22.0 // indirect
 	google.golang.org/grpc v1.66.0
 	gopkg.in/yaml.v2 v2.4.0
@@ -52,11 +51,12 @@ require (
 	github.com/olahol/melody v1.1.3
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/samber/lo v1.38.1
+	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/afero v1.11.0
 	github.com/stretchr/testify v1.9.0
 	github.com/wk8/go-ordered-map/v2 v2.1.8
 	go.etcd.io/bbolt v1.3.6
-	golang.org/x/sync v0.8.0
+	golang.org/x/sync v0.10.0
 	google.golang.org/protobuf v1.34.2
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -115,7 +115,7 @@ require (
 	github.com/denis-tingaikin/go-header v0.5.0 // indirect
 	github.com/distribution/reference v0.6.0 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
-	github.com/fatih/color v1.17.0 // indirect
+	github.com/fatih/color v1.18.0 // indirect
 	github.com/fatih/structtag v1.2.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/firefart/nonamedreturns v1.0.5 // indirect
@@ -173,7 +173,6 @@ require (
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/julz/importas v0.1.0 // indirect
 	github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect
-	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kisielk/errcheck v1.7.0 // indirect
 	github.com/kkHAIKE/contextcheck v1.1.5 // indirect
 	github.com/klauspost/compress v1.17.9 // indirect
@@ -197,7 +196,6 @@ require (
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/mgechev/revive v1.3.9 // indirect
-	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -238,7 +236,6 @@ require (
 	github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
 	github.com/securego/gosec/v2 v2.21.2 // indirect
 	github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
-	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/sivchari/containedctx v1.0.3 // indirect
 	github.com/sivchari/tenv v1.10.0 // indirect
 	github.com/sonatard/noctx v0.0.2 // indirect
@@ -282,14 +279,14 @@ require (
 	go.uber.org/automaxprocs v1.5.3 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
 	go.uber.org/zap v1.24.0 // indirect
-	golang.org/x/crypto v0.27.0 // indirect
+	golang.org/x/crypto v0.31.0 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
-	golang.org/x/net v0.28.0 // indirect
-	golang.org/x/sys v0.25.0 // indirect
-	golang.org/x/term v0.24.0 // indirect
-	golang.org/x/text v0.18.0 // indirect
+	golang.org/x/net v0.33.0 // indirect
+	golang.org/x/sys v0.28.0 // indirect
+	golang.org/x/term v0.27.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
 	golang.org/x/time v0.6.0 // indirect
-	golang.org/x/tools v0.24.0 // indirect
+	golang.org/x/tools v0.27.0 // indirect
 	google.golang.org/api v0.196.0 // indirect
 	google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
diff --git a/go.sum b/go.sum
index 13015e43c..32649db9a 100644
--- a/go.sum
+++ b/go.sum
@@ -53,8 +53,6 @@ github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8
 github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0=
 github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY=
 github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM=
-github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
-github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
 github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM=
 github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns=
 github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ=
@@ -79,8 +77,6 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
 github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
-github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
 github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA=
 github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ=
 github.com/Sereal/Sereal v0.0.0-20221130110801-16a4f76670cd h1:rP6LH3aVJTIxgTA3q79sSfnt8DvOlt17IRAklRBN+xo=
@@ -179,8 +175,6 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
-github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo=
 github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc=
 github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c=
@@ -211,8 +205,8 @@ github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQt
 github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
-github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -431,8 +425,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
-github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
-github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -476,8 +468,6 @@ github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY=
 github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0=
 github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos=
 github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0=
 github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ=
@@ -533,7 +523,6 @@ github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859
 github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -557,9 +546,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A=
 github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU=
-github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
-github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -750,7 +736,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -866,8 +851,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -912,8 +897,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -955,8 +940,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 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=
@@ -979,8 +964,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
 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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 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=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1031,7 +1016,6 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1042,17 +1026,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 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.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
+golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
 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=
@@ -1063,8 +1046,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1132,8 +1115,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
 golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
+golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/pkg/provider/client.go b/pkg/provider/client.go
index 6b81eb0a5..a2b988a78 100644
--- a/pkg/provider/client.go
+++ b/pkg/provider/client.go
@@ -135,6 +135,49 @@ func (p *DeploymentClient) Down(deploymentRequest *deploy.DeploymentDownRequest)
 	return eventChan, errorChan
 }
 
+func (p *DeploymentClient) Preview(deploymentRequest *deploy.DeploymentPreviewRequest) (<-chan *deploy.DeploymentPreviewEvent, <-chan error) {
+	eventChan := make(chan *deploy.DeploymentPreviewEvent)
+	errorChan := make(chan error)
+
+	go func() {
+		defer close(eventChan)
+
+		conn, err := p.dialConnection()
+		if err != nil {
+			errorChan <- fmt.Errorf("failed to connect to provider at %s: %w", p.address, err)
+			return
+		}
+		defer conn.Close()
+
+		client := deploy.NewDeploymentClient(conn)
+
+		op, err := client.Preview(context.Background(), deploymentRequest)
+		if err != nil {
+			errorChan <- err
+			return
+		}
+
+		for {
+			evt, err := op.Recv()
+			if err != nil {
+				if !errors.Is(err, io.EOF) {
+					if st, ok := status.FromError(err); ok {
+						errorChan <- fmt.Errorf("%s", st.Message())
+					} else {
+						errorChan <- err
+					}
+				}
+
+				break
+			}
+
+			eventChan <- evt
+		}
+	}()
+
+	return eventChan, errorChan
+}
+
 func NewDeploymentClient(address string, interactive bool) *DeploymentClient {
 	return &DeploymentClient{
 		address:     address,
diff --git a/pkg/view/tui/commands/stack/preview/confirm_deployment.go b/pkg/view/tui/commands/stack/preview/confirm_deployment.go
new file mode 100644
index 000000000..2431d2ab0
--- /dev/null
+++ b/pkg/view/tui/commands/stack/preview/confirm_deployment.go
@@ -0,0 +1,69 @@
+package stack_preview
+
+import (
+	"github.com/charmbracelet/bubbles/key"
+	tea "github.com/charmbracelet/bubbletea"
+	tui "github.com/nitrictech/cli/pkg/view/tui"
+	"github.com/nitrictech/cli/pkg/view/tui/components/listprompt"
+	"github.com/nitrictech/cli/pkg/view/tui/teax"
+)
+
+type ConfirmDeploymentModel struct {
+	windowSize tea.WindowSizeMsg
+
+	confirmPrompt listprompt.ListPrompt
+}
+
+// Init initializes the model, used by Bubbletea
+func (m ConfirmDeploymentModel) Init() tea.Cmd {
+	return nil
+}
+
+// Update the model based on a message
+func (m ConfirmDeploymentModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var cmd tea.Cmd
+
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		m.windowSize = msg
+
+		if m.windowSize.Height < 7 {
+			m.confirmPrompt.SetMinimized(true)
+			m.confirmPrompt.SetMaxDisplayedItems(m.windowSize.Height - 1)
+		} else {
+			m.confirmPrompt.SetMinimized(false)
+			maxItems := ((m.windowSize.Height - 1) / 3) // make room for the exit message
+			m.confirmPrompt.SetMaxDisplayedItems(maxItems)
+		}
+
+		return m, nil
+	case tea.KeyMsg:
+		switch {
+		case key.Matches(msg, tui.KeyMap.Quit):
+			return m, teax.Quit
+		}
+	}
+
+	m.confirmPrompt, cmd = m.confirmPrompt.UpdateListPrompt(msg)
+	if m.confirmPrompt.IsComplete() {
+		return m, teax.Quit
+	}
+
+	return m, cmd
+}
+
+func (m ConfirmDeploymentModel) View() string {
+	return m.confirmPrompt.View()
+}
+
+func (m ConfirmDeploymentModel) Choice() string {
+	return m.confirmPrompt.Choice()
+}
+
+func NewConfirmDeployment(args listprompt.ListPromptArgs) *ConfirmDeploymentModel {
+	confirmPrompt := listprompt.NewListPrompt(args)
+
+	return &ConfirmDeploymentModel{
+		confirmPrompt: confirmPrompt,
+	}
+}
diff --git a/pkg/view/tui/commands/stack/preview/stack_preview.go b/pkg/view/tui/commands/stack/preview/stack_preview.go
new file mode 100644
index 000000000..eb623fe83
--- /dev/null
+++ b/pkg/view/tui/commands/stack/preview/stack_preview.go
@@ -0,0 +1,288 @@
+// Copyright Nitric Pty Ltd.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at:
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package stack_preview
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/charmbracelet/bubbles/key"
+	"github.com/charmbracelet/bubbles/spinner"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+	"github.com/samber/lo"
+
+	tui "github.com/nitrictech/cli/pkg/view/tui"
+	"github.com/nitrictech/cli/pkg/view/tui/commands/stack"
+	"github.com/nitrictech/cli/pkg/view/tui/components/view"
+	"github.com/nitrictech/cli/pkg/view/tui/fragments"
+	"github.com/nitrictech/cli/pkg/view/tui/reactive"
+	"github.com/nitrictech/cli/pkg/view/tui/teax"
+	deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1"
+)
+
+type Model struct {
+	windowSize tea.WindowSizeMsg
+
+	provider           string
+	stack              *stack.Resource
+	defaultParent      *stack.Resource
+	updatesChan        <-chan *deploymentspb.DeploymentPreviewEvent
+	errorChan          <-chan error
+	providerStdoutChan <-chan string
+	providerStdout     []string
+	providerMessages   []string
+	errs               []error
+	resultOutput       string
+	done               bool
+
+	spinner spinner.Model
+}
+
+var _ tea.Model = Model{}
+
+func (m Model) Init() tea.Cmd {
+	return tea.Batch(
+		m.spinner.Tick,
+		reactive.AwaitChannel(m.updatesChan),
+		reactive.AwaitChannel(m.errorChan),
+		reactive.AwaitChannel(m.providerStdoutChan),
+	)
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var cmds []tea.Cmd
+
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		m.windowSize = msg
+
+		return m, nil
+	case tea.KeyMsg:
+		switch {
+		case key.Matches(msg, tui.KeyMap.Quit):
+			m.done = true
+			return m, teax.Quit
+		}
+
+	case reactive.ChanMsg[string]:
+		if !msg.Ok {
+			break
+		}
+
+		m.providerStdout = append(m.providerStdout, msg.Value)
+
+		return m, reactive.AwaitChannel(msg.Source)
+	case reactive.ChanMsg[*deploymentspb.DeploymentPreviewEvent]:
+		// the source channel is closed
+		if !msg.Ok {
+			m.done = true
+			return m, teax.Quit
+		}
+
+		switch content := msg.Value.Content.(type) {
+		case *deploymentspb.DeploymentPreviewEvent_Message:
+			m.providerMessages = append(m.providerMessages, content.Message)
+		case *deploymentspb.DeploymentPreviewEvent_Update:
+			if content.Update == nil {
+				break
+			}
+
+			name := content.Update.SubResource
+			if name == "" && content.Update.Id != nil {
+				name = fmt.Sprintf("%s::%s", content.Update.Id.Type.String(), content.Update.Id.Name)
+			}
+
+			parent := m.stack
+
+			if content.Update.SubResource != "" && content.Update.Id != nil {
+				nitricResource, found := lo.Find(m.stack.Children, func(r *stack.Resource) bool {
+					return r.Name == fmt.Sprintf("%s::%s", content.Update.Id.Type.String(), content.Update.Id.Name)
+				})
+
+				if found {
+					parent = nitricResource
+				} else {
+					// add to the default container, used for resources that are stack level, but not explicitly defined.
+					parent = m.defaultParent
+				}
+			} else if content.Update.SubResource != "" {
+				parent = m.defaultParent
+			}
+
+			existingChild, found := lo.Find(parent.Children, func(item *stack.Resource) bool {
+				return item.Name == name
+			})
+
+			now := time.Now()
+
+			if !found {
+				existingChild = &stack.Resource{
+					Name:      name,
+					Action:    content.Update.Action,
+					StartTime: now,
+				}
+
+				parent.Children = append(parent.Children, existingChild)
+			}
+
+			if content.Update.Status == deploymentspb.ResourceDeploymentStatus_FAILED || content.Update.Status == deploymentspb.ResourceDeploymentStatus_SUCCESS || content.Update.Action == deploymentspb.ResourceDeploymentAction_SAME {
+				existingChild.FinishTime = now
+			}
+
+			// update its status
+			existingChild.Status = content.Update.Status
+			existingChild.Message = content.Update.Message
+		case *deploymentspb.DeploymentPreviewEvent_Result:
+			m.resultOutput = content.Result.GetText()
+		}
+
+		return m, reactive.AwaitChannel(msg.Source)
+	case reactive.ChanMsg[error]:
+		m.errs = append(m.errs, msg.Value)
+
+		return m, nil
+	case spinner.TickMsg:
+		var cmd tea.Cmd
+		m.spinner, cmd = m.spinner.Update(msg)
+		cmds = append(cmds, cmd)
+	}
+
+	return m, tea.Batch(cmds...)
+}
+
+const maxOutputLines = 5
+
+var (
+	terminalBorderStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false, true, false).BorderForeground(tui.Colors.Purple)
+	errorStyle          = lipgloss.NewStyle().Foreground(tui.Colors.Red)
+)
+
+func (m Model) View() string {
+	margin := fragments.TagWidth() + 2
+	if m.windowSize.Width < 60 {
+		margin = 0
+	}
+
+	v := view.New(view.WithStyle(lipgloss.NewStyle().Width(m.windowSize.Width)))
+	v.Break()
+	v.Add(fragments.Tag("preview"))
+	v.Addf("  Previewing deployment with %s", m.provider)
+
+	if m.done {
+		v.Break()
+	} else {
+		v.Addln(m.spinner.View())
+	}
+
+	v.Break()
+
+	if len(m.providerMessages) > 0 {
+		for _, message := range m.providerMessages {
+			v.Addln(message).WithStyle(lipgloss.NewStyle().MarginLeft(margin))
+		}
+
+		v.Break()
+	}
+
+	// Not all providers report a stack tree, so we only render it if there are children
+	if len(m.stack.Children) > 1 {
+		statusTree := fragments.NewStatusNode("stack", "")
+
+		for _, child := range m.stack.Children {
+			currentNode := statusTree.AddNode(child.Name, "")
+
+			for _, grandchild := range child.Children {
+				statusColor := tui.Colors.Blue
+				if grandchild.Action == deploymentspb.ResourceDeploymentAction_DELETE {
+					statusColor = tui.Colors.Red
+				} else if grandchild.Action == deploymentspb.ResourceDeploymentAction_CREATE {
+					statusColor = tui.Colors.Green
+				} else if grandchild.Action == deploymentspb.ResourceDeploymentAction_SAME {
+					statusColor = tui.Colors.Gray
+				}
+
+				// Always uses the pending verbage to show it will happen, not that it has happened
+				statusVerbage := stack.VerbMap[grandchild.Action][deploymentspb.ResourceDeploymentStatus_PENDING]
+
+				currentNode.AddNode(grandchild.Name, lipgloss.NewStyle().Foreground(statusColor).Render(statusVerbage))
+			}
+		}
+
+		// when the final output is rendered the available output width is 5 characters narrower than the window size.
+		lastRunFix := 5
+
+		v.Addln(statusTree.Render(m.windowSize.Width - margin - lastRunFix)).WithStyle(lipgloss.NewStyle().MarginLeft(margin))
+	}
+
+	// Provider Stdout and Stderr rendering
+	if len(m.providerStdout) > 0 {
+		v.Addln("%s stdout:", m.provider).WithStyle(lipgloss.NewStyle().Bold(true).Foreground(tui.Colors.Blue))
+
+		providerTerm := view.New(view.WithStyle(terminalBorderStyle))
+
+		for i, line := range m.providerStdout[max(0, len(m.providerStdout)-maxOutputLines):] {
+			providerTerm.Add(line).WithStyle(lipgloss.NewStyle().Width(min(m.windowSize.Width, 100)))
+
+			if i < len(m.providerStdout)-1 {
+				providerTerm.Break()
+			}
+		}
+
+		v.Addln(providerTerm.Render())
+	}
+
+	for _, e := range m.errs[max(0, len(m.errs)-maxOutputLines):] {
+		v.Break()
+		v.Add(fragments.ErrorTag())
+		v.Addln("  %s", e.Error()).WithStyle(errorStyle)
+	}
+
+	if m.resultOutput != "" {
+		v.Break()
+		v.Add(fragments.Tag("plan"))
+		v.Addln("  %s", m.resultOutput)
+	}
+
+	return v.Render()
+}
+
+func New(providerName string, stackName string, updatesChan <-chan *deploymentspb.DeploymentPreviewEvent, providerStdoutChan <-chan string, errorChan <-chan error) Model {
+	orphanParent := &stack.Resource{
+		Name:     fmt.Sprintf("Stack::%s", stackName),
+		Message:  "",
+		Action:   deploymentspb.ResourceDeploymentAction_SAME,
+		Status:   deploymentspb.ResourceDeploymentStatus_PENDING,
+		Children: []*stack.Resource{},
+	}
+
+	return Model{
+		provider:           providerName,
+		spinner:            spinner.New(spinner.WithSpinner(spinner.Ellipsis)),
+		updatesChan:        updatesChan,
+		providerStdoutChan: providerStdoutChan,
+		errorChan:          errorChan,
+		defaultParent:      orphanParent,
+		stack: &stack.Resource{
+			Name:    "stack",
+			Message: "",
+			Children: []*stack.Resource{
+				orphanParent,
+			},
+		},
+	}
+}
diff --git a/pkg/view/tui/components/listprompt/listprompt.go b/pkg/view/tui/components/listprompt/listprompt.go
index b4eac5272..fcdf5e8cf 100644
--- a/pkg/view/tui/components/listprompt/listprompt.go
+++ b/pkg/view/tui/components/listprompt/listprompt.go
@@ -17,8 +17,6 @@
 package listprompt
 
 import (
-	"strings"
-
 	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
@@ -105,7 +103,7 @@ func (m ListPrompt) View() string {
 		listView.Addln(m.Choice()).WithStyle(historyTextStyle)
 	}
 
-	return strings.TrimSuffix(listView.Render(), "\n")
+	return listView.Render()
 }
 
 type ListPromptArgs struct {