diff --git a/cli/module_build.go b/cli/module_build.go index a3912d9bb40..945a6b6499e 100644 --- a/cli/module_build.go +++ b/cli/module_build.go @@ -678,10 +678,18 @@ func (c *viamClient) shouldIgnoreFile(relPath string, matcher gitignore.Matcher) return matcher.Match(strings.Split(normalizedPath, "/"), false) } -func (c *viamClient) ensureModuleRegisteredInCloud(ctx *cli.Context, moduleID moduleID, manifest *moduleManifest) error { +func (c *viamClient) ensureModuleRegisteredInCloud( + ctx *cli.Context, moduleID moduleID, manifest *moduleManifest, pm *ProgressManager, +) error { _, err := c.getModule(moduleID) if err != nil { // Module is not registered in the cloud, prompt user for confirmation + // Stop the spinner before prompting for user input to avoid interference + // with the interactive prompt. + if pm != nil { + pm.Stop() + } + red := "\033[1;31m%s\033[0m" printf(ctx.App.Writer, red, "Error: module not registered in cloud or you lack permissions to edit it.") @@ -705,6 +713,12 @@ func (c *viamClient) ensureModuleRegisteredInCloud(ctx *cli.Context, moduleID mo // If user confirmed, we'll proceed with the reload which will register the module // The registration happens implicitly through the cloud build process + // Restart the spinner after user input + if pm != nil { + if err := pm.Start("register"); err != nil { + return err + } + } org, err := getOrgByModuleIDPrefix(c, moduleID.prefix) if err != nil { @@ -815,8 +829,9 @@ func (c *viamClient) triggerCloudReloadBuild( } var errs error + // Suppress the "Uploading... X%" progress bar output since we have our own spinner if err := sendUploadRequests( - ctx.Context, stream, file, c.c.App.Writer, getNextReloadBuildUploadRequest); err != nil && !errors.Is(err, io.EOF) { + ctx.Context, stream, file, io.Discard, getNextReloadBuildUploadRequest); err != nil && !errors.Is(err, io.EOF) { errs = multierr.Combine(errs, errors.Wrapf(err, "could not upload %s", file.Name())) } @@ -840,23 +855,45 @@ func getNextReloadBuildUploadRequest(file *os.File) (*buildpb.StartReloadBuildRe }, byteLen, nil } -// moduleCloudReload triggers a cloud build and then reloads the specified module with that build. +// moduleCloudBuildInfo contains information needed to download a cloud build artifact. +type moduleCloudBuildInfo struct { + ID string + Version string + Platform string + ArchivePath string // Path to the temporary archive that should be deleted after download +} + +// moduleCloudReload triggers a cloud build and returns info needed to download the artifact. func (c *viamClient) moduleCloudReload( ctx *cli.Context, args reloadModuleArgs, platform string, manifest moduleManifest, partID string, -) (string, error) { + pm *ProgressManager, +) (*moduleCloudBuildInfo, error) { + // Start the "Preparing for build..." parent step (prints as header) + if err := pm.Start("prepare"); err != nil { + return nil, err + } + // ensure that the module has been registered in the cloud moduleID, err := parseModuleID(manifest.ModuleID) if err != nil { - return "", err + return nil, err } - err = c.ensureModuleRegisteredInCloud(ctx, moduleID, &manifest) + if err := pm.Start("register"); err != nil { + return nil, err + } + err = c.ensureModuleRegisteredInCloud(ctx, moduleID, &manifest, pm) if err != nil { - return "", err + _ = pm.FailWithMessage("register", "Registration failed") //nolint:errcheck + _ = pm.FailWithMessage("prepare", "Preparing for build...") //nolint:errcheck + return nil, err + } + if err := pm.Complete("register"); err != nil { + return nil, err } id := ctx.String(generalFlagID) @@ -864,47 +901,90 @@ func (c *viamClient) moduleCloudReload( id = manifest.ModuleID } + if err := pm.Start("archive"); err != nil { + return nil, err + } archivePath, err := c.createGitArchive(args.Path) if err != nil { - return "", err + _ = pm.FailWithMessage("archive", "Archive creation failed") //nolint:errcheck + _ = pm.FailWithMessage("prepare", "Preparing for build...") //nolint:errcheck + return nil, err + } + if err := pm.Complete("archive"); err != nil { + return nil, err } - infof(c.c.App.Writer, "Creating a new cloud build and swapping it onto the requested machine part. This may take a few minutes...") + if err := pm.Start("upload-source"); err != nil { + return nil, err + } buildID, err := c.triggerCloudReloadBuild(ctx, args, manifest, archivePath, partID) if err != nil { - return "", err + _ = pm.FailWithMessage("upload-source", "Upload failed") //nolint:errcheck + _ = pm.FailWithMessage("prepare", "Preparing for build...") //nolint:errcheck + return nil, err + } + if err := pm.Complete("upload-source"); err != nil { + return nil, err + } + + // Complete the "Preparing for build..." parent step AFTER all its children + if err := pm.Complete("prepare"); err != nil { + return nil, err + } + + // Start the "Building..." parent step (prints as header) + if err := pm.Start("build"); err != nil { + return nil, err + } + + if err := pm.Start("build-start"); err != nil { + return nil, err + } + if err := pm.CompleteWithMessage("build-start", fmt.Sprintf("Build started (ID: %s)", buildID)); err != nil { + return nil, err + } + + if err := pm.Start("build-wait"); err != nil { + return nil, err } - // ensure the build completes before we try to dowload and use it + // ensure the build completes before we try to download and use it statuses, err := c.waitForBuildToFinish(buildID, platform) if err != nil { - return "", err + _ = pm.FailWithMessage("build-wait", "Build wait failed") //nolint:errcheck + _ = pm.FailWithMessage("build", "Building...") //nolint:errcheck + return nil, err } // if the build failed, print the logs and return an error if statuses[platform] == jobStatusFailed { + _ = pm.FailWithMessage("build-wait", fmt.Sprintf("Build %s failed", buildID)) //nolint:errcheck + _ = pm.FailWithMessage("build", "Building...") //nolint:errcheck + // Print error message without exiting (don't use Errorf since it calls os.Exit(1)) errorf(c.c.App.Writer, "Build %q failed to complete. Please check the logs below for more information.", buildID) if err = c.printModuleBuildLogs(buildID, platform); err != nil { - return "", err + return nil, err } - return "", errors.Errorf("Reloading module failed") + return nil, errors.Errorf("Reloading module failed") } - downloadArgs := downloadModuleFlags{ - ID: id, - Version: getReloadVersion(reloadVersionPrefix, partID), - Platform: platform, + if err := pm.Complete("build-wait"); err != nil { + return nil, err } - - // delete the archive we created - if err := os.Remove(archivePath); err != nil { - warningf(ctx.App.Writer, "failed to delete archive at %s", archivePath) + if err := pm.Complete("build"); err != nil { + return nil, err } - return c.downloadModuleAction(ctx, downloadArgs) + // Return build info so the caller can download the artifact with a spinner + return &moduleCloudBuildInfo{ + ID: id, + Version: getReloadVersion(reloadVersionPrefix, partID), + Platform: platform, + ArchivePath: archivePath, + }, nil } // ReloadModuleLocalAction builds a module locally, configures it on a robot, and starts or restarts it. @@ -1009,8 +1089,31 @@ func reloadModuleActionInner( // CLI will see configuration changes before the robot, and skip to the needsRestart // case on the second call. Because these are triggered by user actions, we're okay // with this behavior, and the robot will eventually converge to what is in config. + + // Define all steps upfront (build + reload) with clear parent/child relationships + allSteps := []*Step{ + {ID: "prepare", Message: "Preparing for build...", IndentLevel: 0}, + {ID: "register", Message: "Ensuring module is registered...", CompletedMsg: "Module is registered", IndentLevel: 1}, + {ID: "archive", Message: "Creating source code archive...", CompletedMsg: "Source code archive created", IndentLevel: 1}, + {ID: "upload-source", Message: "Uploading source code...", CompletedMsg: "Source code uploaded", IndentLevel: 1}, + {ID: "build", Message: "Building...", IndentLevel: 0}, + {ID: "build-start", Message: "Starting build...", IndentLevel: 1}, + {ID: "build-wait", Message: "Waiting for build to finish...", CompletedMsg: "Build completed successfully", IndentLevel: 1}, + {ID: "reload", Message: "Reloading to part...", IndentLevel: 0}, + {ID: "download", Message: "Downloading build artifact...", CompletedMsg: "Build artifact downloaded", IndentLevel: 1}, + {ID: "shell", Message: "Setting up shell service...", CompletedMsg: "Shell service ready", IndentLevel: 1}, + {ID: "upload", Message: "Uploading package...", CompletedMsg: "Package uploaded", IndentLevel: 1}, + {ID: "configure", Message: "Configuring module...", CompletedMsg: "Module configured", IndentLevel: 1}, + {ID: "restart", Message: "Restarting module...", CompletedMsg: "Module restarted successfully", IndentLevel: 1}, + {ID: "resource", Message: "Adding resource...", CompletedMsg: "Resource added", IndentLevel: 1}, + } + + pm := NewProgressManager(allSteps, WithProgressOutput(!args.NoProgress)) + defer pm.Stop() + var needsRestart bool var buildPath string + var buildInfo *moduleCloudBuildInfo if !args.NoBuild { if manifest == nil { return fmt.Errorf(`manifest not found at "%s". manifest required for build`, moduleFlagPath) @@ -1019,12 +1122,43 @@ func reloadModuleActionInner( err = moduleBuildLocalAction(c, manifest, environment) buildPath = manifest.Build.Path } else { - buildPath, err = vc.moduleCloudReload(c, args, platform, *manifest, partID) + buildInfo, err = vc.moduleCloudReload(c, args, platform, *manifest, partID, pm) + if err != nil { + return err + } + + // Download the build artifact with a spinner + if err := pm.Start("reload"); err != nil { + return err + } + if err := pm.Start("download"); err != nil { + return err + } + downloadArgs := downloadModuleFlags{ + ID: buildInfo.ID, + Version: buildInfo.Version, + Platform: buildInfo.Platform, + } + buildPath, err = vc.downloadModuleAction(c, downloadArgs) + if err != nil { + _ = pm.Fail("download", err) //nolint:errcheck + _ = pm.FailWithMessage("reload", "Reloading to part...") //nolint:errcheck + return err + } + if err := pm.Complete("download"); err != nil { + return err + } + + // Delete the archive we created + if err := os.Remove(buildInfo.ArchivePath); err != nil { + warningf(c.App.Writer, "failed to delete archive at %s", buildInfo.ArchivePath) + } } if err != nil { return err } } + if !args.Local { if manifest == nil || manifest.Build == nil || buildPath == "" { return errors.New( @@ -1039,26 +1173,66 @@ func reloadModuleActionInner( return err } } - if err := addShellService(c, vc, part.Part, true); err != nil { + + // Start the "Reloading to part..." parent step if not already started (for local builds with cloud-built artifacts) + if !cloudBuild { + if err := pm.Start("reload"); err != nil { + return err + } + } + if err := pm.Start("shell"); err != nil { return err } - infof(c.App.Writer, "Copying %s to part %s", buildPath, part.Part.Id) + shellAdded, err := addShellService(c, vc, logger, part.Part, true) + if err != nil { + _ = pm.Fail("shell", err) //nolint:errcheck + _ = pm.FailWithMessage("reload", "Reloading to part...") //nolint:errcheck + return err + } + if shellAdded { + if err := pm.CompleteWithMessage("shell", "Shell service installed"); err != nil { + return err + } + } else { + if err := pm.CompleteWithMessage("shell", "Shell service already exists"); err != nil { + return err + } + } + globalArgs, err := getGlobalArgs(c) if err != nil { return err } dest := reloadingDestination(c, manifest) + + if err := pm.Start("upload"); err != nil { + return err + } err = vc.copyFilesToFqdn( part.Part.Fqdn, globalArgs.Debug, false, false, []string{buildPath}, - dest, logging.NewLogger(reloadVersionPrefix), args.NoProgress) + dest, logger, true) if err != nil { if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied { warningf(c.App.ErrWriter, "RDK couldn't write to the default file copy destination. "+ "If you're running as non-root, try adding --home $HOME or --home /user/username to your CLI command. "+ "Alternatively, run the RDK as root.") } + _ = pm.Fail("upload", err) //nolint:errcheck + _ = pm.FailWithMessage("reload", "Reloading to part...") //nolint:errcheck return fmt.Errorf("failed copying to part (%v): %w", dest, err) } + if err := pm.Complete("upload"); err != nil { + return err + } + } else { + // For local builds, start the "Reloading to part..." parent step right before configure + if err := pm.Start("reload"); err != nil { + return err + } + } + + if err := pm.Start("configure"); err != nil { + return err } var newPart *apppb.RobotPart newPart, needsRestart, err = configureModule(c, vc, manifest, part.Part, args.Local) @@ -1069,22 +1243,58 @@ func reloadModuleActionInner( } if err != nil { + _ = pm.Fail("configure", err) //nolint:errcheck + _ = pm.FailWithMessage("reload", "Reloading to part...") //nolint:errcheck return err } + if !needsRestart { + if err := pm.CompleteWithMessage("configure", "Module added to part"); err != nil { + return err + } + } else { + if err := pm.CompleteWithMessage("configure", "Module already exists on part"); err != nil { + return err + } + } + if needsRestart { + if err := pm.Start("restart"); err != nil { + return err + } if err = restartModule(c, vc, part.Part, manifest, logger); err != nil { + _ = pm.Fail("restart", err) //nolint:errcheck + _ = pm.FailWithMessage("reload", "Reloading to part...") //nolint:errcheck + return err + } + if err := pm.Complete("restart"); err != nil { return err } - } else { - infof(c.App.Writer, "Reload complete") } if args.ModelName != "" { + if err := pm.Start("resource"); err != nil { + return err + } if err = vc.addResourceFromModule(c, part.Part, manifest, args.ModelName, args.ResourceName); err != nil { + _ = pm.FailWithMessage("resource", fmt.Sprintf("Failed to add resource: %v", err)) //nolint:errcheck warningf(c.App.ErrWriter, "unable to add requested resource to robot config: %s", err) + } else { + resourceName := args.ResourceName + if resourceName == "" { + resourceName = args.ModelName + } + if err := pm.CompleteWithMessage("resource", fmt.Sprintf("Added %s", resourceName)); err != nil { + return err + } } } + + // Complete the parent "Reloading to part..." step + if err := pm.Complete("reload"); err != nil { + return err + } + return nil } @@ -1242,9 +1452,5 @@ func restartModule( defer robotClient.Close(c.Context) //nolint: errcheck debugf(c.App.Writer, args.Debug, "restarting module %v", restartReq) // todo: make this a stream so '--wait' can tell user what's happening - err = robotClient.RestartModule(c.Context, *restartReq) - if err == nil { - infof(c.App.Writer, "restarted module.") - } - return err + return robotClient.RestartModule(c.Context, *restartReq) } diff --git a/cli/module_registry.go b/cli/module_registry.go index 6323d198b28..5e4c63b0dd8 100644 --- a/cli/module_registry.go +++ b/cli/module_registry.go @@ -12,7 +12,6 @@ import ( "math" "os" "os/exec" - "path" "path/filepath" "regexp" "runtime" @@ -1079,7 +1078,6 @@ func (c *viamClient) downloadModuleAction(ctx *cli.Context, flags downloadModule return "", fmt.Errorf("version %s not found in versions for module", requestedVersion) } } - infof(ctx.App.ErrWriter, "found version %s", ver.Version) if len(ver.Files) == 0 { return "", fmt.Errorf("version %s has 0 files uploaded", ver.Version) } @@ -1105,7 +1103,6 @@ func (c *viamClient) downloadModuleAction(ctx *cli.Context, flags downloadModule return "", err } destName := strings.ReplaceAll(moduleID, ":", "-") - infof(ctx.App.ErrWriter, "saving to %s", path.Join(flags.Destination, fullVersion, destName+".tar.gz")) return downloadPackageFromURL(ctx.Context, c.authFlow.httpClient, flags.Destination, destName, fullVersion, pkg.Package.Url, c.conf.Auth, diff --git a/cli/module_reload.go b/cli/module_reload.go index 36b0ee718b0..9c8a061db8d 100644 --- a/cli/module_reload.go +++ b/cli/module_reload.go @@ -110,10 +110,11 @@ func (c *viamClient) addResourceFromModule( } // addShellService adds a shell service to the services slice if missing. Mutates part.RobotConfig. -func addShellService(c *cli.Context, vc *viamClient, part *apppb.RobotPart, wait bool) error { +// Returns (wasAdded, error) where wasAdded indicates if the shell service was newly added. +func addShellService(c *cli.Context, vc *viamClient, logger logging.Logger, part *apppb.RobotPart, wait bool) (bool, error) { args, err := getGlobalArgs(c) if err != nil { - return err + return false, err } partMap := part.RobotConfig.AsMap() if _, ok := partMap["services"]; !ok { @@ -126,7 +127,7 @@ func addShellService(c *cli.Context, vc *viamClient, part *apppb.RobotPart, wait return service["type"] == "shell" || service["api"] == "rdk:service:shell" }) { debugf(c.App.Writer, args.Debug, "shell service found on target machine, not installing") - return nil + return false, nil } services = append(services, ResourceMap{"name": "shell", "api": "rdk:service:shell"}) asAny, _ := rutils.MapOver(services, func(service ResourceMap) (any, error) { //nolint:errcheck @@ -134,29 +135,28 @@ func addShellService(c *cli.Context, vc *viamClient, part *apppb.RobotPart, wait }) partMap["services"] = asAny if err := writeBackConfig(part, partMap); err != nil { - return err + return false, err } - infof(c.App.Writer, "installing shell service on target machine for file transfer") if err := vc.updateRobotPart(part, partMap); err != nil { - return err + return false, err } if !wait { - return nil + return true, nil } // note: we wait up to 11 seconds; that's the 10 second default Cloud.RefreshInterval plus padding. // If we don't wait, the reload command will usually fail on first run. for i := 0; i < 11; i++ { time.Sleep(time.Second) - _, closeClient, err := vc.connectToShellServiceFqdn(part.Fqdn, args.Debug, logging.NewLogger("shellsvc")) + _, closeClient, err := vc.connectToShellServiceFqdn(part.Fqdn, args.Debug, logger) if err == nil { goutils.UncheckedError(closeClient(c.Context)) - return nil + return true, nil } if !errors.Is(err, errNoShellService) { - return err + return false, err } } - return errors.New("timed out waiting for shell service to start") + return false, errors.New("timed out waiting for shell service to start") } // writeBackConfig mutates part.RobotConfig with an edited config; this is necessary so that changes diff --git a/cli/module_reload_test.go b/cli/module_reload_test.go index 4ad94905e03..e665df0d5fe 100644 --- a/cli/module_reload_test.go +++ b/cli/module_reload_test.go @@ -98,7 +98,7 @@ func TestFullReloadFlow(t *testing.T) { t.Run("addShellService", func(t *testing.T) { t.Run("addsServiceWhenMissing", func(t *testing.T) { part, _ := vc.getRobotPart("id") - err := addShellService(cCtx, vc, part.Part, false) + _, err := addShellService(cCtx, vc, logging.NewTestLogger(t), part.Part, false) test.That(t, err, test.ShouldBeNil) services, ok := part.Part.RobotConfig.AsMap()["services"].([]any) test.That(t, ok, test.ShouldBeTrue) @@ -127,7 +127,7 @@ func TestFullReloadFlow(t *testing.T) { ) part, _ := vc2.getRobotPart("id") - err = addShellService(cCtx2, vc2, part.Part, false) + _, err = addShellService(cCtx2, vc2, logging.NewTestLogger(t), part.Part, false) test.That(t, err, test.ShouldBeNil) services, ok := part.Part.RobotConfig.AsMap()["services"].([]any) test.That(t, ok, test.ShouldBeTrue) diff --git a/cli/progress_manager.go b/cli/progress_manager.go new file mode 100644 index 00000000000..da97038e33a --- /dev/null +++ b/cli/progress_manager.go @@ -0,0 +1,377 @@ +package cli + +import ( + "fmt" + "os" + "sync" + "time" + + "github.com/pterm/pterm" +) + +type progressSpinner interface { + Stop() error + Success(...any) + Fail(...any) + UpdateText(string) +} + +type progressSpinnerFactory func(string) (progressSpinner, error) + +var defaultSpinnerFactory progressSpinnerFactory = func(text string) (progressSpinner, error) { + spinner, err := pterm.DefaultSpinner. + WithRemoveWhenDone(false). + WithText(text). + Start() + if err != nil { + return nil, err + } + return spinner, nil +} + +// StepStatus represents the state of a progress step. +type StepStatus int + +const ( + // StepPending indicates a step has not yet started. + StepPending StepStatus = iota + // StepRunning indicates a step is currently in progress. + StepRunning + // StepCompleted indicates a step finished successfully. + StepCompleted + // StepFailed indicates a step encountered an error. + StepFailed +) + +// Step represents a single progress step. +type Step struct { + ID string + Message string + Status StepStatus + CompletedMsg string // Optional: Custom message when completed + FailedMsg string // Optional: Custom message when failed + IndentLevel int // 0 = root, 1 = child (→), 2 = nested child, etc. + startTime time.Time // Internal: when the step started +} + +// ProgressManager manages a sequence of steps with spinners (sequential display). +type ProgressManager struct { + steps []*Step + stepMap map[string]*Step + currentSpinner progressSpinner // Active child spinner (IndentLevel > 0) + spinnerFactory progressSpinnerFactory + mu sync.Mutex + disabled bool +} + +// ProgressManagerOption allows customizing ProgressManager behavior at creation time. +type ProgressManagerOption func(*ProgressManager) + +// WithProgressOutput enables or disables terminal output for a ProgressManager. +func WithProgressOutput(enabled bool) ProgressManagerOption { + return func(pm *ProgressManager) { + pm.disabled = !enabled + } +} + +func withProgressSpinnerFactory(factory progressSpinnerFactory) ProgressManagerOption { + return func(pm *ProgressManager) { + pm.spinnerFactory = factory + } +} + +// NewProgressManager creates a new ProgressManager with all steps registered upfront. +func NewProgressManager(steps []*Step, opts ...ProgressManagerOption) *ProgressManager { + // Customize spinner style globally + pterm.Success.Prefix = pterm.Prefix{ + Text: "✓", + Style: pterm.NewStyle(pterm.FgGreen), + } + pterm.Error.Prefix = pterm.Prefix{ + Text: "✗", + Style: pterm.NewStyle(pterm.FgRed), + } + // Add a leading space to each spinner sequence character for alignment + baseSequence := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + spinnerSequence := make([]string, len(baseSequence)) + for i, char := range baseSequence { + spinnerSequence[i] = " " + char + } + pterm.DefaultSpinner.Sequence = spinnerSequence + pterm.DefaultSpinner.Style = pterm.NewStyle(pterm.FgCyan) + + stepMap := make(map[string]*Step) + for _, step := range steps { + stepMap[step.ID] = step + } + + pm := &ProgressManager{ + steps: steps, + stepMap: stepMap, + currentSpinner: nil, + spinnerFactory: defaultSpinnerFactory, + } + + for _, opt := range opts { + opt(pm) + } + + return pm +} + +// getPrefix returns the formatted prefix for a step based on its indent level. +func getPrefix(step *Step) string { + prefix := "" + for i := 0; i < step.IndentLevel; i++ { + prefix += " " + } + if step.IndentLevel > 0 { + prefix += "→ " + } + return prefix +} + +// Start begins animating the spinner for the given step ID. +func (pm *ProgressManager) Start(stepID string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + step, exists := pm.stepMap[stepID] + if !exists { + return fmt.Errorf("step %q not found", stepID) + } + + step.Status = StepRunning + step.startTime = time.Now() // Record start time + + if pm.disabled { + return nil + } + + // If this is a parent step (IndentLevel == 0), print it as a static "in progress" indicator + if step.IndentLevel == 0 { + // Use ellipsis to indicate parent is in progress (with extra space for alignment) + _, _ = os.Stdout.WriteString(fmt.Sprintf(" … %s\n", step.Message)) //nolint:errcheck + return nil + } + + // For child steps, stop the previous child spinner if one is active + if pm.currentSpinner != nil { + _ = pm.currentSpinner.Stop() //nolint:errcheck + } + + // Create and start a new spinner for this child step + // pterm adds an automatic space after the spinner character, so we need to + // add one MORE space to the prefix to match the completed format + adjustedPrefix := "" + for i := 0; i < step.IndentLevel; i++ { + adjustedPrefix += " " + } + if step.IndentLevel > 0 { + adjustedPrefix += " → " // Three spaces before arrow to match completed format + } + + if pm.spinnerFactory == nil { + pm.spinnerFactory = defaultSpinnerFactory + } + + spinner, err := pm.spinnerFactory(adjustedPrefix + step.Message) + if err != nil { + return fmt.Errorf("failed to start child spinner: %w", err) + } + + pm.currentSpinner = spinner + + return nil +} + +// Complete marks a step as completed with a success message. +func (pm *ProgressManager) Complete(stepID string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + step, exists := pm.stepMap[stepID] + if !exists { + return fmt.Errorf("step %q not found", stepID) + } + + step.Status = StepCompleted + + msg := step.CompletedMsg + if msg == "" { + msg = step.Message + } + + // Add elapsed time if step was started + elapsed := "" + if !step.startTime.IsZero() { + duration := time.Since(step.startTime) + elapsed = fmt.Sprintf(" (%s)", duration.Round(time.Second)) + } + + prefix := getPrefix(step) + + if pm.disabled { + return nil + } + + // If this is a parent step (IndentLevel == 0), update the static header + if step.IndentLevel == 0 { + // Count how many child lines were printed after the parent + linesToMoveUp := 0 + foundParent := false + for _, s := range pm.steps { + if s.ID == stepID { + foundParent = true + continue + } + if foundParent && s.IndentLevel > 0 && (s.Status == StepCompleted || s.Status == StepRunning) { + linesToMoveUp++ + } + } + + // Move cursor up to the parent line, clear it, and print success + if linesToMoveUp > 0 { + _, _ = os.Stdout.WriteString(fmt.Sprintf("\033[%dA", linesToMoveUp+1)) //nolint:errcheck // Move up + } + _, _ = os.Stdout.WriteString("\r\033[K") //nolint:errcheck // Clear line + pterm.Success.Println(prefix + msg + elapsed) + + // Move cursor back down + if linesToMoveUp > 0 { + _, _ = os.Stdout.WriteString(fmt.Sprintf("\033[%dB", linesToMoveUp)) //nolint:errcheck // Move down + } + return nil + } + + // For child steps: if this is the currently active spinner, stop it and mark success + if pm.currentSpinner != nil { + pm.currentSpinner.Success(" " + prefix + msg + elapsed) + pm.currentSpinner = nil + } else { + // If no spinner is active, just print the success message + pterm.Success.Println(" " + prefix + msg + elapsed) + } + + return nil +} + +// CompleteWithMessage marks a step as completed with a custom message. +func (pm *ProgressManager) CompleteWithMessage(stepID, message string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + step, exists := pm.stepMap[stepID] + if !exists { + return fmt.Errorf("step %q not found", stepID) + } + + step.Status = StepCompleted + + // Add elapsed time if step was started + elapsed := "" + if !step.startTime.IsZero() { + duration := time.Since(step.startTime) + elapsed = fmt.Sprintf(" (%s)", duration.Round(time.Second)) + } + + prefix := getPrefix(step) + + if pm.disabled { + return nil + } + + // If this is the currently active spinner, stop it and mark success + if pm.currentSpinner != nil { + pm.currentSpinner.Success(" " + prefix + message + elapsed) + pm.currentSpinner = nil + } else { + // If no spinner is active, just print the success message + pterm.Success.Println(" " + prefix + message + elapsed) + } + + return nil +} + +// Fail marks a step as failed with an error message. +func (pm *ProgressManager) Fail(stepID string, err error) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + step, exists := pm.stepMap[stepID] + if !exists { + return fmt.Errorf("step %q not found", stepID) + } + + msg := step.FailedMsg + if msg == "" { + msg = fmt.Sprintf("%s: %v", step.Message, err) + } + + pm.failWithMessageLocked(step, msg) + return nil +} + +// FailWithMessage marks a step as failed with a custom message. +func (pm *ProgressManager) FailWithMessage(stepID, message string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + step, exists := pm.stepMap[stepID] + if !exists { + return fmt.Errorf("step %q not found", stepID) + } + + pm.failWithMessageLocked(step, message) + return nil +} + +// failWithMessageLocked is the shared implementation for failing a step. +// It assumes the lock is already held by the caller. +func (pm *ProgressManager) failWithMessageLocked(step *Step, message string) { + step.Status = StepFailed + + if pm.disabled { + return + } + + prefix := getPrefix(step) + + // If this is the currently active spinner, stop it and mark failure + if pm.currentSpinner != nil { + pm.currentSpinner.Fail(" " + prefix + message) + pm.currentSpinner = nil + } else { + // If no spinner is active, just print the error message + pterm.Error.Println(" " + prefix + message) + } +} + +// UpdateText updates the text of the currently active spinner (for progress updates). +func (pm *ProgressManager) UpdateText(text string) { + pm.mu.Lock() + defer pm.mu.Unlock() + + if pm.disabled { + return + } + + if pm.currentSpinner != nil { + pm.currentSpinner.UpdateText(text) + } +} + +// Stop stops any active spinner. +func (pm *ProgressManager) Stop() { + pm.mu.Lock() + defer pm.mu.Unlock() + + if pm.disabled { + return + } + + if pm.currentSpinner != nil { + _ = pm.currentSpinner.Stop() //nolint:errcheck + pm.currentSpinner = nil + } +} diff --git a/cli/progress_manager_test.go b/cli/progress_manager_test.go new file mode 100644 index 00000000000..c7f3736e323 --- /dev/null +++ b/cli/progress_manager_test.go @@ -0,0 +1,702 @@ +package cli + +import ( + "fmt" + "sync" + "testing" + "time" + + "go.viam.com/test" +) + +type fakeSpinner struct { + mu sync.Mutex + text string + stopped bool + successes []string + failures []string +} + +func (f *fakeSpinner) Stop() error { + f.mu.Lock() + defer f.mu.Unlock() + f.stopped = true + return nil +} + +func (f *fakeSpinner) Success(message ...any) { + f.mu.Lock() + if len(message) > 0 { + f.successes = append(f.successes, fmt.Sprint(message...)) + } else { + f.successes = append(f.successes, "") + } + f.mu.Unlock() + _ = f.Stop() +} + +func (f *fakeSpinner) Fail(message ...any) { + f.mu.Lock() + if len(message) > 0 { + f.failures = append(f.failures, fmt.Sprint(message...)) + } else { + f.failures = append(f.failures, "") + } + f.mu.Unlock() + _ = f.Stop() +} + +func (f *fakeSpinner) UpdateText(text string) { + f.mu.Lock() + f.text = text + f.mu.Unlock() +} + +func newFakeSpinnerFactory() progressSpinnerFactory { + return func(text string) (progressSpinner, error) { + fs := &fakeSpinner{} + fs.UpdateText(text) + return fs, nil + } +} + +func newTestProgressManager(steps []*Step, opts ...ProgressManagerOption) *ProgressManager { + opts = append(opts, withProgressSpinnerFactory(newFakeSpinnerFactory())) + return NewProgressManager(steps, opts...) +} + +func TestNewProgressManager(t *testing.T) { + steps := []*Step{ + {ID: "parent1", Message: "Parent step", IndentLevel: 0}, + {ID: "child1", Message: "Child step", IndentLevel: 1}, + {ID: "child2", Message: "Another child", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + if pm == nil { + t.Fatal("NewProgressManager returned nil") + } + + if len(pm.steps) != 3 { + t.Errorf("Expected 3 steps, got %d", len(pm.steps)) + } + + if len(pm.stepMap) != 3 { + t.Errorf("Expected 3 step map entries, got %d", len(pm.stepMap)) + } + + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil initially") + } + + // Verify step map contains all steps + for _, step := range steps { + if _, exists := pm.stepMap[step.ID]; !exists { + t.Errorf("Step %q not found in step map", step.ID) + } + } +} + +func TestGetPrefix(t *testing.T) { + tests := []struct { + step *Step + expected string + }{ + {&Step{ID: "root", IndentLevel: 0}, ""}, + {&Step{ID: "child1", IndentLevel: 1}, " → "}, + {&Step{ID: "child2", IndentLevel: 2}, " → "}, + {&Step{ID: "child3", IndentLevel: 3}, " → "}, + } + + for _, test := range tests { + result := getPrefix(test.step) + if result != test.expected { + t.Errorf("getPrefix(%v) = %q, expected %q", test.step.IndentLevel, result, test.expected) + } + } +} + +func TestStartParentStep(t *testing.T) { + steps := []*Step{ + {ID: "parent", Message: "Parent step", IndentLevel: 0}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("parent") + if err != nil { + t.Fatalf("Failed to start parent step: %v", err) + } + + step := pm.stepMap["parent"] + if step.Status != StepRunning { + t.Errorf("Expected step status to be StepRunning, got %v", step.Status) + } + + if step.startTime.IsZero() { + t.Error("Expected startTime to be set for parent step") + } +} + +func TestStartChildStep(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + if pm.currentSpinner == nil { + t.Error("Expected currentSpinner to be set for child step") + } + + step := pm.stepMap["child"] + if step.Status != StepRunning { + t.Errorf("Expected step status to be StepRunning, got %v", step.Status) + } +} + +func TestProgressManagerWithOutputDisabled(t *testing.T) { + steps := []*Step{ + {ID: "parent", Message: "Parent", IndentLevel: 0}, + {ID: "child", Message: "Child", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps, WithProgressOutput(false)) + defer pm.Stop() + + err := pm.Start("parent") + if err != nil { + t.Fatalf("Failed to start parent step: %v", err) + } + + if pm.currentSpinner != nil { + t.Fatal("Expected no spinner when output is disabled for parent step") + } + + err = pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + if pm.currentSpinner != nil { + t.Fatal("Expected no spinner when output is disabled for child step") + } + + err = pm.Complete("child") + if err != nil { + t.Fatalf("Failed to complete child step: %v", err) + } + + err = pm.Complete("parent") + if err != nil { + t.Fatalf("Failed to complete parent step: %v", err) + } + + test.That(t, pm.stepMap["parent"].Status, test.ShouldEqual, StepCompleted) + test.That(t, pm.stepMap["child"].Status, test.ShouldEqual, StepCompleted) +} + +func TestStartInvalidStep(t *testing.T) { + steps := []*Step{ + {ID: "valid", Message: "Valid step", IndentLevel: 0}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("invalid") + if err == nil { + t.Error("Expected error for invalid step ID") + } + + expectedError := "step \"invalid\" not found" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } +} + +func TestStartReplacesPreviousSpinner(t *testing.T) { + steps := []*Step{ + {ID: "child1", Message: "First child", IndentLevel: 1}, + {ID: "child2", Message: "Second child", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + // Start first child + err := pm.Start("child1") + if err != nil { + t.Fatalf("Failed to start first child: %v", err) + } + + firstSpinner := pm.currentSpinner + + // Start second child + err = pm.Start("child2") + if err != nil { + t.Fatalf("Failed to start second child: %v", err) + } + + // First spinner should be stopped and replaced + if pm.currentSpinner == firstSpinner { + t.Error("Expected currentSpinner to be replaced with new spinner") + } +} + +func TestCompleteParentStep(t *testing.T) { + steps := []*Step{ + {ID: "parent", Message: "Parent step", CompletedMsg: "Parent completed", IndentLevel: 0}, + {ID: "child1", Message: "Child 1", IndentLevel: 1}, + {ID: "child2", Message: "Child 2", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + + // Complete some child steps first + pm.stepMap["child1"].Status = StepCompleted + pm.stepMap["child2"].Status = StepRunning + + err := pm.Start("parent") + if err != nil { + t.Fatalf("Failed to start parent step: %v", err) + } + + err = pm.Complete("parent") + if err != nil { + t.Fatalf("Failed to complete parent step: %v", err) + } + + step := pm.stepMap["parent"] + if step.Status != StepCompleted { + t.Errorf("Expected step status to be StepCompleted, got %v", step.Status) + } + + if step.startTime.IsZero() { + t.Error("Expected startTime to be set for parent step") + } +} + +func TestCompleteChildStep(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", CompletedMsg: "Child completed", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + err = pm.Complete("child") + if err != nil { + t.Fatalf("Failed to complete child step: %v", err) + } + + step := pm.stepMap["child"] + if step.Status != StepCompleted { + t.Errorf("Expected step status to be StepCompleted, got %v", step.Status) + } + + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after completion") + } +} + +func TestCompleteWithElapsedTime(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + // Wait a bit to ensure measurable elapsed time + time.Sleep(10 * time.Millisecond) + + err = pm.Complete("child") + if err != nil { + t.Fatalf("Failed to complete child step: %v", err) + } + + // Check that elapsed time was calculated and included + // This is a bit tricky to test directly since we can't capture pterm output easily + // But we can verify the step timing was recorded + step := pm.stepMap["child"] + if step.startTime.IsZero() { + t.Error("Expected startTime to be set") + } + + elapsed := time.Since(step.startTime) + test.That(t, elapsed, test.ShouldBeGreaterThanOrEqualTo, 10*time.Millisecond) +} + +func TestCompleteWithMessage(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + customMessage := "Custom completion message" + err = pm.CompleteWithMessage("child", customMessage) + if err != nil { + t.Fatalf("Failed to complete child step with message: %v", err) + } + + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after completion") + } +} + +func TestFailParentStep(t *testing.T) { + steps := []*Step{ + {ID: "parent", Message: "Parent step", FailedMsg: "Parent failed", IndentLevel: 0}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("parent") + if err != nil { + t.Fatalf("Failed to start parent step: %v", err) + } + + testErr := fmt.Errorf("test error") + err = pm.Fail("parent", testErr) + if err != nil { + t.Fatalf("Failed to fail parent step: %v", err) + } + + step := pm.stepMap["parent"] + if step.Status != StepFailed { + t.Errorf("Expected step status to be StepFailed, got %v", step.Status) + } +} + +func TestFailChildStep(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + testErr := fmt.Errorf("test error") + err = pm.Fail("child", testErr) + if err != nil { + t.Fatalf("Failed to fail child step: %v", err) + } + + step := pm.stepMap["child"] + if step.Status != StepFailed { + t.Errorf("Expected step status to be StepFailed, got %v", step.Status) + } + + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after failure") + } +} + +func TestFailWithMessage(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + customMessage := "Custom failure message" + err = pm.FailWithMessage("child", customMessage) + if err != nil { + t.Fatalf("Failed to fail child step with message: %v", err) + } + + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after failure") + } +} + +func TestFailWithoutCustomMessage(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + testErr := fmt.Errorf("test error") + err = pm.Fail("child", testErr) + if err != nil { + t.Fatalf("Failed to fail child step: %v", err) + } + + // Verify that the error message includes both the step message and the error + step := pm.stepMap["child"] + expectedMsg := fmt.Sprintf("%s: %v", step.Message, testErr) + if step.FailedMsg != "" { + // If FailedMsg is set, it should be used instead + if step.FailedMsg != expectedMsg { + t.Errorf("Expected failed message to be %q, got %q", expectedMsg, step.FailedMsg) + } + } +} + +func TestStop(t *testing.T) { + steps := []*Step{ + {ID: "child", Message: "Child step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + err := pm.Start("child") + if err != nil { + t.Fatalf("Failed to start child step: %v", err) + } + + if pm.currentSpinner == nil { + t.Fatal("Expected currentSpinner to be set") + } + + pm.Stop() + + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after stop") + } +} + +func TestConcurrentAccess(t *testing.T) { + steps := []*Step{ + {ID: "child1", Message: "Child 1", IndentLevel: 1}, + {ID: "child2", Message: "Child 2", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + var wg sync.WaitGroup + numGoroutines := 10 + + // Test concurrent Start calls + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + // Alternate between two child steps + stepID := "child1" + if index%2 == 0 { + stepID = "child2" + } + pm.Start(stepID) + }(i) + } + + wg.Wait() + + // Test concurrent Complete calls + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + // Alternate between two child steps + stepID := "child1" + if index%2 == 0 { + stepID = "child2" + } + pm.Complete(stepID) + }(i) + } + + wg.Wait() + + // Verify no data races occurred and final state is consistent + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after all operations") + } +} + +func TestStepStatusTransitions(t *testing.T) { + steps := []*Step{ + {ID: "step1", Message: "Step 1", IndentLevel: 1}, + {ID: "step2", Message: "Step 2", IndentLevel: 0}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + // Test initial status + for _, step := range steps { + if step.Status != StepPending { + t.Errorf("Expected initial status to be StepPending, got %v", step.Status) + } + } + + // Test transition to running + err := pm.Start("step1") + if err != nil { + t.Fatalf("Failed to start step1: %v", err) + } + + if pm.stepMap["step1"].Status != StepRunning { + t.Errorf("Expected step1 status to be StepRunning, got %v", pm.stepMap["step1"].Status) + } + + // Test transition to completed + err = pm.Complete("step1") + if err != nil { + t.Fatalf("Failed to complete step1: %v", err) + } + + if pm.stepMap["step1"].Status != StepCompleted { + t.Errorf("Expected step1 status to be StepCompleted, got %v", pm.stepMap["step1"].Status) + } + + // Test transition to failed + err = pm.Start("step2") + if err != nil { + t.Fatalf("Failed to start step2: %v", err) + } + + testErr := fmt.Errorf("test error") + err = pm.Fail("step2", testErr) + if err != nil { + t.Fatalf("Failed to fail step2: %v", err) + } + + if pm.stepMap["step2"].Status != StepFailed { + t.Errorf("Expected step2 status to be StepFailed, got %v", pm.stepMap["step2"].Status) + } +} + +func TestEmptySteps(t *testing.T) { + pm := newTestProgressManager([]*Step{}) + + if len(pm.steps) != 0 { + t.Errorf("Expected 0 steps, got %d", len(pm.steps)) + } + + if len(pm.stepMap) != 0 { + t.Errorf("Expected 0 step map entries, got %d", len(pm.stepMap)) + } +} + +func TestMultipleOperationsOnSameStep(t *testing.T) { + steps := []*Step{ + {ID: "step", Message: "Test step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + // Start the step + err := pm.Start("step") + if err != nil { + t.Fatalf("Failed to start step: %v", err) + } + + // Complete it + err = pm.Complete("step") + if err != nil { + t.Fatalf("Failed to complete step: %v", err) + } + + // Try to complete it again (should still work) + err = pm.Complete("step") + if err != nil { + t.Errorf("Expected second complete to succeed, got error: %v", err) + } + + // Start it again + err = pm.Start("step") + if err != nil { + t.Fatalf("Failed to restart step: %v", err) + } + + // Fail it + testErr := fmt.Errorf("test error") + err = pm.Fail("step", testErr) + if err != nil { + t.Fatalf("Failed to fail step: %v", err) + } +} + +func TestStopAndRestartSpinner(t *testing.T) { + steps := []*Step{ + {ID: "step", Message: "Test step", IndentLevel: 1}, + } + + pm := newTestProgressManager(steps) + defer pm.Stop() // Clean up any active spinners + + // Start the step + err := pm.Start("step") + if err != nil { + t.Fatalf("Failed to start step: %v", err) + } + + // Verify spinner is active + if pm.currentSpinner == nil { + t.Fatal("Expected currentSpinner to be set") + } + + // Stop the spinner + pm.Stop() + + // Verify spinner is cleaned up + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after stop") + } + + // Start it again + err = pm.Start("step") + if err != nil { + t.Fatalf("Failed to restart step: %v", err) + } + + // Verify spinner is active again + if pm.currentSpinner == nil { + t.Error("Expected currentSpinner to be set after restart") + } + + // Complete it + err = pm.Complete("step") + if err != nil { + t.Fatalf("Failed to complete step: %v", err) + } + + // Verify spinner is cleaned up after completion + if pm.currentSpinner != nil { + t.Error("Expected currentSpinner to be nil after completion") + } +} diff --git a/go.mod b/go.mod index d1830ee25a6..ee1cd2381dc 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( github.com/pion/rtp v1.8.21 github.com/pion/stun v0.6.1 github.com/prometheus/procfs v0.15.1 + github.com/pterm/pterm v0.12.82 github.com/rhysd/actionlint v1.7.8 github.com/rs/cors v1.11.1 github.com/samber/lo v1.51.0 @@ -114,6 +115,9 @@ require ( require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 // indirect cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.115.1 // indirect @@ -192,6 +196,7 @@ require ( github.com/ckaznocha/intrange v0.2.0 // indirect github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/containerd/console v1.0.5 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect @@ -261,6 +266,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/gookit/color v1.5.4 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect @@ -300,6 +306,7 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lufeee/execinquery v1.2.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/macabu/inamedparam v0.1.3 // indirect @@ -412,6 +419,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xtgo/set v1.0.0 // indirect github.com/yagipy/maintidx v1.0.0 // indirect diff --git a/go.sum b/go.sum index 45e82c28645..788dc398fbb 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,14 @@ 4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= 4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= 4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 h1:0nWhrRcnkgw1kwJ7xibIO8bqfOA7pBzBjGCDBxIHch8= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= @@ -98,6 +106,15 @@ github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= @@ -153,6 +170,7 @@ github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1 github.com/ashanbrown/makezero v0.0.0-20201205152432-7b7cdbb3025a/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= 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-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= @@ -293,6 +311,9 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= @@ -678,6 +699,10 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gookit/color v1.3.6/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= @@ -833,6 +858,9 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e h1:+lIPJOWl+jSiJOc70QXJ07+2eg2Jy2EC7Mi11BWujeM= github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -893,6 +921,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lmittmann/ppm v1.0.2 h1:YW2FFG864rGdrzYu41XngKfptOQU2V+cOmi/hBbaUlI= github.com/lmittmann/ppm v1.0.2/go.mod h1:GObNM/dbtplb87+9xClwI9bZ+AOPMg0Ujf4k3iLo23E= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= @@ -1197,6 +1227,15 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ= +github.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= github.com/quasilyte/go-ruleguard v0.3.0/go.mod h1:p2miAhLp6fERzFNbcuQ4bevXs8rgK//uCHsUDkumITg= github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= @@ -1256,6 +1295,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/securego/gosec/v2 v2.6.1/go.mod h1:I76p3NTHBXsGhybUW+cEQ692q2Vp+A0Z6ZLzDIZy+Ao= github.com/securego/gosec/v2 v2.21.2 h1:deZp5zmYf3TWwU7A7cR2+SolbTpZ3HQiwFqnzQyEl3M= github.com/securego/gosec/v2 v2.21.2/go.mod h1:au33kg78rNseF5PwPnTWhuYBFf534bvJRvOrgZ/bFzU= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= @@ -1439,6 +1479,9 @@ github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhY github.com/xfmoulet/qoi v0.2.0 h1:+Smrwzy5ptRnPzGm/YHkZfyK9qGUSoOpiEPngGmFv+c= github.com/xfmoulet/qoi v0.2.0/go.mod h1:uuPUygmV7o8qy7PhiaGAQX0iLiqoUvFEUKjwUFtlaTQ= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY= @@ -1763,10 +1806,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/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= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1785,6 +1830,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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=