diff --git a/clicky_lookup.go b/clicky_lookup.go new file mode 100644 index 00000000..4b51b9d5 --- /dev/null +++ b/clicky_lookup.go @@ -0,0 +1,92 @@ +package clicky + +import "github.com/flanksource/clicky/api" + +type entityLookupResponse struct { + Filters map[string]entityLookupFilter `json:"filters"` +} + +type entityLookupFilter struct { + Label string `json:"label,omitempty"` + Options map[string]clickyNode `json:"options,omitempty"` + Selected map[string]clickyNode `json:"selected,omitempty"` + Multi bool `json:"multi,omitempty"` + Type string `json:"type,omitempty"` +} + +type clickyNode struct { + Kind string `json:"kind"` + Plain string `json:"plain,omitempty"` + Text string `json:"text,omitempty"` + Style *clickyStyle `json:"style,omitempty"` + Children []clickyNode `json:"children,omitempty"` + Tooltip *clickyNode `json:"tooltip,omitempty"` +} + +type clickyStyle struct { + ClassName string `json:"className,omitempty"` +} + +func toClickyNodeMap(values map[string]api.Textable) map[string]clickyNode { + if len(values) == 0 { + return nil + } + + nodes := make(map[string]clickyNode, len(values)) + for key, value := range values { + nodes[key] = toClickyNode(value) + } + return nodes +} + +func toClickyNode(value api.Textable) clickyNode { + switch typed := value.(type) { + case api.Text: + return textToClickyNode(typed) + case *api.Text: + if typed == nil { + return clickyNode{Kind: "text"} + } + return textToClickyNode(*typed) + default: + if value == nil { + return clickyNode{Kind: "text"} + } + plain := value.String() + return clickyNode{ + Kind: "text", + Text: plain, + Plain: plain, + } + } +} + +func textToClickyNode(text api.Text) clickyNode { + node := clickyNode{ + Kind: "text", + Text: text.Content, + Plain: text.String(), + } + + if text.Style != "" { + node.Style = &clickyStyle{ClassName: text.Style} + } + + if len(text.Children) > 0 { + node.Children = make([]clickyNode, 0, len(text.Children)) + for _, child := range text.Children { + node.Children = append(node.Children, toClickyNode(child)) + } + } + + if text.Tooltip != nil { + tooltip := toClickyNode(text.Tooltip) + node.Tooltip = &tooltip + } + + if node.Text == "" && node.Plain != "" { + node.Text = node.Plain + } + + return node +} diff --git a/cobra_command.go b/cobra_command.go index 72710c52..fb6576d9 100644 --- a/cobra_command.go +++ b/cobra_command.go @@ -17,6 +17,9 @@ import ( // that can invoke the user function directly with flag values. var dataFuncRegistry sync.Map // map[*cobra.Command]func(flags map[string]string, args []string) (any, error) +// lookupFuncRegistry maps cobra commands to filter metadata lookup closures. +var lookupFuncRegistry sync.Map // map[*cobra.Command]func(flags map[string]string, args []string) (any, error) + // GetDataFunc returns the direct data function registered for a command, if any. // Used by the RPC converter to wire DataFunc on RPCOperation. func GetDataFunc(cmd *cobra.Command) func(flags map[string]string, args []string) (any, error) { @@ -26,6 +29,14 @@ func GetDataFunc(cmd *cobra.Command) func(flags map[string]string, args []string return nil } +// GetLookupFunc returns the direct lookup function registered for a command, if any. +func GetLookupFunc(cmd *cobra.Command) func(flags map[string]string, args []string) (any, error) { + if v, ok := lookupFuncRegistry.Load(cmd); ok { + return v.(func(flags map[string]string, args []string) (any, error)) + } + return nil +} + // AddCommand creates a Cobra command with automatic flag parsing from struct tags, // execution, and result formatting. // diff --git a/entity.go b/entity.go index f4e89e9c..d61bd388 100644 --- a/entity.go +++ b/entity.go @@ -7,6 +7,7 @@ import ( "os" "reflect" "sort" + "strings" "sync" "github.com/flanksource/clicky/api" @@ -37,6 +38,7 @@ type EntityInfo struct { Operations []EntityOperation Actions []ActionInfo BulkActions []BulkActionInfo + ValidArgs func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) IsAdmin bool } @@ -48,7 +50,9 @@ type EntityOperation struct { // onto the generated cobra command and collects their values into the // flag map passed to DataFunc. Used by actions that implement // ActionFlags; ignored for the built-in CRUD verbs. - FlagsType reflect.Type + FlagsType reflect.Type + LookupFunc func(flags map[string]string, args []string) (any, error) + BindCompletions func(cmd *cobra.Command) } // ActionInfo is the type-erased representation of a single-entity action. @@ -64,11 +68,13 @@ type ActionInfo struct { // BulkActionInfo is the type-erased representation of a bulk action. type BulkActionInfo struct { - Name string - Short string - DataFunc func(flags map[string]string, args []string) (any, error) - FilterFunc func(flags map[string]string, args []string) (any, error) - ListType reflect.Type + Name string + Short string + DataFunc func(flags map[string]string, args []string) (any, error) + FilterFunc func(flags map[string]string, args []string) (any, error) + LookupFunc func(flags map[string]string, args []string) (any, error) + ListType reflect.Type + BindCompletions func(cmd *cobra.Command) } // ActionFlags is a marker interface implemented by options structs that @@ -83,6 +89,15 @@ type ActionFlags interface { ClickyActionFlags() } +// Filter resolves raw entity option values into the backend-specific shape and +// exposes UI metadata for the currently selected and available values. +type Filter[ListOpts any] interface { + Key() string + Label() string + Lookup(opts *ListOpts) (map[string]api.Textable, error) + Options(opts ListOpts) map[string]api.Textable +} + // Action represents a custom operation on a single entity by ID. type Action[T EntityItem] struct { Name string @@ -128,6 +143,7 @@ type Entity[T EntityItem, ListOpts any] struct { Create func(body map[string]any) (any, error) Update func(id string, body map[string]any) (any, error) Delete func(id string) error + Filters []Filter[ListOpts] Actions []Action[T] BulkActions []BulkAction[T, ListOpts] @@ -150,24 +166,30 @@ func RegisterEntity[T EntityItem, ListOpts any](e Entity[T, ListOpts]) { } info := EntityInfo{ - Name: name, - Parent: e.Parent, - Aliases: e.Aliases, - Type: reflect.TypeOf((*T)(nil)).Elem(), - ListType: reflect.TypeOf((*ListOpts)(nil)).Elem(), + Name: name, + Parent: e.Parent, + Aliases: e.Aliases, + Type: reflect.TypeOf((*T)(nil)).Elem(), + ListType: reflect.TypeOf((*ListOpts)(nil)).Elem(), + ValidArgs: e.ValidArgs, } if e.List != nil { info.Operations = append(info.Operations, EntityOperation{ Verb: "list", DataFunc: func(flagMap map[string]string, args []string) (any, error) { - opts := buildOpts[ListOpts](flagMap) + opts, err := resolveEntityOpts[ListOpts](flagMap, e.Filters) + if err != nil { + return nil, err + } items, err := e.List(opts) if err != nil { return nil, err } return withEntityIDs(items), nil }, + LookupFunc: buildLookupFunc[ListOpts](e.Filters), + BindCompletions: buildFilterCompletionBinder[ListOpts](e.Filters), }) } @@ -285,9 +307,14 @@ func RegisterEntity[T EntityItem, ListOpts any](e Entity[T, ListOpts]) { } if b.RunFilter != nil { bai.FilterFunc = func(flagMap map[string]string, args []string) (any, error) { - opts := buildOpts[ListOpts](flagMap) + opts, err := resolveEntityOpts[ListOpts](flagMap, e.Filters) + if err != nil { + return nil, err + } return b.RunFilter(opts, flagMap) } + bai.LookupFunc = buildLookupFunc[ListOpts](e.Filters) + bai.BindCompletions = buildFilterCompletionBinder[ListOpts](e.Filters) } info.BulkActions = append(info.BulkActions, bai) } @@ -297,23 +324,48 @@ func RegisterEntity[T EntityItem, ListOpts any](e Entity[T, ListOpts]) { if admin.Name == "" || admin.Name == name { admin.Name = name } + adminValidArgs := admin.ValidArgs + if adminValidArgs == nil { + adminValidArgs = e.ValidArgs + } // Store as admin sub-entity — GenerateCLI nests under "admin" parent adminInfo := EntityInfo{ - Name: admin.Name, - Type: info.Type, - ListType: info.ListType, - IsAdmin: true, + Name: admin.Name, + Type: info.Type, + ListType: info.ListType, + ValidArgs: adminValidArgs, + IsAdmin: true, } if admin.List != nil { adminInfo.Operations = append(adminInfo.Operations, EntityOperation{ Verb: "list", DataFunc: func(flagMap map[string]string, args []string) (any, error) { - opts := buildOpts[ListOpts](flagMap) + opts, err := resolveEntityOpts[ListOpts](flagMap, admin.Filters) + if err != nil { + return nil, err + } return admin.List(opts) }, + LookupFunc: buildLookupFunc[ListOpts](admin.Filters), + BindCompletions: buildFilterCompletionBinder[ListOpts](admin.Filters), }) } - if admin.Get != nil { + if admin.GetWithFlags != nil { + adminInfo.Operations = append(adminInfo.Operations, EntityOperation{ + Verb: "get", + FlagsType: actionFlagsType(admin.GetFlags), + DataFunc: func(flagMap map[string]string, args []string) (any, error) { + id := flagMap["id"] + if id == "" && len(args) > 0 { + id = args[0] + } + if id == "" { + return nil, fmt.Errorf("id is required") + } + return admin.GetWithFlags(id, flagMap) + }, + }) + } else if admin.Get != nil { adminInfo.Operations = append(adminInfo.Operations, EntityOperation{ Verb: "get", DataFunc: func(flagMap map[string]string, args []string) (any, error) { @@ -424,7 +476,7 @@ func generateEntityCLI(parent *cobra.Command, entity EntityInfo) { Verb: action.Name, DataFunc: action.DataFunc, FlagsType: action.FlagsType, - }) + }, entity.ValidArgs) } for _, ba := range entity.BulkActions { @@ -437,13 +489,13 @@ func generateEntitySubcommand(parent *cobra.Command, entity EntityInfo, op Entit case "list": generateListCommand(parent, entity, op) case "get": - generateIDCommand(parent, "get", fmt.Sprintf("Get a %s by ID", entity.Name), op) + generateIDCommand(parent, "get", fmt.Sprintf("Get a %s by ID", entity.Name), op, entity.ValidArgs) case "create": generateBodyCommand(parent, "create", fmt.Sprintf("Create a %s", entity.Name), op) case "update": generateBodyCommand(parent, "update", fmt.Sprintf("Update a %s", entity.Name), op) case "delete": - generateIDCommand(parent, "delete", fmt.Sprintf("Delete a %s", entity.Name), op) + generateIDCommand(parent, "delete", fmt.Sprintf("Delete a %s", entity.Name), op, entity.ValidArgs) } } @@ -467,12 +519,18 @@ func generateListCommand(parent *cobra.Command, entity EntityInfo, op EntityOper // Bind filter flags from the ListOpts type bindTypeFlags(cmd, entity.ListType) + if op.BindCompletions != nil { + op.BindCompletions(cmd) + } parent.AddCommand(cmd) dataFuncRegistry.Store(cmd, op.DataFunc) + if op.LookupFunc != nil { + lookupFuncRegistry.Store(cmd, op.LookupFunc) + } } -func generateIDCommand(parent *cobra.Command, verb, short string, op EntityOperation) { +func generateIDCommand(parent *cobra.Command, verb, short string, op EntityOperation, validArgs func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)) { hasFlags := op.FlagsType != nil use := fmt.Sprintf("%s ", verb) if hasFlags { @@ -503,6 +561,9 @@ func generateIDCommand(parent *cobra.Command, verb, short string, op EntityOpera if verb == "get" { cmd.Aliases = []string{"inspect"} } + if validArgs != nil { + cmd.ValidArgsFunction = validArgs + } if hasFlags { bindTypeFlags(cmd, op.FlagsType) } @@ -579,6 +640,13 @@ func generateBodyCommand(parent *cobra.Command, verb, short string, op EntityOpe } func generateBulkActionCommand(parent *cobra.Command, ba BulkActionInfo) { + execute := func(flagMap map[string]string, args []string) (any, error) { + if ba.FilterFunc != nil && flagMap["filter"] != "" { + return ba.FilterFunc(flagMap, args) + } + return ba.DataFunc(flagMap, args) + } + cmd := &cobra.Command{ Use: fmt.Sprintf("%s [id...]", ba.Name), Short: ba.Short, @@ -590,18 +658,7 @@ func generateBulkActionCommand(parent *cobra.Command, ba BulkActionInfo) { }) // Use filter mode if --filter flag is set and FilterFunc exists - if ba.FilterFunc != nil && flagMap["filter"] != "" { - result, err := ba.FilterFunc(flagMap, args) - if err != nil { - return err - } - if result != nil { - MustPrint(result, Flags.FormatOptions) - } - return nil - } - - result, err := ba.DataFunc(flagMap, args) + result, err := execute(flagMap, args) if err != nil { return err } @@ -614,10 +671,16 @@ func generateBulkActionCommand(parent *cobra.Command, ba BulkActionInfo) { if ba.FilterFunc != nil { bindTypeFlags(cmd, ba.ListType) + if ba.BindCompletions != nil { + ba.BindCompletions(cmd) + } } parent.AddCommand(cmd) - dataFuncRegistry.Store(cmd, ba.DataFunc) + dataFuncRegistry.Store(cmd, execute) + if ba.LookupFunc != nil { + lookupFuncRegistry.Store(cmd, ba.LookupFunc) + } } // bindTypeFlags registers cobra flags from a struct type's field tags. @@ -628,33 +691,243 @@ func bindTypeFlags(cmd *cobra.Command, t reflect.Type) { } } +func buildFilterCompletionBinder[T any](filters []Filter[T]) func(cmd *cobra.Command) { + if len(filters) == 0 { + return nil + } + + return func(cmd *cobra.Command) { + for _, filter := range filters { + if cmd.Flag(filter.Key()) == nil { + continue + } + if _, exists := cmd.GetFlagCompletionFunc(filter.Key()); exists { + continue + } + + filter := filter + _ = cmd.RegisterFlagCompletionFunc(filter.Key(), func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + flagMap := make(map[string]string) + cmd.Flags().Visit(func(f *pflag.Flag) { + if f.Name == filter.Key() { + return + } + flagMap[f.Name] = f.Value.String() + }) + + opts, err := resolveEntityOpts[T](flagMap, filters) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + options := filter.Options(opts) + if len(options) == 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + keys := make([]string, 0, len(options)) + for key := range options { + if strings.HasPrefix(key, toComplete) { + keys = append(keys, key) + } + } + sort.Strings(keys) + + completions := make([]string, 0, len(keys)) + for _, key := range keys { + description := sanitizeCompletionDescription(options[key].String()) + if description == "" || description == key { + completions = append(completions, key) + continue + } + completions = append(completions, cobra.CompletionWithDesc(key, description)) + } + + return completions, cobra.ShellCompDirectiveNoFileComp + }) + } + } +} + +func sanitizeCompletionDescription(value string) string { + value = strings.TrimSpace(value) + value = strings.ReplaceAll(value, "\t", " ") + value = strings.ReplaceAll(value, "\n", " ") + return value +} + // buildOpts creates an instance of T and populates it from a flag map. -func buildOpts[T any](flagMap map[string]string) T { +func buildOpts[T any](flagMap map[string]string) (T, error) { + var zero T t := reflect.TypeOf((*T)(nil)).Elem() - v := reflect.New(t).Elem() + if t.Kind() != reflect.Struct { + return zero, fmt.Errorf("expected struct options, got %s", t.Kind()) + } - fieldInfos, _ := flags.ParseStructFields(t) + cmd := &cobra.Command{Use: "opts"} + fieldInfos, err := flags.ParseStructFields(t) + if err != nil { + return zero, err + } + flagValues := make([]*flags.FlagValue, 0, len(fieldInfos)) for _, info := range fieldInfos { - name := info.FlagName - if name == "" { - name = lo.KebabCase(info.FieldName) - } - if val, ok := flagMap[name]; ok && val != "" { - field := v.FieldByIndex(info.FieldPath) - switch field.Kind() { - case reflect.String: - field.SetString(val) - case reflect.Int, reflect.Int64: - var n int64 - _, _ = fmt.Sscanf(val, "%d", &n) - field.SetInt(n) - case reflect.Bool: - field.SetBool(val == "true") + if fv := flags.BindFlag(cmd, info); fv != nil { + flagValues = append(flagValues, fv) + } + } + + for name, val := range flagMap { + flag := cmd.Flags().Lookup(name) + if flag == nil { + continue + } + if err := flag.Value.Set(val); err != nil { + return zero, fmt.Errorf("setting flag %s: %w", name, err) + } + flag.Changed = true + } + + v := reflect.New(t).Elem() + for _, fv := range flagValues { + if err := flags.AssignFieldValue(v, fv, nil, false); err != nil { + return zero, err + } + } + + return v.Interface().(T), nil +} + +func resolveEntityOpts[T any](flagMap map[string]string, filters []Filter[T]) (T, error) { + opts, err := buildOpts[T](flagMap) + if err != nil { + return opts, err + } + _, err = applyEntityFilters(&opts, filters) + return opts, err +} + +func buildLookupFunc[T any](filters []Filter[T]) func(flags map[string]string, args []string) (any, error) { + if len(filters) == 0 { + return nil + } + + lookupMetadata := buildLookupMetadata[T]() + + return func(flagMap map[string]string, args []string) (any, error) { + opts, err := buildOpts[T](flagMap) + if err != nil { + return nil, err + } + + selected, err := applyEntityFilters(&opts, filters) + if err != nil { + return nil, err + } + + response := entityLookupResponse{ + Filters: make(map[string]entityLookupFilter, len(filters)), + } + for _, filter := range filters { + meta := lookupMetadata[filter.Key()] + response.Filters[filter.Key()] = entityLookupFilter{ + Label: filter.Label(), + Options: toClickyNodeMap(filter.Options(opts)), + Selected: toClickyNodeMap(selected[filter.Key()]), + Multi: meta.Multi, + Type: meta.Type, } } + return response, nil + } +} + +type entityLookupMetadata struct { + Multi bool + Type string +} + +func buildLookupMetadata[T any]() map[string]entityLookupMetadata { + structType := reflect.TypeOf((*T)(nil)).Elem() + if structType.Kind() != reflect.Struct { + return nil + } + + fields, err := flags.ParseStructFields(structType) + if err != nil { + return nil + } + + metadata := make(map[string]entityLookupMetadata, len(fields)) + for _, field := range fields { + if field.FlagName == "" || field.FlagName == "-" { + continue + } + metadata[field.FlagName] = describeLookupField(field) + } + + return metadata +} + +func describeLookupField(field flags.FieldInfo) entityLookupMetadata { + fieldType := field.FieldType + for fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + meta := entityLookupMetadata{ + Multi: fieldType.Kind() == reflect.Slice, } - return v.Interface().(T) + switch { + case fieldType.Kind() == reflect.Bool: + meta.Type = "bool" + case isNumericKind(fieldType.Kind()): + meta.Type = "number" + case fieldType.String() == "time.Time": + switch { + case isRangeStartFlag(field.FlagName): + meta.Type = "from" + case isRangeEndFlag(field.FlagName): + meta.Type = "to" + default: + meta.Type = "date" + } + } + + return meta +} + +func isNumericKind(kind reflect.Kind) bool { + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + +func isRangeStartFlag(name string) bool { + return name == "from" || strings.HasSuffix(name, "-from") +} + +func isRangeEndFlag(name string) bool { + return name == "to" || strings.HasSuffix(name, "-to") +} + +func applyEntityFilters[T any](opts *T, filters []Filter[T]) (map[string]map[string]api.Textable, error) { + selected := make(map[string]map[string]api.Textable, len(filters)) + for _, filter := range filters { + values, err := filter.Lookup(opts) + if err != nil { + return nil, fmt.Errorf("%s: %w", filter.Key(), err) + } + if len(values) > 0 { + selected[filter.Key()] = values + } + } + return selected, nil } // withEntityIDs wraps each EntityItem in the slice to include a _id field in JSON output. diff --git a/entity_filters_test.go b/entity_filters_test.go new file mode 100644 index 00000000..8fc5046b --- /dev/null +++ b/entity_filters_test.go @@ -0,0 +1,611 @@ +package clicky + +import ( + "encoding/json" + "strings" + "testing" + "time" + + "github.com/flanksource/clicky/api" + "github.com/flanksource/commons/duration" + "github.com/spf13/cobra" +) + +type entityFilterTestEntity struct { + ID string + Name string +} + +func (e entityFilterTestEntity) GetID() string { return e.ID } +func (e entityFilterTestEntity) GetName() string { return e.Name } + +type entityFilterEmbeddedOpts struct { + Tags []string `flag:"tags"` + Window duration.Duration `flag:"window" default:"24h"` +} + +type entityFilterTestOpts struct { + entityFilterEmbeddedOpts + Owner string `flag:"owner"` + Status string `flag:"status"` + Active bool `flag:"active"` + Since time.Time `flag:"since"` + From time.Time `flag:"from"` + To time.Time `flag:"to"` +} + +type ownerEntityFilter struct{} + +func (ownerEntityFilter) Key() string { return "owner" } +func (ownerEntityFilter) Label() string { return "Owner" } + +func (ownerEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + if opts.Owner == "" { + return nil, nil + } + opts.Owner = "team/" + opts.Owner + return map[string]api.Textable{ + opts.Owner: api.Text{Content: strings.TrimPrefix(opts.Owner, "team/")}, + }, nil +} + +func (ownerEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + return map[string]api.Textable{ + "team/platform": api.Text{Content: "Platform"}, + "team/core": api.Text{Content: "Core"}, + } +} + +type statusEntityFilter struct{} + +func (statusEntityFilter) Key() string { return "status" } +func (statusEntityFilter) Label() string { return "Status" } + +func (statusEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + if opts.Status == "" { + return nil, nil + } + + switch opts.Status { + case "healthy": + opts.Status = "status:healthy" + return map[string]api.Textable{ + opts.Status: api.Text{ + Content: "Healthy", + Style: "font-semibold", + Tooltip: api.Text{Content: "Ready"}, + }, + }, nil + case "degraded": + opts.Status = "status:degraded" + return map[string]api.Textable{ + opts.Status: api.Text{Content: "Degraded"}, + }, nil + default: + return nil, nil + } +} + +func (statusEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + options := map[string]api.Textable{ + "status:healthy": api.Text{Content: "Healthy", Style: "font-semibold"}, + "status:degraded": api.Text{Content: "Degraded"}, + } + if opts.Owner == "team/platform" { + return map[string]api.Textable{ + "status:healthy": options["status:healthy"], + } + } + return options +} + +type tagsEntityFilter struct{} + +func (tagsEntityFilter) Key() string { return "tags" } +func (tagsEntityFilter) Label() string { return "Tags" } + +func (tagsEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + if len(opts.Tags) == 0 { + return nil, nil + } + + selected := make(map[string]api.Textable, len(opts.Tags)) + for _, tag := range opts.Tags { + selected[tag] = api.Text{Content: strings.ToUpper(tag)} + } + return selected, nil +} + +func (tagsEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + return map[string]api.Textable{ + "api": api.Text{Content: "API"}, + "worker": api.Text{Content: "Worker"}, + } +} + +type activeEntityFilter struct{} + +func (activeEntityFilter) Key() string { return "active" } +func (activeEntityFilter) Label() string { return "Active" } + +func (activeEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + if !opts.Active { + return nil, nil + } + return map[string]api.Textable{ + "true": api.Text{Content: "Active only"}, + }, nil +} + +func (activeEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + return map[string]api.Textable{ + "true": api.Text{Content: "Active only"}, + } +} + +type sinceEntityFilter struct{} + +func (sinceEntityFilter) Key() string { return "since" } +func (sinceEntityFilter) Label() string { return "Since" } + +func (sinceEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + return nil, nil +} + +func (sinceEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + return nil +} + +type fromEntityFilter struct{} + +func (fromEntityFilter) Key() string { return "from" } +func (fromEntityFilter) Label() string { return "From" } + +func (fromEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + return nil, nil +} + +func (fromEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + return nil +} + +type toEntityFilter struct{} + +func (toEntityFilter) Key() string { return "to" } +func (toEntityFilter) Label() string { return "To" } + +func (toEntityFilter) Lookup(opts *entityFilterTestOpts) (map[string]api.Textable, error) { + return nil, nil +} + +func (toEntityFilter) Options(opts entityFilterTestOpts) map[string]api.Textable { + return nil +} + +func TestBuildOptsSupportsRichTypes(t *testing.T) { + opts, err := buildOpts[entityFilterTestOpts](map[string]string{ + "tags": "api,worker", + "window": "48h", + "since": "2026-04-20T15:04:05Z", + "owner": "platform", + "status": "healthy", + }) + if err != nil { + t.Fatalf("buildOpts returned error: %v", err) + } + + if len(opts.Tags) != 2 || opts.Tags[0] != "api" || opts.Tags[1] != "worker" { + t.Fatalf("expected tags to be parsed, got %#v", opts.Tags) + } + + if time.Duration(opts.Window) != 48*time.Hour { + t.Fatalf("expected 48h window, got %s", opts.Window) + } + + expectedSince := time.Date(2026, 4, 20, 15, 4, 5, 0, time.UTC) + if !opts.Since.Equal(expectedSince) { + t.Fatalf("expected since %s, got %s", expectedSince, opts.Since) + } + + if opts.Owner != "platform" || opts.Status != "healthy" { + t.Fatalf("expected string fields to be populated, got %#v", opts) + } +} + +func TestEntityListFiltersResolveAndLookup(t *testing.T) { + resetEntityRegistry(t) + defer resetEntityRegistry(t) + + var received entityFilterTestOpts + + RegisterEntity(Entity[entityFilterTestEntity, entityFilterTestOpts]{ + Name: "filtered-entity", + Filters: []Filter[entityFilterTestOpts]{ + ownerEntityFilter{}, + statusEntityFilter{}, + tagsEntityFilter{}, + activeEntityFilter{}, + sinceEntityFilter{}, + fromEntityFilter{}, + toEntityFilter{}, + }, + List: func(opts entityFilterTestOpts) ([]entityFilterTestEntity, error) { + received = opts + return []entityFilterTestEntity{{ID: "1", Name: "alpha"}}, nil + }, + }) + + root := &cobra.Command{Use: "root"} + GenerateCLI(root) + + listCmd, _, err := root.Find([]string{"filtered-entity", "list"}) + if err != nil || listCmd == nil { + t.Fatalf("expected to find list command, got err=%v", err) + } + + dataFunc := GetDataFunc(listCmd) + if dataFunc == nil { + t.Fatal("expected list command to register data func") + } + + if _, err := dataFunc(map[string]string{ + "owner": "platform", + "status": "healthy", + "tags": "api,worker", + "window": "48h", + "since": "2026-04-20T15:04:05Z", + }, nil); err != nil { + t.Fatalf("list data func returned error: %v", err) + } + + if received.Owner != "team/platform" { + t.Fatalf("expected owner to be resolved, got %q", received.Owner) + } + + if received.Status != "status:healthy" { + t.Fatalf("expected status to be resolved, got %q", received.Status) + } + + if len(received.Tags) != 2 || received.Tags[0] != "api" || received.Tags[1] != "worker" { + t.Fatalf("expected tags to be parsed, got %#v", received.Tags) + } + + if time.Duration(received.Window) != 48*time.Hour { + t.Fatalf("expected resolved window to be 48h, got %s", received.Window) + } + + lookupFunc := GetLookupFunc(listCmd) + if lookupFunc == nil { + t.Fatal("expected list command to register lookup func") + } + + lookupAny, err := lookupFunc(map[string]string{ + "owner": "platform", + "status": "healthy", + }, nil) + if err != nil { + t.Fatalf("lookup func returned error: %v", err) + } + + lookup, ok := lookupAny.(entityLookupResponse) + if !ok { + t.Fatalf("expected lookup response type, got %T", lookupAny) + } + + statusLookup, ok := lookup.Filters["status"] + if !ok { + t.Fatalf("expected status lookup payload, got %#v", lookup.Filters) + } + + if len(statusLookup.Options) != 1 { + t.Fatalf("expected narrowed status options, got %#v", statusLookup.Options) + } + + if !lookup.Filters["tags"].Multi { + t.Fatalf("expected tags lookup to advertise multi=true, got %#v", lookup.Filters["tags"]) + } + + if lookup.Filters["active"].Type != "bool" { + t.Fatalf("expected active lookup type=bool, got %#v", lookup.Filters["active"]) + } + + if lookup.Filters["since"].Type != "date" { + t.Fatalf("expected since lookup type=date, got %#v", lookup.Filters["since"]) + } + + if lookup.Filters["from"].Type != "from" { + t.Fatalf("expected from lookup type=from, got %#v", lookup.Filters["from"]) + } + + if lookup.Filters["to"].Type != "to" { + t.Fatalf("expected to lookup type=to, got %#v", lookup.Filters["to"]) + } + + selected := statusLookup.Selected["status:healthy"] + if selected.Kind != "text" || selected.Text != "Healthy" { + t.Fatalf("expected selected option to be clicky text, got %#v", selected) + } + + if selected.Style == nil || selected.Style.ClassName != "font-semibold" { + t.Fatalf("expected selected option style to be preserved, got %#v", selected.Style) + } + + if selected.Tooltip == nil || selected.Tooltip.Text != "Ready" { + t.Fatalf("expected selected option tooltip to be preserved, got %#v", selected.Tooltip) + } + + data, err := json.Marshal(lookup) + if err != nil { + t.Fatalf("failed to marshal lookup response: %v", err) + } + + if !strings.Contains(string(data), `"kind":"text"`) { + t.Fatalf("expected marshaled lookup to contain clicky nodes, got %s", string(data)) + } +} + +func TestEntityBulkActionUsesResolvedFilters(t *testing.T) { + resetEntityRegistry(t) + defer resetEntityRegistry(t) + + var received entityFilterTestOpts + + RegisterEntity(Entity[entityFilterTestEntity, entityFilterTestOpts]{ + Name: "filtered-bulk", + Filters: []Filter[entityFilterTestOpts]{ownerEntityFilter{}, statusEntityFilter{}}, + BulkActions: []BulkAction[entityFilterTestEntity, entityFilterTestOpts]{ + { + Name: "bulk-suspend", + Short: "Suspend entities matched by filters", + Run: func(ids []string, flags map[string]string) (any, error) { + return ids, nil + }, + RunFilter: func(opts entityFilterTestOpts, flags map[string]string) (any, error) { + received = opts + return map[string]string{ + "owner": opts.Owner, + "status": opts.Status, + }, nil + }, + }, + }, + }) + + root := &cobra.Command{Use: "root"} + GenerateCLI(root) + + bulkCmd, _, err := root.Find([]string{"filtered-bulk", "bulk-suspend"}) + if err != nil || bulkCmd == nil { + t.Fatalf("expected to find bulk action command, got err=%v", err) + } + + dataFunc := GetDataFunc(bulkCmd) + if dataFunc == nil { + t.Fatal("expected bulk action to register data func") + } + + if _, err := dataFunc(map[string]string{ + "filter": "status == 'healthy'", + "owner": "platform", + "status": "healthy", + }, []string{"123"}); err != nil { + t.Fatalf("bulk action data func returned error: %v", err) + } + + if received.Owner != "team/platform" { + t.Fatalf("expected owner to be resolved for bulk action, got %q", received.Owner) + } + + if received.Status != "status:healthy" { + t.Fatalf("expected status to be resolved for bulk action, got %q", received.Status) + } + + if GetLookupFunc(bulkCmd) == nil { + t.Fatal("expected bulk action to register lookup func") + } +} + +func TestEntityListCommandsRegisterFilterCompletions(t *testing.T) { + resetEntityRegistry(t) + defer resetEntityRegistry(t) + + RegisterEntity(Entity[entityFilterTestEntity, entityFilterTestOpts]{ + Name: "completion-entity", + Filters: []Filter[entityFilterTestOpts]{ownerEntityFilter{}, statusEntityFilter{}}, + List: func(opts entityFilterTestOpts) ([]entityFilterTestEntity, error) { + return nil, nil + }, + }) + + root := &cobra.Command{Use: "root"} + GenerateCLI(root) + + listCmd, _, err := root.Find([]string{"completion-entity", "list"}) + if err != nil || listCmd == nil { + t.Fatalf("expected to find list command, got err=%v", err) + } + + ownerCompletion, ok := listCmd.GetFlagCompletionFunc("owner") + if !ok { + t.Fatal("expected owner flag completion to be registered") + } + + completions, directive := ownerCompletion(listCmd, nil, "") + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Fatalf("expected no-file completion directive, got %v", directive) + } + + expectedOwner := []string{ + cobra.CompletionWithDesc("team/core", "Core"), + cobra.CompletionWithDesc("team/platform", "Platform"), + } + if len(completions) != len(expectedOwner) { + t.Fatalf("expected owner completions %#v, got %#v", expectedOwner, completions) + } + for i, completion := range expectedOwner { + if completions[i] != completion { + t.Fatalf("expected owner completion %q at index %d, got %q", completion, i, completions[i]) + } + } + + if err := listCmd.Flags().Set("owner", "platform"); err != nil { + t.Fatalf("failed to set owner flag: %v", err) + } + + statusCompletion, ok := listCmd.GetFlagCompletionFunc("status") + if !ok { + t.Fatal("expected status flag completion to be registered") + } + + completions, directive = statusCompletion(listCmd, nil, "status:") + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Fatalf("expected no-file completion directive, got %v", directive) + } + + expectedStatus := []string{ + cobra.CompletionWithDesc("status:healthy", "Healthy"), + } + if len(completions) != len(expectedStatus) { + t.Fatalf("expected status completions %#v, got %#v", expectedStatus, completions) + } + for i, completion := range expectedStatus { + if completions[i] != completion { + t.Fatalf("expected status completion %q at index %d, got %q", completion, i, completions[i]) + } + } +} + +func TestEntityBulkCommandsRegisterFilterCompletions(t *testing.T) { + resetEntityRegistry(t) + defer resetEntityRegistry(t) + + RegisterEntity(Entity[entityFilterTestEntity, entityFilterTestOpts]{ + Name: "completion-bulk", + Filters: []Filter[entityFilterTestOpts]{ownerEntityFilter{}, statusEntityFilter{}}, + BulkActions: []BulkAction[entityFilterTestEntity, entityFilterTestOpts]{ + { + Name: "bulk-suspend", + Short: "Suspend entities matched by filters", + Run: func(ids []string, flags map[string]string) (any, error) { + return ids, nil + }, + RunFilter: func(opts entityFilterTestOpts, flags map[string]string) (any, error) { + return nil, nil + }, + }, + }, + }) + + root := &cobra.Command{Use: "root"} + GenerateCLI(root) + + bulkCmd, _, err := root.Find([]string{"completion-bulk", "bulk-suspend"}) + if err != nil || bulkCmd == nil { + t.Fatalf("expected to find bulk action command, got err=%v", err) + } + + if err := bulkCmd.Flags().Set("owner", "platform"); err != nil { + t.Fatalf("failed to set owner flag: %v", err) + } + + statusCompletion, ok := bulkCmd.GetFlagCompletionFunc("status") + if !ok { + t.Fatal("expected bulk status flag completion to be registered") + } + + completions, directive := statusCompletion(bulkCmd, nil, "status:") + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Fatalf("expected no-file completion directive, got %v", directive) + } + + expected := []string{ + cobra.CompletionWithDesc("status:healthy", "Healthy"), + } + if len(completions) != len(expected) { + t.Fatalf("expected status completions %#v, got %#v", expected, completions) + } + for i, completion := range expected { + if completions[i] != completion { + t.Fatalf("expected status completion %q at index %d, got %q", completion, i, completions[i]) + } + } +} + +func TestEntityValidArgsPropagateToGeneratedIDCommands(t *testing.T) { + resetEntityRegistry(t) + defer resetEntityRegistry(t) + + validArgs := func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + all := []string{"alpha", "beta"} + var matches []string + for _, candidate := range all { + if strings.HasPrefix(candidate, toComplete) { + matches = append(matches, candidate) + } + } + return matches, cobra.ShellCompDirectiveNoFileComp + } + + RegisterEntity(Entity[entityFilterTestEntity, entityFilterTestOpts]{ + Name: "id-completion-entity", + ValidArgs: validArgs, + Get: func(id string) (any, error) { + return id, nil + }, + Delete: func(id string) error { + return nil + }, + Actions: []Action[entityFilterTestEntity]{ + { + Name: "restart", + Short: "Restart one entity", + Run: func(id string, flags map[string]string) (any, error) { + return id, nil + }, + }, + }, + Admin: &Entity[entityFilterTestEntity, entityFilterTestOpts]{ + Get: func(id string) (any, error) { + return id, nil + }, + Actions: []Action[entityFilterTestEntity]{ + { + Name: "reconcile", + Short: "Reconcile one entity", + Run: func(id string, flags map[string]string) (any, error) { + return id, nil + }, + }, + }, + }, + }) + + root := &cobra.Command{Use: "root"} + GenerateCLI(root) + + paths := [][]string{ + {"id-completion-entity", "get"}, + {"id-completion-entity", "delete"}, + {"id-completion-entity", "restart"}, + {"admin", "id-completion-entity", "get"}, + {"admin", "id-completion-entity", "reconcile"}, + } + + for _, path := range paths { + cmd, _, err := root.Find(path) + if err != nil || cmd == nil { + t.Fatalf("expected to find command %v, got err=%v", path, err) + } + if cmd.ValidArgsFunction == nil { + t.Fatalf("expected valid args function on %v", path) + } + + completions, directive := cmd.ValidArgsFunction(cmd, nil, "a") + if directive != cobra.ShellCompDirectiveNoFileComp { + t.Fatalf("expected no-file completion directive for %v, got %v", path, directive) + } + if len(completions) != 1 || completions[0] != "alpha" { + t.Fatalf("expected alpha completion for %v, got %#v", path, completions) + } + } +} diff --git a/examples/README.md b/examples/README.md index 43dd1bd8..fadc0752 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,6 +5,7 @@ This directory contains examples showing how to use the clicky library to format ## Files - `clicky-pipe.go` - Main example program that can read JSON from stdin or file +- `enitity/` - End-to-end entity example covering CRUD, filters, admin entities, nested parents, and `openapi serve --enable-executor` - `example-data.json` - Sample order data with complex nested structures and arrays - `go.mod` - Module configuration diff --git a/examples/enitity/e2e/.gitignore b/examples/enitity/e2e/.gitignore new file mode 100644 index 00000000..15791c3f --- /dev/null +++ b/examples/enitity/e2e/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +playwright-report/ +test-results/ +.cache/ diff --git a/examples/enitity/e2e/playwright.config.ts b/examples/enitity/e2e/playwright.config.ts new file mode 100644 index 00000000..49df8923 --- /dev/null +++ b/examples/enitity/e2e/playwright.config.ts @@ -0,0 +1,44 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig, devices } from "@playwright/test"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const exampleDir = path.resolve(__dirname, ".."); + +const PORT = process.env.E2E_PORT ?? "18080"; +const HOST = process.env.E2E_HOST ?? "127.0.0.1"; +const BASE_URL = process.env.E2E_BASE_URL ?? `http://${HOST}:${PORT}`; +const GOCACHE = process.env.GOCACHE ?? "/tmp/go-build"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: false, + workers: 1, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? "github" : "list", + timeout: 45_000, + expect: { + timeout: 10_000, + }, + use: { + baseURL: BASE_URL, + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + ], + webServer: { + command: `cd "${exampleDir}" && GOCACHE="${GOCACHE}" task build && GOCACHE="${GOCACHE}" ./entity-demo serve-ui --host ${HOST} --port ${PORT}`, + url: BASE_URL, + reuseExistingServer: false, + timeout: 240_000, + stdout: "pipe", + stderr: "pipe", + }, +}); diff --git a/examples/enitity/e2e/tests/entity-demo.spec.ts b/examples/enitity/e2e/tests/entity-demo.spec.ts new file mode 100644 index 00000000..94f2f066 --- /dev/null +++ b/examples/enitity/e2e/tests/entity-demo.spec.ts @@ -0,0 +1,313 @@ +import { expect, test, type APIRequestContext, type Locator, type Page } from "@playwright/test"; + +const stackRow = (page: Page, name: string) => + page.locator("tbody tr").filter({ hasText: name }).first(); +const explorerSearch = (page: Page) => page.getByPlaceholder("Search api explorer..."); +const responseBody = (scope: Page | Locator) => + scope.locator('[aria-label="Response body"]'); + +function escapeRegExp(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function explorerCard(page: Page, path: string) { + return page.locator("a").filter({ + has: page.locator("code").filter({ + hasText: new RegExp(`^${escapeRegExp(path)}$`), + }), + }); +} + +function waitForJson(page: Page, pathname: string, expected: Record = {}) { + return page.waitForResponse((response) => { + const url = new URL(response.url()); + if (url.pathname !== pathname || !response.ok()) { + return false; + } + if (url.searchParams.get("__lookup") === "filters") { + return false; + } + return Object.entries(expected).every( + ([key, value]) => url.searchParams.get(key) === value, + ); + }); +} + +function waitForLookup(page: Page, pathname: string, expected: Record) { + return page.waitForResponse((response) => { + const url = new URL(response.url()); + if (url.pathname !== pathname || !response.ok()) { + return false; + } + if (url.searchParams.get("__lookup") !== "filters") { + return false; + } + return Object.entries(expected).every( + ([key, value]) => url.searchParams.get(key) === value, + ); + }); +} + +async function resetDemoStore(request: APIRequestContext) { + const response = await request.post("/api/v1/stack/seed"); + expect(response.ok()).toBeTruthy(); + expect(await response.text()).toContain("reset demo store with 3 stacks and 3 clusters"); +} + +test.describe("entity demo", () => { + test.beforeEach(async ({ request }) => { + await resetDemoStore(request); + }); + + test("boots from / into stacks and narrows rows with the live lookup filter", async ({ + page, + }) => { + const openApi = waitForJson(page, "/api/openapi.json"); + const list = waitForJson(page, "/api/v1/stack"); + const lookup = waitForLookup(page, "/api/v1/stack", {}); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await Promise.all([openApi, list, lookup]); + + await expect(page).toHaveURL(/\/stacks$/); + await expect(page.getByRole("heading", { name: "Stacks" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Stacks", exact: true })).toBeVisible(); + await expect(page.getByRole("link", { name: "Clusters" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Admin — Stacks" })).toBeVisible(); + await expect(page.getByRole("link", { name: "API Explorer" })).toBeVisible(); + + await expect(stackRow(page, "checkout")).toBeVisible(); + await expect(stackRow(page, "billing")).toBeVisible(); + await expect(page.getByText("marketing-site")).toHaveCount(0); + await expect(page.getByRole("button", { name: /time range filter/i })).toBeVisible(); + + const filteredLookup = waitForLookup(page, "/api/v1/stack", { + team: "team/platform", + }); + await page.getByLabel("Team").fill("team/platform"); + await filteredLookup; + + const filteredList = page.waitForResponse((response) => { + const url = new URL(response.url()); + return ( + url.pathname === "/api/v1/stack" && + response.ok() && + !url.searchParams.has("__lookup") && + url.searchParams.get("team") === "team/platform" + ); + }); + await page.getByRole("button", { name: "Apply" }).click(); + await filteredList; + + await expect(stackRow(page, "checkout")).toBeVisible(); + await expect(stackRow(page, "billing")).toHaveCount(0); + await expect(page.getByText("marketing-site")).toHaveCount(0); + }); + + test("renders the special time range filter and applies from/to over the real stack list", async ({ + page, + }) => { + const stacks = waitForJson(page, "/api/v1/stack"); + const lookup = waitForLookup(page, "/api/v1/stack", {}); + + await page.goto("/stacks", { waitUntil: "domcontentloaded" }); + await Promise.all([stacks, lookup]); + + await expect(page.getByRole("button", { name: /time range filter/i })).toBeVisible(); + await expect(stackRow(page, "checkout")).toBeVisible(); + await expect(stackRow(page, "billing")).toBeVisible(); + + const rangedLookup = waitForLookup(page, "/api/v1/stack", { + from: "now-24h", + to: "now", + }); + await page.getByRole("button", { name: /time range filter/i }).click(); + await page.getByRole("button", { name: /last 24 hours/i }).click(); + await rangedLookup; + + const rangedList = waitForJson(page, "/api/v1/stack", { + from: "now-24h", + to: "now", + }); + await page.getByRole("button", { name: "Apply" }).click(); + await rangedList; + + await expect(stackRow(page, "checkout")).toBeVisible(); + await expect(stackRow(page, "billing")).toHaveCount(0); + + const adminList = waitForJson(page, "/api/v1/admin/stack"); + await page.goto("/admin-stacks", { waitUntil: "domcontentloaded" }); + await adminList; + await expect(page.getByRole("button", { name: /time range filter/i })).toBeVisible(); + }); + + test("shows cluster rows and the real cluster endpoint list", async ({ page }) => { + const clusters = waitForJson(page, "/api/v1/catalog/cluster"); + + await page.goto("/clusters", { waitUntil: "domcontentloaded" }); + await clusters; + + await expect(page.getByRole("heading", { name: "Clusters" })).toBeVisible(); + await expect(stackRow(page, "shared-eu1")).toBeVisible(); + await expect(stackRow(page, "payments-us1")).toBeVisible(); + + await page.getByRole("button", { name: "Endpoint list view" }).click(); + + await expect(page.getByText("/api/v1/catalog/cluster/{id}")).toBeVisible(); + await expect(page.getByText("Get a cluster by ID")).toBeVisible(); + }); + + test("clicking a list row navigates to detail and locks the row action id", async ({ + page, + }) => { + const stacks = waitForJson(page, "/api/v1/stack"); + + await page.goto("/stacks", { waitUntil: "domcontentloaded" }); + await stacks; + + await Promise.all([ + page.waitForURL(/\/entity\/stacks\/stk-001$/), + stackRow(page, "checkout").getByRole("link", { name: "stk-001" }).click(), + ]); + + await expect(responseBody(page).first()).toContainText("stk-001"); + await expect(responseBody(page).first()).toContainText("checkout"); + await expect(page.getByRole("radiogroup", { name: /clicky view mode/i })).toBeVisible(); + await expect(page.getByRole("button", { name: /^download$/i })).toBeVisible(); + + await page.getByRole("button", { name: /restart/i }).click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog.getByLabel("Id")).toHaveValue("stk-001"); + await expect(dialog.getByLabel("Id")).toBeDisabled(); + + const actionRequest = page.waitForResponse((response) => { + const url = new URL(response.url()); + return ( + url.pathname === "/api/v1/stack/stk-001/restart" && + response.request().method() === "POST" && + response.ok() + ); + }); + + await dialog.getByLabel("Reason").fill("row-action-test"); + await dialog.getByLabel("Drain").uncheck(); + await dialog.getByRole("button", { name: "Execute request" }).click(); + await actionRequest; + + await expect(responseBody(dialog)).toContainText("row-action-test"); + }); + + test("surfaces archived admin rows and explorer endpoints from the generated spec", async ({ + page, + }) => { + const adminList = waitForJson(page, "/api/v1/admin/stack"); + + await page.goto("/admin-stacks", { waitUntil: "domcontentloaded" }); + await adminList; + + await expect(page.getByRole("heading", { name: "Admin — Stacks" })).toBeVisible(); + await expect(stackRow(page, "marketing-site")).toBeVisible(); + + await page.goto("/explorer", { waitUntil: "domcontentloaded" }); + + await expect(page.getByRole("heading", { name: "API Explorer" })).toBeVisible(); + await expect(page.getByText("/api/v1/stack/{id}/restart")).toBeVisible(); + await expect(page.getByText("/api/v1/admin/stack/{id}/reconcile")).toBeVisible(); + }); + + test("API Explorer stack endpoints can be accessed and executed", async ({ + page, + }) => { + const openApi = waitForJson(page, "/api/openapi.json"); + + await page.goto("/explorer", { waitUntil: "domcontentloaded" }); + await openApi; + + await expect(page.getByRole("heading", { name: "API Explorer" })).toBeVisible(); + + const search = explorerSearch(page); + + await search.fill("list stack resources"); + const listCard = explorerCard(page, "/api/v1/stack"); + await expect(listCard).toBeVisible(); + const listRequest = waitForJson(page, "/api/v1/stack"); + await listCard.click(); + await expect(page).toHaveURL(/\/commands\/stack_list$/); + await page.getByRole("button", { name: "Execute request" }).click(); + await listRequest; + await expect(responseBody(page)).toContainText("checkout"); + await expect(responseBody(page)).toContainText("billing"); + await expect(page.getByRole("radiogroup", { name: /clicky view mode/i })).toBeVisible(); + await expect(page.getByRole("button", { name: /^download$/i })).toBeVisible(); + + await page.goto("/explorer", { waitUntil: "domcontentloaded" }); + await search.fill("get a stack by id"); + const getCard = explorerCard(page, "/api/v1/stack/{id}"); + await expect(getCard).toBeVisible(); + const getRequest = waitForJson(page, "/api/v1/stack/stk-001"); + await getCard.click(); + await expect(page).toHaveURL(/\/commands\/stack_get$/); + await page.getByLabel("Id").fill("stk-001"); + await page.getByRole("button", { name: "Execute request" }).click(); + await getRequest; + await expect(responseBody(page)).toContainText("stk-001"); + await expect(responseBody(page)).toContainText("checkout"); + await expect(page.getByRole("radiogroup", { name: /clicky view mode/i })).toBeVisible(); + await expect(page.getByRole("button", { name: /^download$/i })).toBeVisible(); + + await page.goto("/explorer", { waitUntil: "domcontentloaded" }); + await search.fill("restart a stack"); + const actionCard = explorerCard(page, "/api/v1/stack/{id}/restart"); + await expect(actionCard).toBeVisible(); + const actionRequest = page.waitForResponse((response) => { + const url = new URL(response.url()); + return ( + url.pathname === "/api/v1/stack/stk-002/restart" && + response.request().method() === "POST" && + response.ok() + ); + }); + await actionCard.click(); + await expect(page).toHaveURL(/\/commands\/stack_restart$/); + await page.getByLabel("Id").fill("stk-002"); + await page.getByLabel("Reason").fill("explorer-test"); + await page.getByLabel("Drain").uncheck(); + await page.getByRole("button", { name: "Execute request" }).click(); + await actionRequest; + await expect(responseBody(page)).toContainText("restart"); + await expect(responseBody(page)).toContainText("explorer-test"); + }); + + test("reflects a real restart mutation after reloading the stacks table", async ({ + page, + request, + }) => { + const initialList = waitForJson(page, "/api/v1/stack"); + + await page.goto("/stacks", { waitUntil: "domcontentloaded" }); + await initialList; + + await expect(stackRow(page, "billing")).toContainText("status:degraded"); + + const response = await request.post("/api/v1/stack/stk-002/restart", { + data: { + reason: "playwright", + drain: false, + }, + }); + expect(response.ok()).toBeTruthy(); + expect(await response.json()).toMatchObject({ + action: "restart", + ids: ["stk-002"], + reason: "playwright", + }); + + const refreshedList = waitForJson(page, "/api/v1/stack"); + await page.reload({ waitUntil: "domcontentloaded" }); + await refreshedList; + + await expect(stackRow(page, "billing")).toContainText("status:healthy"); + await expect(stackRow(page, "billing")).not.toContainText("status:degraded"); + }); +}); diff --git a/examples/enitity/main.go b/examples/enitity/main.go new file mode 100644 index 00000000..ed312afe --- /dev/null +++ b/examples/enitity/main.go @@ -0,0 +1,1135 @@ +package main + +import ( + "context" + "embed" + "fmt" + "io/fs" + "net/http" + "os" + "os/signal" + "path" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/flanksource/clicky" + "github.com/flanksource/clicky/api" + "github.com/flanksource/clicky/extensions" + "github.com/flanksource/clicky/rpc" + "github.com/spf13/cobra" +) + +// webappFS carries the Vite-built single-page app. `webapp/dist/index.html` +// is committed as a placeholder so this embed always resolves; running +// `pnpm build` inside webapp/ replaces the contents with hashed bundles. +// +//go:embed webapp/dist +var webappFS embed.FS + +type stack struct { + ID string `json:"id"` + Name string `json:"name"` + Team string `json:"team"` + Status string `json:"status"` + Region string `json:"region"` + Tags []string `json:"tags,omitempty"` + Archived bool `json:"archived,omitempty"` + Version int `json:"version"` + LastDeploy time.Time `json:"lastDeploy"` +} + +func (s stack) GetID() string { return s.ID } +func (s stack) GetName() string { return s.Name } + +func (s stack) PrettyRow(_ interface{}) map[string]api.Text { + return map[string]api.Text{ + "Name": { + Content: s.Name, + Style: "font-semibold", + }, + "Team": { + Content: labelFromCanonicalTeam(s.Team), + }, + "Status": { + Content: labelFromCanonicalStatus(s.Status), + Style: statusStyle(s.Status), + }, + "Region": { + Content: s.Region, + }, + "Tags": { + Content: strings.Join(s.Tags, ", "), + Style: "text-slate-600", + }, + } +} + +type cluster struct { + ID string `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + Region string `json:"region"` +} + +func (c cluster) GetID() string { return c.ID } +func (c cluster) GetName() string { return c.Name } + +type stackWindowOpts struct { + Tags []string `flag:"tags" help:"Return only stacks containing all of these tags"` + From time.Time `flag:"from" help:"Return stacks deployed on or after this time" default:"now-30d"` + To time.Time `flag:"to" help:"Return stacks deployed on or before this time" default:"now"` +} + +type stackListOpts struct { + stackWindowOpts + Team string `flag:"team" help:"Team slug or canonical backend team id"` + Status string `flag:"status" help:"Status label or canonical backend status id"` + Region string `flag:"region" help:"Region filter"` + Filter string `flag:"filter" help:"Switch bulk actions into filter mode when set"` + IncludeArchived bool `flag:"include-archived" help:"Include archived stacks"` +} + +type clusterListOpts struct { + Provider string `flag:"provider" help:"Cloud provider filter"` + Region string `flag:"region" help:"Region filter"` +} + +type inspectFlags struct { + Events int `flag:"events" help:"Number of synthetic events to include" default:"3"` + IncludeAudit bool `flag:"include-audit" help:"Include audit metadata" default:"false"` +} + +func (inspectFlags) ClickyActionFlags() {} + +type restartFlags struct { + Drain bool `flag:"drain" help:"Drain traffic before restart" default:"true"` + Reason string `flag:"reason" help:"Reason to record for the restart" default:"manual"` +} + +func (restartFlags) ClickyActionFlags() {} + +type adminInspectFlags struct { + IncludeSecret bool `flag:"include-secret" help:"Include simulated secret material" default:"false"` +} + +func (adminInspectFlags) ClickyActionFlags() {} + +type stackSummaryOpts struct { + Team string `flag:"team" help:"Only summarize one team"` + IncludeArchived bool `flag:"include-archived" help:"Include archived stacks in the summary"` +} + +type stackDetail struct { + Stack stack `json:"stack"` + Events []string `json:"events,omitempty"` + Audit map[string]any `json:"audit,omitempty"` + Notes map[string]string `json:"notes,omitempty"` + Secret map[string]string `json:"secret,omitempty"` + Admin map[string]string `json:"admin,omitempty"` + Flags map[string]string `json:"flags,omitempty"` + Meta map[string]any `json:"meta,omitempty"` +} + +type actionResult struct { + Action string `json:"action"` + IDs []string `json:"ids,omitempty"` + Reason string `json:"reason,omitempty"` + Drain bool `json:"drain,omitempty"` + MatchedBy string `json:"matchedBy,omitempty"` + MatchedIDs []string `json:"matchedIds,omitempty"` +} + +type stackSummary struct { + Total int `json:"total"` + ByTeam map[string]int `json:"byTeam"` + ByStatus map[string]int `json:"byStatus"` +} + +type demoStore struct { + mu sync.Mutex + nextStackID int + stacks map[string]stack + clusters map[string]cluster + restartLog []string + reconcileLog []string +} + +func newDemoStore() *demoStore { + store := &demoStore{} + store.reset() + return store +} + +func (d *demoStore) reset() { + d.mu.Lock() + defer d.mu.Unlock() + + d.nextStackID = 4 + d.restartLog = nil + d.reconcileLog = nil + d.stacks = map[string]stack{ + "stk-001": { + ID: "stk-001", + Name: "checkout", + Team: "team/platform", + Status: "status:healthy", + Region: "eu-west-1", + Tags: []string{"critical", "customer"}, + Version: 12, + LastDeploy: time.Now().Add(-4 * time.Hour).UTC(), + }, + "stk-002": { + ID: "stk-002", + Name: "billing", + Team: "team/core", + Status: "status:degraded", + Region: "us-east-1", + Tags: []string{"payments", "database"}, + Version: 19, + LastDeploy: time.Now().Add(-48 * time.Hour).UTC(), + }, + "stk-003": { + ID: "stk-003", + Name: "marketing-site", + Team: "team/platform", + Status: "status:paused", + Region: "eu-west-1", + Tags: []string{"public", "edge"}, + Archived: true, + Version: 7, + LastDeploy: time.Now().Add(-14 * 24 * time.Hour).UTC(), + }, + } + d.clusters = map[string]cluster{ + "cls-001": {ID: "cls-001", Name: "shared-eu1", Provider: "aws", Region: "eu-west-1"}, + "cls-002": {ID: "cls-002", Name: "payments-us1", Provider: "aws", Region: "us-east-1"}, + "cls-003": {ID: "cls-003", Name: "labs-eu2", Provider: "gcp", Region: "europe-west2"}, + } +} + +func (d *demoStore) counts() (int, int) { + d.mu.Lock() + defer d.mu.Unlock() + return len(d.stacks), len(d.clusters) +} + +func (d *demoStore) listStacks(opts stackListOpts) ([]stack, error) { + d.mu.Lock() + defer d.mu.Unlock() + + items := d.matchingStacksLocked(opts, false) + sort.Slice(items, func(i, j int) bool { + return items[i].ID < items[j].ID + }) + return items, nil +} + +func (d *demoStore) listAdminStacks(opts stackListOpts) ([]stack, error) { + d.mu.Lock() + defer d.mu.Unlock() + + items := d.matchingStacksLocked(opts, true) + sort.Slice(items, func(i, j int) bool { + return items[i].ID < items[j].ID + }) + return items, nil +} + +func (d *demoStore) getStack(id string, flags map[string]string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.stacks[id] + if !ok { + return nil, fmt.Errorf("stack %q not found", id) + } + + detail := stackDetail{ + Stack: item, + Events: syntheticEvents( + item.Name, + intFlag(flags, "events", 3), + ), + Flags: copyStringMap(flags), + Notes: map[string]string{ + "team": labelFromCanonicalTeam(item.Team), + "status": labelFromCanonicalStatus(item.Status), + }, + } + if boolFlag(flags, "include-audit") { + detail.Audit = map[string]any{ + "lastRestart": lastEntry(d.restartLog), + "lastDeploy": item.LastDeploy, + "version": item.Version, + } + } + + return detail, nil +} + +func (d *demoStore) getAdminStack(id string, flags map[string]string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.stacks[id] + if !ok { + return nil, fmt.Errorf("stack %q not found", id) + } + + detail := stackDetail{ + Stack: item, + Admin: map[string]string{ + "canonicalTeam": item.Team, + "canonicalStatus": item.Status, + "reconcileHint": "run admin stack reconcile to refresh synthetic health", + }, + Meta: map[string]any{ + "restartLog": append([]string(nil), d.restartLog...), + "reconcileLog": append([]string(nil), d.reconcileLog...), + }, + Flags: copyStringMap(flags), + } + if boolFlag(flags, "include-secret") { + detail.Secret = map[string]string{ + "token": "demo-token", + "rotation": "disabled in the example store", + } + } + return detail, nil +} + +func (d *demoStore) createStack(body map[string]any) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + name := valueOrDefault(stringValue(body["name"]), "unnamed") + team := canonicalTeam(valueOrDefault(stringValue(body["team"]), "platform")) + status := canonicalStatus(valueOrDefault(stringValue(body["status"]), "healthy")) + region := valueOrDefault(stringValue(body["region"]), "eu-west-1") + id := fmt.Sprintf("stk-%03d", d.nextStackID) + d.nextStackID++ + + item := stack{ + ID: id, + Name: name, + Team: team, + Status: status, + Region: region, + Tags: sliceValue(body["tags"]), + Archived: boolValue(body["archived"]), + Version: 1, + LastDeploy: time.Now().UTC(), + } + d.stacks[id] = item + return item, nil +} + +func (d *demoStore) updateStack(id string, body map[string]any) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.stacks[id] + if !ok { + return nil, fmt.Errorf("stack %q not found", id) + } + + if value := stringValue(body["name"]); value != "" { + item.Name = value + } + if value := stringValue(body["team"]); value != "" { + item.Team = canonicalTeam(value) + } + if value := stringValue(body["status"]); value != "" { + item.Status = canonicalStatus(value) + } + if value := stringValue(body["region"]); value != "" { + item.Region = value + } + if raw, ok := body["tags"]; ok { + item.Tags = sliceValue(raw) + } + if raw, ok := body["archived"]; ok { + item.Archived = boolValue(raw) + } + item.Version++ + item.LastDeploy = time.Now().UTC() + d.stacks[id] = item + return item, nil +} + +func (d *demoStore) deleteStack(id string) error { + d.mu.Lock() + defer d.mu.Unlock() + + if _, ok := d.stacks[id]; !ok { + return fmt.Errorf("stack %q not found", id) + } + delete(d.stacks, id) + return nil +} + +func (d *demoStore) restartStack(id string, flags map[string]string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.stacks[id] + if !ok { + return nil, fmt.Errorf("stack %q not found", id) + } + item.Status = "status:healthy" + item.Version++ + item.LastDeploy = time.Now().UTC() + d.stacks[id] = item + + reason := valueOrDefault(strings.TrimSpace(flags["reason"]), "manual") + drain := boolFlagDefault(flags, "drain", true) + entry := fmt.Sprintf("%s restarted with reason=%s drain=%t", id, reason, drain) + d.restartLog = append(d.restartLog, entry) + + return actionResult{ + Action: "restart", + IDs: []string{id}, + Reason: reason, + Drain: drain, + }, nil +} + +func (d *demoStore) reconcileStack(id string, flags map[string]string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.stacks[id] + if !ok { + return nil, fmt.Errorf("stack %q not found", id) + } + item.Status = "status:healthy" + d.stacks[id] = item + + entry := fmt.Sprintf("%s reconciled", id) + d.reconcileLog = append(d.reconcileLog, entry) + + return actionResult{ + Action: "reconcile", + IDs: []string{id}, + }, nil +} + +func (d *demoStore) pauseStacks(ids []string, _ map[string]string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + updated := make([]string, 0, len(ids)) + for _, id := range ids { + item, ok := d.stacks[id] + if !ok { + return nil, fmt.Errorf("stack %q not found", id) + } + item.Status = "status:paused" + item.Version++ + d.stacks[id] = item + updated = append(updated, id) + } + sort.Strings(updated) + + return actionResult{ + Action: "pause", + IDs: updated, + }, nil +} + +func (d *demoStore) pauseStacksByFilter(opts stackListOpts, _ map[string]string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + matched := d.matchingStacksLocked(opts, true) + ids := make([]string, 0, len(matched)) + for _, item := range matched { + current := d.stacks[item.ID] + current.Status = "status:paused" + current.Version++ + d.stacks[item.ID] = current + ids = append(ids, item.ID) + } + sort.Strings(ids) + + return actionResult{ + Action: "pause", + MatchedBy: opts.Filter, + MatchedIDs: ids, + }, nil +} + +func (d *demoStore) summarizeStacks(opts stackSummaryOpts) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + summary := stackSummary{ + ByTeam: map[string]int{}, + ByStatus: map[string]int{}, + } + team := canonicalTeam(opts.Team) + for _, item := range d.stacks { + if !opts.IncludeArchived && item.Archived { + continue + } + if opts.Team != "" && item.Team != team { + continue + } + summary.Total++ + summary.ByTeam[labelFromCanonicalTeam(item.Team)]++ + summary.ByStatus[labelFromCanonicalStatus(item.Status)]++ + } + return summary, nil +} + +func (d *demoStore) listClusters(opts clusterListOpts) ([]cluster, error) { + d.mu.Lock() + defer d.mu.Unlock() + + items := make([]cluster, 0, len(d.clusters)) + for _, item := range d.clusters { + if opts.Provider != "" && !strings.EqualFold(item.Provider, opts.Provider) { + continue + } + if opts.Region != "" && !strings.EqualFold(item.Region, opts.Region) { + continue + } + items = append(items, item) + } + sort.Slice(items, func(i, j int) bool { + return items[i].ID < items[j].ID + }) + return items, nil +} + +func (d *demoStore) getCluster(id string) (any, error) { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.clusters[id] + if !ok { + return nil, fmt.Errorf("cluster %q not found", id) + } + return item, nil +} + +func (d *demoStore) completeStackIDs(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + d.mu.Lock() + defer d.mu.Unlock() + + ids := make([]string, 0, len(d.stacks)) + for id := range d.stacks { + if strings.HasPrefix(id, toComplete) { + ids = append(ids, id) + } + } + sort.Strings(ids) + return ids, cobra.ShellCompDirectiveNoFileComp +} + +func (d *demoStore) matchingStacksLocked(opts stackListOpts, includeArchived bool) []stack { + items := make([]stack, 0, len(d.stacks)) + for _, item := range d.stacks { + if item.Archived && !includeArchived && !opts.IncludeArchived { + continue + } + if opts.Team != "" && item.Team != opts.Team { + continue + } + if opts.Status != "" && item.Status != opts.Status { + continue + } + if opts.Region != "" && !strings.EqualFold(item.Region, opts.Region) { + continue + } + if len(opts.Tags) > 0 && !containsAll(item.Tags, opts.Tags) { + continue + } + if !opts.From.IsZero() && item.LastDeploy.Before(opts.From) { + continue + } + if !opts.To.IsZero() && item.LastDeploy.After(opts.To) { + continue + } + items = append(items, item) + } + return items +} + +type stackTeamFilter struct{} + +func (stackTeamFilter) Key() string { return "team" } +func (stackTeamFilter) Label() string { return "Team" } + +func (stackTeamFilter) Lookup(opts *stackListOpts) (map[string]api.Textable, error) { + if opts.Team == "" { + return nil, nil + } + opts.Team = canonicalTeam(opts.Team) + label := labelFromCanonicalTeam(opts.Team) + return map[string]api.Textable{ + opts.Team: api.Text{ + Content: label, + Style: "font-semibold", + }, + }, nil +} + +func (stackTeamFilter) Options(opts stackListOpts) map[string]api.Textable { + options := map[string]api.Textable{ + "team/platform": api.Text{Content: "Platform"}, + "team/core": api.Text{Content: "Core"}, + "team/data": api.Text{Content: "Data"}, + } + if strings.EqualFold(opts.Region, "us-east-1") { + return map[string]api.Textable{ + "team/core": options["team/core"], + } + } + return options +} + +type stackStatusFilter struct{} + +func (stackStatusFilter) Key() string { return "status" } +func (stackStatusFilter) Label() string { return "Status" } + +func (stackStatusFilter) Lookup(opts *stackListOpts) (map[string]api.Textable, error) { + if opts.Status == "" { + return nil, nil + } + opts.Status = canonicalStatus(opts.Status) + label := labelFromCanonicalStatus(opts.Status) + return map[string]api.Textable{ + opts.Status: api.Text{ + Content: label, + Style: statusStyle(opts.Status), + Tooltip: api.Text{Content: "Canonical backend value: " + opts.Status}, + }, + }, nil +} + +func (stackStatusFilter) Options(opts stackListOpts) map[string]api.Textable { + options := map[string]api.Textable{ + "status:healthy": api.Text{Content: "Healthy", Style: statusStyle("status:healthy")}, + "status:degraded": api.Text{Content: "Degraded", Style: statusStyle("status:degraded")}, + "status:paused": api.Text{Content: "Paused", Style: statusStyle("status:paused")}, + } + if opts.Team == "team/platform" { + return map[string]api.Textable{ + "status:healthy": options["status:healthy"], + "status:paused": options["status:paused"], + } + } + return options +} + +type stackFromFilter struct{} + +func (stackFromFilter) Key() string { return "from" } +func (stackFromFilter) Label() string { return "From" } + +func (stackFromFilter) Lookup(_ *stackListOpts) (map[string]api.Textable, error) { + return nil, nil +} + +func (stackFromFilter) Options(_ stackListOpts) map[string]api.Textable { + return nil +} + +type stackToFilter struct{} + +func (stackToFilter) Key() string { return "to" } +func (stackToFilter) Label() string { return "To" } + +func (stackToFilter) Lookup(_ *stackListOpts) (map[string]api.Textable, error) { + return nil, nil +} + +func (stackToFilter) Options(_ stackListOpts) map[string]api.Textable { + return nil +} + +func registerEntities(store *demoStore) { + stackFilters := []clicky.Filter[stackListOpts]{ + stackTeamFilter{}, + stackStatusFilter{}, + stackFromFilter{}, + stackToFilter{}, + } + + clicky.RegisterEntity(clicky.Entity[stack, stackListOpts]{ + Name: "stack", + Aliases: []string{"stacks", "svc"}, + Filters: stackFilters, + List: store.listStacks, + GetFlags: inspectFlags{}, + GetWithFlags: func(id string, flags map[string]string) (any, error) { + return store.getStack(id, flags) + }, + Create: store.createStack, + Update: store.updateStack, + Delete: store.deleteStack, + Actions: []clicky.Action[stack]{ + { + Name: "restart", + Short: "Restart a stack and record synthetic audit metadata", + Flags: restartFlags{}, + Run: func(id string, flags map[string]string) (any, error) { + return store.restartStack(id, flags) + }, + }, + }, + BulkActions: []clicky.BulkAction[stack, stackListOpts]{ + { + Name: "pause", + Short: "Pause stacks directly by id or indirectly through filter mode", + Run: func(ids []string, flags map[string]string) (any, error) { + return store.pauseStacks(ids, flags) + }, + RunFilter: func(opts stackListOpts, flags map[string]string) (any, error) { + return store.pauseStacksByFilter(opts, flags) + }, + }, + }, + Admin: &clicky.Entity[stack, stackListOpts]{ + Filters: stackFilters, + List: store.listAdminStacks, + GetFlags: adminInspectFlags{}, + GetWithFlags: func(id string, flags map[string]string) (any, error) { + return store.getAdminStack(id, flags) + }, + Actions: []clicky.Action[stack]{ + { + Name: "reconcile", + Short: "Force a synthetic admin reconcile for a stack", + Run: func(id string, flags map[string]string) (any, error) { + return store.reconcileStack(id, flags) + }, + }, + }, + }, + ValidArgs: store.completeStackIDs, + }) + + clicky.RegisterEntity(clicky.Entity[cluster, clusterListOpts]{ + Parent: "catalog", + List: store.listClusters, + Get: store.getCluster, + }) +} + +func registerSubCommands(store *demoStore) { + clicky.RegisterSubCommand("stack", &cobra.Command{ + Use: "seed", + Short: "Reset the in-memory demo data set", + RunE: func(cmd *cobra.Command, args []string) error { + store.reset() + stacks, clusters := store.counts() + fmt.Fprintf(cmd.OutOrStdout(), "reset demo store with %d stacks and %d clusters\n", stacks, clusters) + return nil + }, + }) + + clicky.RegisterSubCommandFn("stack", func(parent *cobra.Command) { + clicky.AddNamedCommand("summary", parent, stackSummaryOpts{}, store.summarizeStacks) + }) +} + +// newServeUICommand wires the clicky RPC executor together with the Vite +// webapp embedded above. The webapp consumes `@flanksource/clicky-ui`'s +// `OperationCatalog` against `/api/openapi.json` and `/api/v1/...`. +func newServeUICommand() *cobra.Command { + var ( + host string + port int + ) + + cmd := &cobra.Command{ + Use: "serve-ui", + Short: "Start the HTTP API and embedded operation-catalog UI", + Long: `Start an HTTP server that exposes both the executor-backed OpenAPI endpoints +and the embedded React UI built from clicky-ui's OperationCatalog component. + +The API is served at /api/openapi.json + /api/v1/..., the UI at /. Build the +Vite frontend with ` + "`cd webapp && pnpm install && pnpm build`" + ` before +compiling the Go binary so the embedded assets are current.`, + Example: ` entity-demo serve-ui --port 8080 + entity-demo serve-ui --host 0.0.0.0 --port 9090`, + RunE: func(cmd *cobra.Command, args []string) error { + if port < 1 || port > 65535 { + return fmt.Errorf("invalid port: %d", port) + } + + rootCmd := cmd.Root() + openAPIConfig := &rpc.OpenAPIConfig{ + Title: "Clicky Entity Example", + Description: "Entity example app with embedded OperationCatalog UI.", + Version: "1.0.0", + } + serveConfig := &rpc.ServeConfig{ + Host: host, + Port: port, + Title: openAPIConfig.Title, + Version: openAPIConfig.Version, + Executor: &rpc.ExecutorConfig{ + Enabled: true, + SkipPreRun: true, + PathPrefix: "/api/v1", + }, + } + + server := rpc.NewSwaggerServer(serveConfig, rootCmd, openAPIConfig) + + mux := http.NewServeMux() + server.RegisterRoutes(mux) + + uiHandler, err := newWebappHandler() + if err != nil { + return fmt.Errorf("load embedded webapp: %w", err) + } + mux.Handle("/", uiHandler) + + addr := fmt.Sprintf("%s:%d", host, port) + httpSrv := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + + errCh := make(chan error, 1) + go func() { + fmt.Fprintf(cmd.OutOrStdout(), "🚀 Entity UI listening on http://%s\n", addr) + fmt.Fprintf(cmd.OutOrStdout(), " • UI: http://%s/\n", addr) + fmt.Fprintf(cmd.OutOrStdout(), " • OpenAPI JSON: http://%s/api/openapi.json\n", addr) + fmt.Fprintf(cmd.OutOrStdout(), " • Executor API: http://%s/api/v1/...\n", addr) + if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() + + select { + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return httpSrv.Shutdown(shutdownCtx) + case err := <-errCh: + return err + } + }, + } + + cmd.Flags().StringVar(&host, "host", "localhost", "Host to bind the server to") + cmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to bind the server to") + + return cmd +} + +// newWebappHandler returns an http.Handler that serves the embedded Vite +// build. Unknown paths fall back to index.html so the React router can +// handle client-side routes on a full page load. +func newWebappHandler() (http.Handler, error) { + sub, err := fs.Sub(webappFS, "webapp/dist") + if err != nil { + return nil, err + } + fileServer := http.FileServer(http.FS(sub)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requested := strings.TrimPrefix(r.URL.Path, "/") + if requested == "" { + serveIndex(w, sub) + return + } + if _, err := fs.Stat(sub, requested); err != nil { + // File not found: assume a SPA route and return index.html. + if !looksLikeAssetRequest(requested) { + serveIndex(w, sub) + return + } + http.NotFound(w, r) + return + } + fileServer.ServeHTTP(w, r) + }), nil +} + +func serveIndex(w http.ResponseWriter, sub fs.FS) { + data, err := fs.ReadFile(sub, "index.html") + if err != nil { + http.Error(w, "webapp index.html missing — run `pnpm build` in webapp/", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + _, _ = w.Write(data) +} + +// looksLikeAssetRequest returns true when the request targets a file with a +// known extension, so we don't swallow a genuine 404 (e.g. a missing image +// reference) with the SPA fallback. +func looksLikeAssetRequest(requested string) bool { + ext := strings.ToLower(path.Ext(requested)) + switch ext { + case ".js", ".mjs", ".css", ".map", ".ico", ".png", ".jpg", ".jpeg", + ".gif", ".svg", ".webp", ".woff", ".woff2", ".ttf", ".eot", ".txt": + return true + } + return false +} + +func main() { + store := newDemoStore() + + rootCmd := &cobra.Command{ + Use: "entity-demo", + Short: "Entity example covering clicky entity generation and served execution", + Long: `A self-contained example showing how clicky entities can power both a CLI +and the executor-backed OpenAPI serve mode from the same registrations.`, + } + + registerEntities(store) + registerSubCommands(store) + clicky.GenerateCLI(rootCmd) + + extensions.CobraExtensions(rootCmd).OpenAPICommandWithConfig(&rpc.OpenAPIConfig{ + Title: "Clicky Entity Example", + Description: "Entity example app covering CRUD, actions, filters, admin views, nested parents, and executor-backed serve mode.", + Version: "1.0.0", + Tags: []rpc.OpenAPITag{ + {Name: "stack", Description: "Stack entity operations"}, + {Name: "catalog", Description: "Nested catalog entity operations"}, + {Name: "admin", Description: "Administrative entity operations"}, + }, + }) + + rootCmd.AddCommand(newServeUICommand()) + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func containsAll(have, want []string) bool { + if len(want) == 0 { + return true + } + set := make(map[string]struct{}, len(have)) + for _, value := range have { + set[strings.ToLower(strings.TrimSpace(value))] = struct{}{} + } + for _, value := range want { + if _, ok := set[strings.ToLower(strings.TrimSpace(value))]; !ok { + return false + } + } + return true +} + +func canonicalTeam(value string) string { + value = strings.TrimSpace(strings.ToLower(value)) + switch value { + case "", "team/platform": + return "team/platform" + case "platform": + return "team/platform" + case "team/core": + return "team/core" + case "core": + return "team/core" + case "team/data": + return "team/data" + case "data": + return "team/data" + default: + if strings.HasPrefix(value, "team/") { + return value + } + return "team/" + value + } +} + +func labelFromCanonicalTeam(value string) string { + return titleCase(strings.TrimPrefix(value, "team/")) +} + +func canonicalStatus(value string) string { + value = strings.TrimSpace(strings.ToLower(value)) + switch value { + case "", "status:healthy": + return "status:healthy" + case "healthy": + return "status:healthy" + case "status:degraded": + return "status:degraded" + case "degraded": + return "status:degraded" + case "status:paused": + return "status:paused" + case "paused": + return "status:paused" + default: + if strings.HasPrefix(value, "status:") { + return value + } + return "status:" + value + } +} + +func labelFromCanonicalStatus(value string) string { + return titleCase(strings.TrimPrefix(value, "status:")) +} + +func statusStyle(value string) string { + switch value { + case "status:healthy": + return "text-green-600 font-semibold" + case "status:degraded": + return "text-amber-600 font-semibold" + case "status:paused": + return "text-slate-500" + default: + return "text-slate-700" + } +} + +func syntheticEvents(name string, count int) []string { + if count <= 0 { + return nil + } + events := make([]string, 0, count) + for i := 0; i < count; i++ { + events = append(events, fmt.Sprintf("%s event %d", name, i+1)) + } + return events +} + +func lastEntry(values []string) string { + if len(values) == 0 { + return "" + } + return values[len(values)-1] +} + +func copyStringMap(values map[string]string) map[string]string { + if len(values) == 0 { + return nil + } + out := make(map[string]string, len(values)) + for key, value := range values { + out[key] = value + } + return out +} + +func intFlag(flags map[string]string, key string, fallback int) int { + if value, err := strconv.Atoi(flags[key]); err == nil { + return value + } + return fallback +} + +func boolFlag(flags map[string]string, key string) bool { + value, err := strconv.ParseBool(flags[key]) + return err == nil && value +} + +func boolFlagDefault(flags map[string]string, key string, fallback bool) bool { + value, err := strconv.ParseBool(flags[key]) + if err != nil { + return fallback + } + return value +} + +func stringValue(value any) string { + switch typed := value.(type) { + case string: + return strings.TrimSpace(typed) + case fmt.Stringer: + return strings.TrimSpace(typed.String()) + case nil: + return "" + default: + return strings.TrimSpace(fmt.Sprintf("%v", typed)) + } +} + +func sliceValue(value any) []string { + switch typed := value.(type) { + case []string: + return append([]string(nil), typed...) + case []any: + items := make([]string, 0, len(typed)) + for _, item := range typed { + if value := stringValue(item); value != "" { + items = append(items, value) + } + } + return items + case string: + if typed == "" { + return nil + } + parts := strings.Split(typed, ",") + items := make([]string, 0, len(parts)) + for _, part := range parts { + if value := strings.TrimSpace(part); value != "" { + items = append(items, value) + } + } + return items + default: + value := stringValue(value) + if value == "" { + return nil + } + return []string{value} + } +} + +func boolValue(value any) bool { + switch typed := value.(type) { + case bool: + return typed + case string: + parsed, err := strconv.ParseBool(typed) + return err == nil && parsed + default: + parsed, err := strconv.ParseBool(fmt.Sprintf("%v", typed)) + return err == nil && parsed + } +} + +func valueOrDefault(value, fallback string) string { + if value == "" { + return fallback + } + return value +} + +func titleCase(value string) string { + if value == "" { + return "" + } + parts := strings.FieldsFunc(value, func(r rune) bool { + return r == '-' || r == '_' || r == ' ' + }) + for i := range parts { + if parts[i] == "" { + continue + } + parts[i] = strings.ToUpper(parts[i][:1]) + strings.ToLower(parts[i][1:]) + } + return strings.Join(parts, " ") +} diff --git a/examples/enitity/main_test.go b/examples/enitity/main_test.go new file mode 100644 index 00000000..cc21ee9e --- /dev/null +++ b/examples/enitity/main_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "testing" + "time" +) + +func TestMatchingStacksLockedHonorsFromAndTo(t *testing.T) { + store := newDemoStore() + + items := store.matchingStacksLocked(stackListOpts{ + stackWindowOpts: stackWindowOpts{ + From: time.Now().Add(-24 * time.Hour).UTC(), + To: time.Now().UTC(), + }, + }, false) + + if len(items) != 1 { + t.Fatalf("expected exactly one stack in the last 24h window, got %d", len(items)) + } + + if items[0].ID != "stk-001" { + t.Fatalf("expected stk-001 in the last 24h window, got %s", items[0].ID) + } +} diff --git a/examples/enitity/webapp/.gitignore b/examples/enitity/webapp/.gitignore new file mode 100644 index 00000000..c5e4a1b2 --- /dev/null +++ b/examples/enitity/webapp/.gitignore @@ -0,0 +1,13 @@ +node_modules +# Keep dist/index.html committed as a placeholder so go:embed always has +# something to embed. `vite build` regenerates the directory with real +# hashed assets which we ignore here. +dist/assets +dist/*.js +dist/*.css +dist/*.svg +dist/*.ico +dist/*.png +dist/.vite +*.log +.vite diff --git a/examples/enitity/webapp/src/App.tsx b/examples/enitity/webapp/src/App.tsx new file mode 100644 index 00000000..331e40bd --- /dev/null +++ b/examples/enitity/webapp/src/App.tsx @@ -0,0 +1,126 @@ +import { Link, Navigate, Route, Routes, useParams } from "react-router-dom"; +import { + OperationCatalog, + OperationCommandPage, + OperationEntityPage, + ThemeSwitcher, + type RenderLink, +} from "@flanksource/clicky-ui"; +import { apiClient } from "./api"; +import { domainOrder, domains } from "./domains"; + +const renderLink: RenderLink = ({ to, className, children, title, key }) => ( + + {children} + +); + +function DomainPage() { + const { domainKey } = useParams<{ domainKey: string }>(); + const spec = domainKey ? domains[domainKey] : undefined; + if (!spec) { + return ( +
+ Unknown domain: {domainKey} +
+ ); + } + return ( + + ); +} + +function CommandRoute() { + const { operationId } = useParams<{ operationId: string }>(); + return ( + + ); +} + +function EntityRoute() { + const { domainKey, id } = useParams<{ domainKey: string; id: string }>(); + const spec = domainKey ? domains[domainKey] : undefined; + + if (!spec) { + return ( +
+ Unknown domain: {domainKey} +
+ ); + } + + return ( + + ); +} + +function Sidebar() { + return ( + + ); +} + +export function App() { + return ( +
+ +
+ + } /> + } /> + } /> + } /> + +
+
+ ); +} diff --git a/examples/enitity/webapp/src/api.ts b/examples/enitity/webapp/src/api.ts new file mode 100644 index 00000000..7c66ad68 --- /dev/null +++ b/examples/enitity/webapp/src/api.ts @@ -0,0 +1,126 @@ +import type { + ExecutionResponse, + OpenAPISpec, + OperationLookupResponse, + OperationsApiClient, +} from "@flanksource/clicky-ui"; + +// substitutePath replaces `{name}` segments in an OpenAPI path with the +// corresponding values from `params`, returning the resolved path and the +// remaining params (those not consumed by a path segment). +function substitutePath( + path: string, + params: Record, +): { resolved: string; remaining: Record } { + const remaining = { ...params }; + let resolved = path; + for (const [key, value] of Object.entries(params)) { + if (resolved.includes(`{${key}}`)) { + resolved = resolved.replace(`{${key}}`, encodeURIComponent(value)); + delete remaining[key]; + delete remaining.args; + } + } + return { resolved, remaining }; +} + +async function request( + path: string, + method: string, + body?: unknown, + headers?: Record, + requestUrl?: string, +): Promise { + const upper = method.toUpperCase(); + const init: RequestInit = { + method: upper, + headers: { Accept: "application/json", ...(headers ?? {}) }, + }; + if (upper !== "GET" && body !== undefined) { + init.headers = { "Content-Type": "application/json", ...init.headers }; + init.body = JSON.stringify(body); + } + const response = await fetch(path, init); + const text = await response.text(); + const contentType = response.headers.get("Content-Type") || undefined; + if (!response.ok) { + throw new Error( + `${upper} ${path} failed with ${response.status}: ${text || response.statusText}`, + ); + } + return { + success: true, + exit_code: 0, + stdout: text, + contentType, + requestUrl, + parsed: maybeParseJson(text, contentType), + }; +} + +function maybeParseJson(text: string, contentType?: string) { + const trimmed = text.trim(); + if (!trimmed) { + return undefined; + } + + const shouldParse = + contentType?.toLowerCase().includes("json") || + trimmed.startsWith("{") || + trimmed.startsWith("["); + if (!shouldParse) { + return undefined; + } + + try { + return JSON.parse(trimmed) as unknown; + } catch { + return undefined; + } +} + +export const apiClient: OperationsApiClient = { + async getOpenAPISpec(): Promise { + const response = await fetch("/api/openapi.json", { + headers: { Accept: "application/json" }, + }); + if (!response.ok) { + throw new Error( + `GET /api/openapi.json failed with ${response.status}: ${response.statusText}`, + ); + } + return (await response.json()) as OpenAPISpec; + }, + + async executeCommand(path, method, params, headers) { + const { resolved, remaining } = substitutePath(path, params); + if (method.toUpperCase() === "GET") { + const search = new URLSearchParams(remaining).toString(); + const url = search ? `${resolved}?${search}` : resolved; + return request(url, method, undefined, headers, url); + } + return request(resolved, method, remaining, headers); + }, + + async lookupFilters( + path: string, + _method: string, + params: Record, + headers?: Record, + ): Promise { + const { resolved, remaining } = substitutePath(path, params); + const searchParams = new URLSearchParams(remaining); + searchParams.set("__lookup", "filters"); + const url = `${resolved}?${searchParams.toString()}`; + const response = await fetch(url, { + method: "GET", + headers: { Accept: "application/json+clicky", ...(headers ?? {}) }, + }); + if (!response.ok) { + throw new Error( + `GET ${url} failed with ${response.status}: ${await response.text() || response.statusText}`, + ); + } + return (await response.json()) as OperationLookupResponse; + }, +}; diff --git a/examples/enitity/webapp/src/domains.ts b/examples/enitity/webapp/src/domains.ts new file mode 100644 index 00000000..148e06f0 --- /dev/null +++ b/examples/enitity/webapp/src/domains.ts @@ -0,0 +1,64 @@ +import type { DomainDefinition } from "@flanksource/clicky-ui"; + +export type DomainSpec = { + definition: DomainDefinition; + entities: string[]; + // When true, the OperationCatalog displays all operations regardless of + // entity tags. Use for an API Explorer-style view. + allOperations?: boolean; + operationIdPrefix?: string; + listOperationId?: string; + detailOperationId?: string; +}; + +export const domains: Record = { + stacks: { + definition: { + key: "stacks", + title: "Stacks", + description: + "Deployable stacks (checkout, billing, …). Lists, filters, and runs entity actions registered with clicky.", + }, + entities: ["stack"], + }, + clusters: { + definition: { + key: "clusters", + title: "Clusters", + description: + "Cloud clusters backing the demo stacks. Nested under the `catalog` parent in the entity registration.", + }, + entities: ["cluster"], + operationIdPrefix: "catalog_", + listOperationId: "catalog_cluster_list", + detailOperationId: "catalog_cluster_get", + }, + "admin-stacks": { + definition: { + key: "admin-stacks", + title: "Admin — Stacks", + description: + "Administrative view of the stack entity. Surfaces archived rows and admin-only fields (secret material, reconcile metadata).", + }, + entities: ["stack"], + operationIdPrefix: "admin_", + listOperationId: "admin_stack_list", + detailOperationId: "admin_stack_get", + }, + explorer: { + definition: { + key: "explorer", + title: "API Explorer", + description: "Every operation exposed by the entity demo's OpenAPI spec.", + }, + entities: [], + allOperations: true, + }, +}; + +export const domainOrder: Array = [ + "stacks", + "clusters", + "admin-stacks", + "explorer", +]; diff --git a/examples/enitity/webapp/src/index.css b/examples/enitity/webapp/src/index.css new file mode 100644 index 00000000..1492e0b4 --- /dev/null +++ b/examples/enitity/webapp/src/index.css @@ -0,0 +1,18 @@ +@import "tailwindcss"; +@import "@flanksource/clicky-ui/styles.css"; +@source "../node_modules/@flanksource/clicky-ui/dist"; + +html, +body, +#root { + height: 100%; + margin: 0; + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + sans-serif; +} diff --git a/examples/enitity/webapp/src/main.tsx b/examples/enitity/webapp/src/main.tsx new file mode 100644 index 00000000..d8ca8f9c --- /dev/null +++ b/examples/enitity/webapp/src/main.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter } from "react-router-dom"; +import { ThemeProvider, DensityProvider } from "@flanksource/clicky-ui"; +// Register the web component so clicky-ui's glyphs +// render (it loads icons on-demand from the Iconify CDN at runtime). +import "iconify-icon"; +import "@flanksource/clicky-ui/styles.css"; +import "./index.css"; +import { App } from "./App"; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 0, staleTime: 30_000 } }, +}); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + + + + , +); diff --git a/examples/enitity/webapp/tsconfig.tsbuildinfo b/examples/enitity/webapp/tsconfig.tsbuildinfo new file mode 100644 index 00000000..d446189f --- /dev/null +++ b/examples/enitity/webapp/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/api.ts","./src/domains.ts","./src/main.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/examples/enitity/webapp/vite.config.ts b/examples/enitity/webapp/vite.config.ts new file mode 100644 index 00000000..30550573 --- /dev/null +++ b/examples/enitity/webapp/vite.config.ts @@ -0,0 +1,31 @@ +import path from "path"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// The Go server injects CLICKY_EXAMPLE_API_URL when launched via +// `entity-demo serve-ui --dev`, so Vite's proxy targets the same process. +const apiTarget = process.env.CLICKY_EXAMPLE_API_URL || "http://localhost:8080"; + +export default defineConfig({ + // The example app is embedded and served from "/", so root-relative assets + // keep deep links like /entity/:domainKey/:id working on full page loads. + base: "/", + plugins: [tailwindcss(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + dedupe: ["react", "react-dom"], + }, + server: { + proxy: { + "/api": apiTarget, + "/health": apiTarget, + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); diff --git a/examples/go.mod b/examples/go.mod index 67231b6b..a619b604 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,146 +1,167 @@ module examples -go 1.25.1 +go 1.26.1 require ( - github.com/flanksource/clicky v0.0.0-20250813170955-5d09bb0e45d6 - github.com/flanksource/commons v1.42.3 + github.com/flanksource/clicky v1.21.1 + github.com/flanksource/commons v1.50.3 github.com/labstack/echo/v4 v4.13.4 - github.com/spf13/cobra v1.9.2-0.20250831231508-51d675196729 - github.com/spf13/pflag v1.0.9 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect - github.com/alecthomas/chroma/v2 v2.20.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/alecthomas/chroma/v2 v2.23.1 // indirect + github.com/antchfx/xmlquery v1.5.1 // indirect + github.com/antchfx/xpath v1.3.6 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cert-manager/cert-manager v1.16.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/cert-manager/cert-manager v1.19.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/huh v1.0.0 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect github.com/fatih/color v1.18.0 // indirect - github.com/flanksource/gomplate/v3 v3.24.60 // indirect - github.com/flanksource/is-healthy v1.0.79 // indirect + github.com/flanksource/gomplate/v3 v3.24.77 // indirect + github.com/flanksource/is-healthy v1.0.86 // indirect github.com/flanksource/kubectl-neat v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - github.com/google/cel-go v0.26.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/cel-go v0.27.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gosimple/slug v1.13.1 // indirect + github.com/gosimple/slug v1.15.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf // indirect github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce // indirect github.com/henvic/httpretty v0.1.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/itchyny/gojq v0.12.17 // indirect - github.com/itchyny/timefmt-go v0.1.6 // indirect - github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603 // indirect + github.com/itchyny/gojq v0.12.18 // indirect + github.com/itchyny/timefmt-go v0.1.7 // indirect + github.com/jeremywohl/flatten v1.0.1 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect - github.com/lmittmann/tint v1.1.2 // indirect + github.com/lmittmann/tint v1.1.3 // indirect github.com/lrita/cmap v0.0.0-20231108122212-cb084a67f554 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/ohler55/ojg v1.25.0 // indirect + github.com/ohler55/ojg v1.28.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.0.9 // indirect - github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.7 // indirect + github.com/olekukonko/tablewriter v1.1.3 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/playwright-community/playwright-go v0.4702.0 // indirect + github.com/playwright-community/playwright-go v0.5700.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.17.0 // indirect - github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/robertkrimen/otto v0.3.0 // indirect + github.com/robertkrimen/otto v0.5.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/samber/lo v1.52.0 // indirect - github.com/samber/oops v1.19.3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/samber/lo v1.53.0 // indirect + github.com/samber/oops v1.21.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/tiendc/go-deepcopy v1.6.0 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/timberio/go-datemath v0.1.0 // indirect github.com/tj/go-naturaldate v1.3.0 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xuri/efp v0.0.1 // indirect - github.com/xuri/excelize/v2 v2.9.1 // indirect - github.com/xuri/nfp v0.0.1 // indirect + github.com/xuri/excelize/v2 v2.10.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/protobuf v1.36.10 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.1 // indirect - k8s.io/apiextensions-apiserver v0.31.1 // indirect - k8s.io/apimachinery v0.34.1 // indirect - k8s.io/client-go v0.34.1 // indirect + k8s.io/api v0.35.2 // indirect + k8s.io/apiextensions-apiserver v0.35.2 // indirect + k8s.io/apimachinery v0.35.2 // indirect + k8s.io/client-go v0.35.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf // indirect - sigs.k8s.io/gateway-api v1.1.0 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/gateway-api v1.5.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 8e6e54c9..91461aee 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,50 +1,131 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/Snawoot/go-http-digest-auth-client v1.1.3 h1:Xd/SNBuIUJqotzmxRpbXovBJxmlVZOT19IZZdMdrJ0Q= github.com/Snawoot/go-http-digest-auth-client v1.1.3/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg= +github.com/Venafi/vcert/v5 v5.12.2/go.mod h1:x3l0pB0q0E6wuhPe7nzfkUEwwraK7amnBWQ4LtT1bbw= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.0.0/go.mod h1:Bf6hnZkloZnfL4I/gFGnMMMdMHiu/ERnSOWtFgnodDk= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78= +github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8= +github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= +github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.58.4/go.mod h1:xNLZLn4SusktBQ5moqUOgiDKGz3a7vHwF4W0KD+WBPc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cert-manager/cert-manager v1.16.1 h1:1ceFMqTtwiqY2vyfaRT85CNiVmK7pJjt3GebYCx9awY= github.com/cert-manager/cert-manager v1.16.1/go.mod h1:MfLVTL45hFZsqmaT1O0+b2ugaNNQQZttSFV9hASHUb0= +github.com/cert-manager/cert-manager v1.19.4 h1:7lOkSYj+nJNjgGFfAznQzPpOfWX+1Kgz6xUXwTa/K5k= +github.com/cert-manager/cert-manager v1.19.4/go.mod h1:9uBnn3IK9NxjjuXmQDYhwOwFUU5BtGVB1g/voPvvcVw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -55,36 +136,68 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/digitalocean/godo v1.165.1/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 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/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flanksource/commons v1.42.3 h1:53tr1A8fFywYD/56kZgYNAxUEqOdvbXAiQr+3/M2Zso= github.com/flanksource/commons v1.42.3/go.mod h1:xEBCobwaM0+EHn2SvFpqiCBJCVk1803An1kZleXFR9Y= +github.com/flanksource/commons v1.47.2 h1:3CE3MHBWM/1rehHDduUR8r55rP8HorbYuiB2LO8Rjmw= +github.com/flanksource/commons v1.47.2/go.mod h1:ouTo/VFVZ/7LKEDWgHLQKdCJsTreDrUOmV0Z3CxZosY= +github.com/flanksource/commons v1.50.3 h1:uWsI9dCxkCrpnCplFlQ+wy3jbOedOvsGcqoogrgmrsQ= +github.com/flanksource/commons v1.50.3/go.mod h1:IAZ94jQnUMMRShmbkg4+sPHEdyQfVc7a1tl2BnL1VF8= github.com/flanksource/gomplate/v3 v3.24.60 h1:g1SjmR3m5YlXpuxyegvEj6OVbkoqP3btZbqUH0f7920= github.com/flanksource/gomplate/v3 v3.24.60/go.mod h1:hGEObNtnOQs8rNUX8sM8aJTAhnt4ehjyOw1MvDhl6AU= +github.com/flanksource/gomplate/v3 v3.24.74 h1:LR1TGUxbQiZyjPgxP7dElVZi6LzMwBVqyZ3z0FgkMPU= +github.com/flanksource/gomplate/v3 v3.24.74/go.mod h1:l3iet81uj0sje3uMIkAURjOu1E4UErJ3PMVTYhvABCM= +github.com/flanksource/gomplate/v3 v3.24.77 h1:StGjXRxeADULhxA9rXbPzAPk+C/952gNeU+LUO7qRXk= +github.com/flanksource/gomplate/v3 v3.24.77/go.mod h1:IioRhY9IdwdlPI/xdZOiLHGsFJqW4Sp6yXmrqCFuh+k= github.com/flanksource/is-healthy v1.0.79 h1:fBdmJJ7CoNtphZ08gOKxHWS76HltGwIi2L1396cxU2s= github.com/flanksource/is-healthy v1.0.79/go.mod h1:AV3S/uXPnjvfaB4X6CV7xojAHjOmATY6Y9emWdnkJuQ= +github.com/flanksource/is-healthy v1.0.86 h1:N4oxqdW8/YN7+EmEHk4rStpYXzuGADiMAXP2T75bynw= +github.com/flanksource/is-healthy v1.0.86/go.mod h1:xoEeeCamUiW8fGWGyRaGL9NU4xQzou+sgDC5raguDew= github.com/flanksource/kubectl-neat v1.0.4 h1:t5/9CqgE84oEtB0KitgJ2+WIeLfD+RhXSxYrqb4X8yI= github.com/flanksource/kubectl-neat v1.0.4/go.mod h1:Un/Voyh3cmiZNKQrW/TkAl28nAA7vwnwDGVjRErKjOw= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -93,110 +206,207 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf h1:I1sbT4ZbIt9i+hB1zfKw2mE8C12TuGxPiW7YmtLbPa4= github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf/go.mod h1:jDHmWDKZY6MIIYltYYfW4Rs7hQ50oS4qf/6spSiZAxY= github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce h1:cVkYhlWAxwuS2/Yp6qPtcl0fGpcWxuZNonywHZ6/I+s= github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce/go.mod h1:7TyiGlHI+IO+iJbqRZ82QbFtvgj/AIcFm5qc9DLn7Kc= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/sdk v0.20.0/go.mod h1:xEjAt/n/2lHBAkYiRPRmvf1d5B6HlisPh2pELlRCosk= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/go-yaml v0.0.0-20251001235044-fca9a0999f15/go.mod h1:Tmbz8uw5I/I6NvVpEGuhzlElCGS5hPoXJkt7l+ul6LE= github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= +github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= +github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= +github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= +github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603 h1:gSech9iGLFCosfl/DC7BWnpSSh/tQClWnKS2I2vdPww= github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= +github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= +github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lrita/cmap v0.0.0-20231108122212-cb084a67f554 h1:a0+bIffIh/HdvvgtPQLRhOef1VDSxZ+8bQiyjQlJzqc= github.com/lrita/cmap v0.0.0-20231108122212-cb084a67f554/go.mod h1:Cn9TaoncDT8Tt/aJ7CIZy+t48MaZWDEwhu1bBXwrzLI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= github.com/ohler55/ojg v1.25.0 h1:sDwc4u4zex65Uz5Nm7O1QwDKTT+YRcpeZQTy1pffRkw= github.com/ohler55/ojg v1.25.0/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= +github.com/ohler55/ojg v1.28.0 h1:8xClBgMIRRJGDUC9xNe7NprP4kD2C3mQMeon3wY4KXA= +github.com/ohler55/ojg v1.28.0/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4= +github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= +github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/playwright-community/playwright-go v0.4702.0 h1:3CwNpk4RoA42tyhmlgPDMxYEYtMydaeEqMYiW0RNlSY= github.com/playwright-community/playwright-go v0.4702.0/go.mod h1:bpArn5TqNzmP0jroCgw4poSOG9gSeQg490iLqWAaa7w= +github.com/playwright-community/playwright-go v0.5700.1 h1:PNFb1byWqrTT720rEO0JL88C6Ju0EmUnR5deFLvtP/U= +github.com/playwright-community/playwright-go v0.5700.1/go.mod h1:MlSn1dZrx8rszbCxY6x3qK89ZesJUYVx21B2JnkoNF0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -208,44 +418,69 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robertkrimen/otto v0.3.0 h1:5RI+8860NSxvXywDY9ddF5HcPw0puRsd8EgbXV0oqRE= github.com/robertkrimen/otto v0.3.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw= +github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0= +github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/oops v1.19.3 h1:GAfSUOCh/JbnKAj5Ia+r/3KNnBdK+VdVDq1F5I8nDfM= github.com/samber/oops v1.19.3/go.mod h1:1lIO/SwpPltzw5cDO8/oiyVuLiQt3/8iid21Vb8QP+8= +github.com/samber/oops v1.21.0 h1:18atcO4oEigNFuGXqr3NZWZ6P0XOSEXyBSAMXdQRxTc= +github.com/samber/oops v1.21.0/go.mod h1:Hsm/sKPxtCfPh0w/cE3xVoRfSiE1joDRiStPAsmG9bo= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/spf13/cobra v1.9.2-0.20250831231508-51d675196729 h1:w1V8WrGuuPFNsCtyTNLki5XUTOuiMpq5SfvI9eAq9QY= github.com/spf13/cobra v1.9.2-0.20250831231508-51d675196729/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -259,6 +494,8 @@ github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -266,6 +503,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo= github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/timberio/go-datemath v0.1.0 h1:1OUCvSIX1qXLJ57h12OWfgt6MNpJnsdNvrp8dLIUFtg= github.com/timberio/go-datemath v0.1.0/go.mod h1:m7kjsbCuO4QKP3KLfnxiUZWiOiFXmxj30HeexjL3lc0= github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI= @@ -276,8 +515,12 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vadimi/go-http-ntlm v1.0.3 h1:o6n2vAtP1MlLT73jIXuQYryIcWzXyMN0SCQWZ2QVLLc= github.com/vadimi/go-http-ntlm v1.0.3/go.mod h1:SwhhmybQ4Yn1mC53UPmQ6MCrBX6UvJHlS1Xt89OmM9M= github.com/vadimi/go-http-ntlm/v2 v2.5.0 h1:sddEWZumD7GoeNkfFZyZq01pq6CB4U6L73EBw3X7vTU= @@ -288,16 +531,24 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 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/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw= github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q= github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -305,45 +556,79 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -352,17 +637,30 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -375,26 +673,45 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.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/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -402,23 +719,41 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.251.0/go.mod h1:Rwy0lPf/TD7+T2VhYcffCHhyyInyuxGjICxdfLqT7KI= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -433,25 +768,53 @@ gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= +k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0= +k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g= k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/code-generator v0.35.2/go.mod h1:id4XLCm0yAQq5nlvyfAKibMOKnMjzlesAwGw6kM3Adc= +k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kms v0.35.2/go.mod h1:VT+4ekZAdrZDMgShK37vvlyHUVhwI9t/9tvh0AyCWmQ= +k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= +k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM= +k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf h1:rRz0YsF7VXj9fXRF6yQgFI7DzST+hsI3TeFSGupntu0= layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= +sigs.k8s.io/gateway-api v1.5.0 h1:duoo14Ky/fJXpjpmyMISE2RTBGnfCg8zICfTYLTnBJA= +sigs.k8s.io/gateway-api v1.5.0/go.mod h1:GvCETiaMAlLym5CovLxGjS0NysqFk3+Yuq3/rh6QL2o= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/examples/uber_demo/go.mod b/examples/uber_demo/go.mod index 36de9f27..505ea514 100644 --- a/examples/uber_demo/go.mod +++ b/examples/uber_demo/go.mod @@ -1,15 +1,16 @@ module github.com/flanksource/clicky/examples/uber_demo -go 1.25.1 +go 1.26.1 require ( github.com/flanksource/clicky v1.21.1 - github.com/flanksource/commons v1.47.2 - github.com/flanksource/gomplate/v3 v3.24.74 + github.com/flanksource/commons v1.50.3 + github.com/flanksource/gomplate/v3 v3.24.77 github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.28.0 github.com/onsi/gomega v1.39.1 github.com/spf13/cobra v1.10.2 + golang.org/x/term v0.40.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -19,15 +20,23 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect github.com/alecthomas/chroma/v2 v2.23.1 // indirect + github.com/antchfx/xmlquery v1.5.1 // indirect + github.com/antchfx/xpath v1.3.6 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect github.com/cert-manager/cert-manager v1.19.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/huh v1.0.0 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -36,6 +45,7 @@ require ( github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect github.com/fatih/color v1.18.0 // indirect github.com/flanksource/is-healthy v1.0.86 // indirect @@ -51,6 +61,7 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/cel-go v0.27.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect @@ -72,9 +83,12 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ohler55/ojg v1.28.0 // indirect @@ -100,6 +114,8 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/samber/lo v1.53.0 // indirect github.com/samber/oops v1.21.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.11.1 // indirect @@ -110,6 +126,7 @@ require ( github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/timberio/go-datemath v0.1.0 // indirect github.com/tj/go-naturaldate v1.3.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect @@ -128,7 +145,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect diff --git a/examples/uber_demo/go.sum b/examples/uber_demo/go.sum index 9a8a078f..463694b9 100644 --- a/examples/uber_demo/go.sum +++ b/examples/uber_demo/go.sum @@ -32,9 +32,15 @@ github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HR github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78= +github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8= +github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= +github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI= github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w= @@ -63,14 +69,22 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cert-manager/cert-manager v1.19.4 h1:7lOkSYj+nJNjgGFfAznQzPpOfWX+1Kgz6xUXwTa/K5k= github.com/cert-manager/cert-manager v1.19.4/go.mod h1:9uBnn3IK9NxjjuXmQDYhwOwFUU5BtGVB1g/voPvvcVw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= @@ -79,6 +93,8 @@ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMx github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -107,6 +123,7 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= @@ -118,10 +135,14 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flanksource/commons v1.47.2 h1:3CE3MHBWM/1rehHDduUR8r55rP8HorbYuiB2LO8Rjmw= github.com/flanksource/commons v1.47.2/go.mod h1:ouTo/VFVZ/7LKEDWgHLQKdCJsTreDrUOmV0Z3CxZosY= +github.com/flanksource/commons v1.50.3 h1:uWsI9dCxkCrpnCplFlQ+wy3jbOedOvsGcqoogrgmrsQ= +github.com/flanksource/commons v1.50.3/go.mod h1:IAZ94jQnUMMRShmbkg4+sPHEdyQfVc7a1tl2BnL1VF8= github.com/flanksource/gomplate/v3 v3.24.71 h1:c610TQ+YEhA39bJIl5wiG3ynWxyrPlbgu4BoCHxlJfo= github.com/flanksource/gomplate/v3 v3.24.71/go.mod h1:PMJGo4K81b0TB0FrC8WJDE9+vW3X/zWHQ8eGpCMYXmo= github.com/flanksource/gomplate/v3 v3.24.74 h1:LR1TGUxbQiZyjPgxP7dElVZi6LzMwBVqyZ3z0FgkMPU= github.com/flanksource/gomplate/v3 v3.24.74/go.mod h1:l3iet81uj0sje3uMIkAURjOu1E4UErJ3PMVTYhvABCM= +github.com/flanksource/gomplate/v3 v3.24.77 h1:StGjXRxeADULhxA9rXbPzAPk+C/952gNeU+LUO7qRXk= +github.com/flanksource/gomplate/v3 v3.24.77/go.mod h1:IioRhY9IdwdlPI/xdZOiLHGsFJqW4Sp6yXmrqCFuh+k= github.com/flanksource/is-healthy v1.0.83 h1:5wiq4gwUXpWnihqikLyGLTri+bbpxWtiUCQPesvJle4= github.com/flanksource/is-healthy v1.0.83/go.mod h1:6/KOjVUeevIbaIaVtSDhy6qsnbqyy5WPZNlRGQCBxcw= github.com/flanksource/is-healthy v1.0.86 h1:N4oxqdW8/YN7+EmEHk4rStpYXzuGADiMAXP2T75bynw= @@ -173,6 +194,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= @@ -182,6 +205,7 @@ github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXn github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -279,6 +303,8 @@ github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3v github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -289,6 +315,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -359,7 +389,9 @@ github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/oops v1.21.0 h1:18atcO4oEigNFuGXqr3NZWZ6P0XOSEXyBSAMXdQRxTc= github.com/samber/oops v1.21.0/go.mod h1:Hsm/sKPxtCfPh0w/cE3xVoRfSiE1joDRiStPAsmG9bo= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= @@ -397,6 +429,7 @@ github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0 github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4RI= github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= @@ -466,7 +499,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= @@ -476,6 +512,9 @@ golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -485,6 +524,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= @@ -492,6 +535,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -505,15 +552,23 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -521,7 +576,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -531,6 +589,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= diff --git a/examples/uber_demo/main.go b/examples/uber_demo/main.go index 90d36fa2..b52e313a 100644 --- a/examples/uber_demo/main.go +++ b/examples/uber_demo/main.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "strings" "time" "github.com/flanksource/clicky" @@ -14,6 +15,7 @@ import ( flanksourceContext "github.com/flanksource/commons/context" "github.com/google/uuid" "github.com/spf13/cobra" + "golang.org/x/term" ) // Helper functions for creating pointers @@ -614,6 +616,21 @@ type TasksOptions struct { Timeout time.Duration `flag:"timeout" help:"Overall timeout for all tasks" default:"0"` } +// TaskPromptOptions configures the interactive task/prompt demo. +type TaskPromptOptions struct { + NoSleep bool `flag:"no-sleep" help:"Skip simulated delays before the prompt appears" default:"false"` + Count int `flag:"count" help:"Number of options to generate for the select prompts" default:"8"` +} + +type TaskPromptResult struct { + SelectedAction string `json:"selected_action" pretty:"label=Selected Action"` + SelectedChecks []string `json:"selected_checks,omitempty" pretty:"label=Selected Checks,omitempty"` + ChangeTicket string `json:"change_ticket,omitempty" pretty:"label=Change Ticket,omitempty"` + PromptStatus string `json:"prompt_status" pretty:"label=Prompt Status"` + OptionCount int `json:"option_count" pretty:"label=Option Count"` + TerminalNote string `json:"terminal_note" pretty:"label=Terminal Handoff"` +} + // ProductTable demonstrates basic table formatting type ProductTable struct { ID int `json:"id" pretty:"label=ID"` @@ -868,6 +885,156 @@ func showTasks(opts TasksOptions) (any, error) { return nil, nil } +func showTaskPrompt(opts TaskPromptOptions) (any, error) { + if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stderr.Fd())) { + return nil, fmt.Errorf("task-prompt requires an interactive terminal") + } + + optionCount := max(opts.Count, 3) + actions := buildRolloutActions(optionCount) + checks := buildValidationChecks(optionCount) + + sleep := func(d time.Duration) { + if !opts.NoSleep { + time.Sleep(d) + } + } + + releaseWatch := make(chan struct{}) + + task.StartTask("Collect rollout snapshot", func(ctx flanksourceContext.Context, t *task.Task) (map[string]any, error) { + steps := []string{ + "Inspecting cluster state", + "Checking canary pods", + "Verifying recent error rates", + } + for i, step := range steps { + sleep(180 * time.Millisecond) + t.SetProgress(i+1, len(steps)) + t.Infof("%s", step) + } + t.Success() + return map[string]any{"region": "us-east-1", "healthy_pods": 12}, nil + }) + + task.StartTask("Watch operator gate", func(ctx flanksourceContext.Context, t *task.Task) (string, error) { + stages := []string{ + "Streaming canary metrics", + "Tailing ingress logs", + "Waiting for operator prompt", + } + for i, stage := range stages { + sleep(150 * time.Millisecond) + t.SetDescription(stage) + t.SetProgress(i+1, len(stages)) + } + + <-releaseWatch + t.SetDescription("Operator responded") + t.Success() + return "gate released", nil + }) + + sleep(350 * time.Millisecond) + + result := TaskPromptResult{ + SelectedAction: "operator cancelled", + PromptStatus: "selection cancelled", + OptionCount: optionCount, + TerminalNote: "Task rendering stops before prompt UI takes over the ANSI terminal.", + } + + action, ok := clicky.PromptSelect(actions, clicky.PromptSelectOptions[string]{ + Title: "Choose the next rollout action", + }) + + if ok { + result.SelectedAction = action + result.PromptStatus = "action selected" + + selectedChecks, checksOK := clicky.PromptMultiSelect(checks, clicky.PromptMultiSelectOptions[string]{ + Title: "Choose the rollout checks to keep running", + Limit: min(4, len(checks)), + Ordered: true, + }) + + if checksOK { + result.SelectedChecks = selectedChecks + result.PromptStatus = "checks selected" + + ticket, ticketOK := clicky.PromptText(clicky.PromptTextOptions{ + Title: "Enter the change ticket", + Placeholder: "e.g. REL-1234", + Validate: func(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("change ticket is required") + } + return nil + }, + }) + if ticketOK { + result.ChangeTicket = ticket + result.PromptStatus = "confirmed" + } else { + result.PromptStatus = "ticket entry cancelled" + } + } else { + result.PromptStatus = "check selection cancelled" + } + } + + close(releaseWatch) + task.Wait() + + return result, nil +} + +func buildRolloutActions(count int) []string { + base := []string{ + "Promote canary", + "Hold rollout", + "Abort release", + "Roll back deploy", + "Pause traffic shift", + "Scale canary up", + "Scale canary down", + "Open incident bridge", + "Notify API owners", + "Trigger smoke suite", + } + + return buildPromptOptions(base, count, "Action") +} + +func buildValidationChecks(count int) []string { + base := []string{ + "Tail ingress error logs", + "Watch saturation dashboard", + "Run synthetic checkout probe", + "Verify canary pod restarts", + "Inspect downstream queue depth", + "Track p95 latency panel", + "Confirm feature flag rollout", + "Ping on-call database owner", + "Check payment gateway health", + "Review deployment event stream", + } + + return buildPromptOptions(base, count, "Check") +} + +func buildPromptOptions(base []string, count int, prefix string) []string { + options := make([]string, 0, count) + for i := 0; i < count; i++ { + if i < len(base) { + options = append(options, base[i]) + continue + } + options = append(options, fmt.Sprintf("%s %02d", prefix, i+1)) + } + return options +} + func runBasicTasks(sleep func(time.Duration), opts ...task.Option) { // Task that succeeds and returns a string result task.StartTask("Download dependencies", func(ctx flanksourceContext.Context, t *task.Task) (string, error) { @@ -1277,6 +1444,7 @@ func main() { clicky.AddNamedCommand("table-provider", rootCmd, TableProviderOptions{}, showTableProvider) clicky.AddNamedCommand("trees", rootCmd, clicky.FileTreeOptions{}, showTrees) clicky.AddNamedCommand("tasks", rootCmd, TasksOptions{}, showTasks) + clicky.AddNamedCommand("task-prompt", rootCmd, TaskPromptOptions{}, showTaskPrompt) clicky.AddNamedCommand("task-ui", rootCmd, TaskUIOptions{}, showTaskUI) clicky.AddNamedCommand("nil-handling", rootCmd, NilHandlingOptions{}, showNilHandling) clicky.AddNamedCommand("components", rootCmd, ComponentsOptions{}, showComponents) diff --git a/formatters/html_react_formatter.go b/formatters/html_react_formatter.go index 06b23f4a..500e9f01 100644 --- a/formatters/html_react_formatter.go +++ b/formatters/html_react_formatter.go @@ -21,9 +21,9 @@ func init() { type HTMLReactFormatter struct{} -// ClickyJSONFormatter emits the Clicky document JSON (application/clicky+json) -// that the html-react formatter embeds inside its HTML shell. The payload is -// the same shape consumed by @flanksource/clicky-ui's . +// ClickyJSONFormatter emits the Clicky document JSON (application/json+clicky). +// The HTTP layer also accepts the legacy application/clicky+json alias. The +// payload is the same shape consumed by @flanksource/clicky-ui's . type ClickyJSONFormatter struct{} type clickyDocument struct { diff --git a/formatters/http/http.go b/formatters/http/http.go index 2eb8728e..1bcca215 100644 --- a/formatters/http/http.go +++ b/formatters/http/http.go @@ -188,7 +188,7 @@ func acceptToFormat(accept string) string { return "excel" case "application/vnd.slack.block-kit+json": return "slack" - case "application/clicky+json": + case "application/clicky+json", "application/json+clicky": return "clicky-json" case "text/plain": return "pretty" @@ -241,7 +241,7 @@ func formatToContentType(format string) string { case "json": return "application/json" case "clicky-json": - return "application/clicky+json" + return "application/json+clicky" case "yaml", "yml": return "application/yaml" case "csv": diff --git a/formatters/http/http_test.go b/formatters/http/http_test.go index f27f78b1..e0f686eb 100644 --- a/formatters/http/http_test.go +++ b/formatters/http/http_test.go @@ -70,6 +70,14 @@ func TestFormatHandler_ClickyJSON(t *testing.T) { return r }(), }, + { + name: "Accept: application/json+clicky", + req: func() *http.Request { + r := httptest.NewRequest("GET", "http://example.com/users", nil) + r.Header.Set("Accept", "application/json+clicky") + return r + }(), + }, } for _, tc := range cases { @@ -80,8 +88,8 @@ func TestFormatHandler_ClickyJSON(t *testing.T) { if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } - if ct := w.Header().Get("Content-Type"); ct != "application/clicky+json" { - t.Errorf("Content-Type = %q, want application/clicky+json", ct) + if ct := w.Header().Get("Content-Type"); ct != "application/json+clicky" { + t.Errorf("Content-Type = %q, want application/json+clicky", ct) } var doc struct { diff --git a/global_task_api.go b/global_task_api.go index bccc40d0..5ce9da25 100644 --- a/global_task_api.go +++ b/global_task_api.go @@ -54,6 +54,11 @@ func CancelAllGlobalTasks() { task.CancelAll() } +// StopTask cancels a specific global task by immutable task ID. +func StopTask(id string) bool { + return task.StopTask(id) +} + // ClearGlobalTasks removes completed tasks from the global TaskManager func ClearGlobalTasks() { task.ClearTasks() diff --git a/go.mod b/go.mod index 6db8031d..672e7748 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,14 @@ go 1.26.1 require ( github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b github.com/alecthomas/chroma/v2 v2.23.1 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/ansi v0.11.6 github.com/creack/pty v1.1.24 - github.com/flanksource/commons v1.47.2 - github.com/flanksource/gomplate/v3 v3.24.74 + github.com/flanksource/commons v1.50.3 + github.com/flanksource/gomplate/v3 v3.24.77 github.com/go-xmlfmt/xmlfmt v1.1.3 github.com/goccy/go-yaml v1.19.2 github.com/golang-jwt/jwt/v5 v5.3.0 @@ -46,7 +50,10 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Snawoot/go-http-digest-auth-client v1.1.3 // indirect + github.com/antchfx/xmlquery v1.5.1 // indirect + github.com/antchfx/xpath v1.3.6 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.9 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect @@ -63,11 +70,12 @@ require ( github.com/aws/smithy-go v1.24.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect github.com/cert-manager/cert-manager v1.19.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -75,6 +83,7 @@ require ( github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect github.com/fatih/color v1.18.0 // indirect github.com/flanksource/is-healthy v1.0.86 // indirect @@ -90,6 +99,7 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/gosimple/slug v1.15.0 // indirect @@ -111,9 +121,12 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ohler55/ojg v1.28.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect diff --git a/go.sum b/go.sum index bc4d55ff..517edf87 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,48 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/Snawoot/go-http-digest-auth-client v1.1.3 h1:Xd/SNBuIUJqotzmxRpbXovBJxmlVZOT19IZZdMdrJ0Q= github.com/Snawoot/go-http-digest-auth-client v1.1.3/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg= +github.com/Venafi/vcert/v5 v5.12.2/go.mod h1:x3l0pB0q0E6wuhPe7nzfkUEwwraK7amnBWQ4LtT1bbw= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.0.0/go.mod h1:Bf6hnZkloZnfL4I/gFGnMMMdMHiu/ERnSOWtFgnodDk= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78= +github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8= +github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= +github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A= @@ -37,6 +61,7 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEd github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.58.4/go.mod h1:xNLZLn4SusktBQ5moqUOgiDKGz3a7vHwF4W0KD+WBPc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y= @@ -51,32 +76,61 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cert-manager/cert-manager v1.19.4 h1:7lOkSYj+nJNjgGFfAznQzPpOfWX+1Kgz6xUXwTa/K5k= github.com/cert-manager/cert-manager v1.19.4/go.mod h1:9uBnn3IK9NxjjuXmQDYhwOwFUU5BtGVB1g/voPvvcVw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -87,22 +141,35 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/digitalocean/godo v1.165.1/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 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/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flanksource/commons v1.47.2 h1:3CE3MHBWM/1rehHDduUR8r55rP8HorbYuiB2LO8Rjmw= github.com/flanksource/commons v1.47.2/go.mod h1:ouTo/VFVZ/7LKEDWgHLQKdCJsTreDrUOmV0Z3CxZosY= +github.com/flanksource/commons v1.50.3 h1:uWsI9dCxkCrpnCplFlQ+wy3jbOedOvsGcqoogrgmrsQ= +github.com/flanksource/commons v1.50.3/go.mod h1:IAZ94jQnUMMRShmbkg4+sPHEdyQfVc7a1tl2BnL1VF8= github.com/flanksource/gomplate/v3 v3.24.74 h1:LR1TGUxbQiZyjPgxP7dElVZi6LzMwBVqyZ3z0FgkMPU= github.com/flanksource/gomplate/v3 v3.24.74/go.mod h1:l3iet81uj0sje3uMIkAURjOu1E4UErJ3PMVTYhvABCM= +github.com/flanksource/gomplate/v3 v3.24.77 h1:StGjXRxeADULhxA9rXbPzAPk+C/952gNeU+LUO7qRXk= +github.com/flanksource/gomplate/v3 v3.24.77/go.mod h1:IioRhY9IdwdlPI/xdZOiLHGsFJqW4Sp6yXmrqCFuh+k= github.com/flanksource/is-healthy v1.0.86 h1:N4oxqdW8/YN7+EmEHk4rStpYXzuGADiMAXP2T75bynw= github.com/flanksource/is-healthy v1.0.86/go.mod h1:xoEeeCamUiW8fGWGyRaGL9NU4xQzou+sgDC5raguDew= github.com/flanksource/kubectl-neat v1.0.4 h1:t5/9CqgE84oEtB0KitgJ2+WIeLfD+RhXSxYrqb4X8yI= github.com/flanksource/kubectl-neat v1.0.4/go.mod h1:Un/Voyh3cmiZNKQrW/TkAl28nAA7vwnwDGVjRErKjOw= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -115,15 +182,25 @@ github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -136,33 +213,69 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf h1:I1sbT4ZbIt9i+hB1zfKw2mE8C12TuGxPiW7YmtLbPa4= github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf/go.mod h1:jDHmWDKZY6MIIYltYYfW4Rs7hQ50oS4qf/6spSiZAxY= github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce h1:cVkYhlWAxwuS2/Yp6qPtcl0fGpcWxuZNonywHZ6/I+s= github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce/go.mod h1:7TyiGlHI+IO+iJbqRZ82QbFtvgj/AIcFm5qc9DLn7Kc= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/sdk v0.20.0/go.mod h1:xEjAt/n/2lHBAkYiRPRmvf1d5B6HlisPh2pELlRCosk= 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/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/go-yaml v0.0.0-20251001235044-fca9a0999f15/go.mod h1:Tmbz8uw5I/I6NvVpEGuhzlElCGS5hPoXJkt7l+ul6LE= github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= @@ -173,15 +286,21 @@ github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -194,30 +313,45 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY= github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= github.com/ohler55/ojg v1.28.0 h1:8xClBgMIRRJGDUC9xNe7NprP4kD2C3mQMeon3wY4KXA= github.com/ohler55/ojg v1.28.0/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= @@ -230,15 +364,21 @@ github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4= github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc= github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -271,6 +411,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/oops v1.21.0 h1:18atcO4oEigNFuGXqr3NZWZ6P0XOSEXyBSAMXdQRxTc= @@ -283,12 +425,16 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -317,8 +463,10 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vadimi/go-http-ntlm v1.0.3 h1:o6n2vAtP1MlLT73jIXuQYryIcWzXyMN0SCQWZ2QVLLc= github.com/vadimi/go-http-ntlm v1.0.3/go.mod h1:SwhhmybQ4Yn1mC53UPmQ6MCrBX6UvJHlS1Xt89OmM9M= github.com/vadimi/go-http-ntlm/v2 v2.5.0 h1:sddEWZumD7GoeNkfFZyZq01pq6CB4U6L73EBw3X7vTU= @@ -330,8 +478,11 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 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/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= @@ -340,16 +491,28 @@ github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzx github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= @@ -358,8 +521,12 @@ go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ7 go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -368,7 +535,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= @@ -378,6 +548,9 @@ golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -387,6 +560,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= @@ -395,6 +572,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -411,14 +592,22 @@ 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -426,7 +615,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -436,23 +628,34 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.251.0/go.mod h1:Rwy0lPf/TD7+T2VhYcffCHhyyInyuxGjICxdfLqT7KI= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -471,23 +674,33 @@ k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpAp k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/code-generator v0.35.2/go.mod h1:id4XLCm0yAQq5nlvyfAKibMOKnMjzlesAwGw6kM3Adc= +k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kms v0.35.2/go.mod h1:VT+4ekZAdrZDMgShK37vvlyHUVhwI9t/9tvh0AyCWmQ= +k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM= k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf h1:rRz0YsF7VXj9fXRF6yQgFI7DzST+hsI3TeFSGupntu0= layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/gateway-api v1.5.0 h1:duoo14Ky/fJXpjpmyMISE2RTBGnfCg8zICfTYLTnBJA= sigs.k8s.io/gateway-api v1.5.0/go.mod h1:GvCETiaMAlLym5CovLxGjS0NysqFk3+Yuq3/rh6QL2o= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/gumchoose/LICENSE b/internal/gumchoose/LICENSE new file mode 100644 index 00000000..49691c48 --- /dev/null +++ b/internal/gumchoose/LICENSE @@ -0,0 +1,23 @@ +Vendored from https://github.com/charmbracelet/gum/tree/main/choose + +MIT License + +Copyright (c) Charmbracelet, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/internal/gumchoose/choose.go b/internal/gumchoose/choose.go new file mode 100644 index 00000000..14770675 --- /dev/null +++ b/internal/gumchoose/choose.go @@ -0,0 +1,417 @@ +// Package gumchoose vendors the choose implementation from charmbracelet/gum. +// It is adapted for local use in clicky's prompt APIs. +package gumchoose + +import ( + "io" + "slices" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/paginator" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +type keymap struct { + Down, Up, Right, Left, Home, End, ToggleAll, Toggle, Abort, Quit, Submit key.Binding +} + +func defaultKeymap() keymap { + return keymap{ + Down: key.NewBinding( + key.WithKeys("down", "j", "ctrl+j", "ctrl+n"), + ), + Up: key.NewBinding( + key.WithKeys("up", "k", "ctrl+k", "ctrl+p"), + ), + Right: key.NewBinding( + key.WithKeys("right", "l", "ctrl+f"), + ), + Left: key.NewBinding( + key.WithKeys("left", "h", "ctrl+b"), + ), + Home: key.NewBinding( + key.WithKeys("g", "home"), + ), + End: key.NewBinding( + key.WithKeys("G", "end"), + ), + ToggleAll: key.NewBinding( + key.WithKeys("a", "A", "ctrl+a"), + key.WithHelp("ctrl+a", "select all"), + key.WithDisabled(), + ), + Toggle: key.NewBinding( + key.WithKeys(" ", "tab", "x", "ctrl+@"), + key.WithHelp("x", "toggle"), + key.WithDisabled(), + ), + Abort: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "abort"), + ), + Quit: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "quit"), + ), + Submit: key.NewBinding( + key.WithKeys("enter", "ctrl+q"), + key.WithHelp("enter", "submit"), + ), + } +} + +func (k keymap) FullHelp() [][]key.Binding { + return nil +} + +func (k keymap) ShortHelp() []key.Binding { + return []key.Binding{ + k.Toggle, + key.NewBinding( + key.WithKeys("up", "down", "right", "left"), + key.WithHelp("←↓↑→", "navigate"), + ), + k.Submit, + k.ToggleAll, + } +} + +// Options configures the vendored chooser. +type Options struct { + Header string + Height int + Cursor string + ShowHelp bool + Limit int + Ordered bool + InitialIndex int + SelectedPrefix string + UnselectedPrefix string +} + +// Result is the outcome of a chooser run. +type Result struct { + Submitted bool + Indexes []int +} + +type item struct { + text string + selected bool + order int +} + +type model struct { + height int + cursor string + + selectedPrefix string + unselectedPrefix string + header string + + items []item + quitting bool + submitted bool + index int + limit int + ordered bool + + numSelected int + currentOrder int + + paginator paginator.Model + showHelp bool + help help.Model + keymap keymap + + cursorStyle lipgloss.Style + headerStyle lipgloss.Style + itemStyle lipgloss.Style + selectedItemStyle lipgloss.Style + paginatorStyle lipgloss.Style +} + +// Run executes the vendored chooser. +func Run(input io.Reader, output io.Writer, items []string, opts Options) (Result, error) { + m := newModel(items, opts) + + program := tea.NewProgram( + m, + tea.WithInput(input), + tea.WithOutput(output), + ) + + finalModel, err := program.Run() + if err != nil { + return Result{}, err + } + + finished := finalModel.(model) + return Result{ + Submitted: finished.submitted, + Indexes: finished.selectedIndexes(), + }, nil +} + +func newModel(options []string, opts Options) model { + height := opts.Height + if height <= 0 { + height = 10 + } + + limit := opts.Limit + if limit <= 0 { + limit = 1 + } + + km := defaultKeymap() + if limit > 1 { + km.Toggle.SetEnabled(true) + km.ToggleAll.SetEnabled(true) + } + + p := paginator.New() + p.Type = paginator.Dots + p.PerPage = height + p.SetTotalPages(len(options)) + + items := make([]item, 0, len(options)) + for _, option := range options { + items = append(items, item{text: option}) + } + + index := clamp(opts.InitialIndex, 0, max(len(items)-1, 0)) + p.Page = index / height + + theme := huh.ThemeCharm() + selectedPrefix := defaultString(opts.SelectedPrefix, "◉ ") + unselectedPrefix := defaultString(opts.UnselectedPrefix, "○ ") + if limit <= 1 { + selectedPrefix = "" + unselectedPrefix = "" + } + + return model{ + height: height, + cursor: defaultString(opts.Cursor, "> "), + selectedPrefix: selectedPrefix, + unselectedPrefix: unselectedPrefix, + header: opts.Header, + items: items, + index: index, + limit: limit, + ordered: opts.Ordered, + paginator: p, + showHelp: opts.ShowHelp, + help: themedHelp(theme), + keymap: km, + cursorStyle: theme.Focused.SelectSelector.UnsetString(), + headerStyle: theme.Group.Title, + itemStyle: theme.Focused.UnselectedOption, + selectedItemStyle: theme.Focused.SelectedOption, + paginatorStyle: theme.Group.Description, + } +} + +func themedHelp(theme *huh.Theme) help.Model { + model := help.New() + model.Styles = theme.Help + return model +} + +func defaultString(value, fallback string) string { + if value == "" { + return fallback + } + return value +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + return m, nil + case tea.KeyMsg: + start, end := m.paginator.GetSliceBounds(len(m.items)) + km := m.keymap + + switch { + case key.Matches(msg, km.Down): + m.index++ + if m.index >= len(m.items) { + m.index = 0 + m.paginator.Page = 0 + } + if m.index >= end { + m.paginator.NextPage() + } + case key.Matches(msg, km.Up): + m.index-- + if m.index < 0 { + m.index = len(m.items) - 1 + m.paginator.Page = m.paginator.TotalPages - 1 + } + if m.index < start { + m.paginator.PrevPage() + } + case key.Matches(msg, km.Right): + m.index = clamp(m.index+m.height, 0, len(m.items)-1) + m.paginator.NextPage() + case key.Matches(msg, km.Left): + m.index = clamp(m.index-m.height, 0, len(m.items)-1) + m.paginator.PrevPage() + case key.Matches(msg, km.End): + m.index = len(m.items) - 1 + m.paginator.Page = m.paginator.TotalPages - 1 + case key.Matches(msg, km.Home): + m.index = 0 + m.paginator.Page = 0 + case key.Matches(msg, km.ToggleAll): + if m.limit <= 1 { + break + } + if m.numSelected < len(m.items) && m.numSelected < m.limit { + m = m.selectAll() + } else { + m = m.deselectAll() + } + case key.Matches(msg, km.Quit): + m.quitting = true + return m, tea.Quit + case key.Matches(msg, km.Abort): + m.quitting = true + return m, tea.Interrupt + case key.Matches(msg, km.Toggle): + if m.limit == 1 { + break + } + if m.items[m.index].selected { + m.items[m.index].selected = false + m.numSelected-- + } else if m.numSelected < m.limit { + m.items[m.index].selected = true + m.items[m.index].order = m.currentOrder + m.numSelected++ + m.currentOrder++ + } + case key.Matches(msg, km.Submit): + m.quitting = true + if m.limit <= 1 && m.numSelected < 1 { + m.items[m.index].selected = true + } + m.submitted = true + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.paginator, cmd = m.paginator.Update(msg) + return m, cmd +} + +func (m model) selectAll() model { + for i := range m.items { + if m.numSelected >= m.limit { + break + } + if m.items[i].selected { + continue + } + m.items[i].selected = true + m.items[i].order = m.currentOrder + m.numSelected++ + m.currentOrder++ + } + return m +} + +func (m model) deselectAll() model { + for i := range m.items { + m.items[i].selected = false + m.items[i].order = 0 + } + m.numSelected = 0 + m.currentOrder = 0 + return m +} + +func (m model) selectedIndexes() []int { + indexes := make([]int, 0, len(m.items)) + for index, item := range m.items { + if item.selected { + indexes = append(indexes, index) + } + } + + if m.ordered { + slices.SortStableFunc(indexes, func(a, b int) int { + return m.items[a].order - m.items[b].order + }) + } + + return indexes +} + +func (m model) View() string { + if m.quitting { + return "" + } + + var s strings.Builder + start, end := m.paginator.GetSliceBounds(len(m.items)) + + for i, item := range m.items[start:end] { + isCurrent := i == m.index%m.height + if isCurrent { + s.WriteString(m.cursorStyle.Render(m.cursor)) + } else { + s.WriteString(strings.Repeat(" ", lipgloss.Width(m.cursor))) + } + + switch { + case item.selected: + s.WriteString(m.selectedItemStyle.Render(m.selectedPrefix + item.text)) + case isCurrent: + s.WriteString(m.selectedItemStyle.Render(m.unselectedPrefix + item.text)) + default: + s.WriteString(m.itemStyle.Render(m.unselectedPrefix + item.text)) + } + + if i != m.height { + s.WriteRune('\n') + } + } + + if m.paginator.TotalPages > 1 { + s.WriteString(strings.Repeat("\n", m.height-m.paginator.ItemsOnPage(len(m.items))+1)) + s.WriteString(m.paginatorStyle.Render(" " + m.paginator.View())) + } + + var parts []string + if m.header != "" { + parts = append(parts, m.headerStyle.Render(m.header)) + } + parts = append(parts, s.String()) + if m.showHelp { + parts = append(parts, "", m.help.View(m.keymap)) + } + + return lipgloss.JoinVertical(lipgloss.Left, parts...) +} + +func clamp(x, low, high int) int { + if x < low { + return low + } + if x > high { + return high + } + return x +} diff --git a/internal/gumchoose/choose_test.go b/internal/gumchoose/choose_test.go new file mode 100644 index 00000000..75423671 --- /dev/null +++ b/internal/gumchoose/choose_test.go @@ -0,0 +1,37 @@ +package gumchoose + +import ( + "strings" + "testing" + + "github.com/charmbracelet/x/ansi" +) + +func TestSingleSelectCurrentRowDoesNotDuplicateCursorOrPrefix(t *testing.T) { + model := newModel([]string{"Scale canary up", "Abort release"}, Options{ + Header: "Choose action", + Height: 5, + Limit: 1, + }) + + view := ansi.Strip(model.View()) + if strings.Contains(view, "> > ") { + t.Fatalf("expected no duplicated cursor, got %q", view) + } + if !strings.Contains(view, "> Scale canary up") { + t.Fatalf("expected single-select row to render without extra prefix, got %q", view) + } +} + +func TestMultiSelectUsesCirclePrefixes(t *testing.T) { + model := newModel([]string{"Check pods", "Inspect logs"}, Options{ + Header: "Choose checks", + Height: 5, + Limit: 2, + }) + + view := ansi.Strip(model.View()) + if !strings.Contains(view, "> ○ Check pods") { + t.Fatalf("expected current multi-select row to include unselected circle, got %q", view) + } +} diff --git a/prompt.go b/prompt.go new file mode 100644 index 00000000..2b298dac --- /dev/null +++ b/prompt.go @@ -0,0 +1,606 @@ +package clicky + +import ( + "errors" + "fmt" + "io" + "os" + "reflect" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/flanksource/clicky/api" + "github.com/flanksource/clicky/internal/gumchoose" + "github.com/flanksource/clicky/task" + "golang.org/x/sys/unix" + "golang.org/x/term" +) + +const ( + promptMinSelectPageSize = 5 + promptClearScreenSequence = "\x1b[2J\x1b[H" + promptCursorQuerySequence = "\x1b[6n" + promptCursorQueryTimeout = 150 * time.Millisecond +) + +// PromptSelectOptions configures an ANSI list prompt. +type PromptSelectOptions[T any] struct { + Title string + InitialIndex int + PageSize int + Render func(T) api.Textable +} + +// PromptMultiSelectOptions configures an ANSI multi-select prompt. +type PromptMultiSelectOptions[T any] struct { + Title string + PageSize int + Limit int + Ordered bool + Render func(T) api.Textable +} + +// PromptTextOptions configures an ANSI text prompt. +type PromptTextOptions struct { + Title string + Default string + Placeholder string + Secret bool + Validate func(string) error +} + +type promptPretty interface { + Pretty() api.Text +} + +type promptTerminal struct { + input *os.File + output *os.File + close func() error +} + +func openPromptTerminal() (*promptTerminal, error) { + if term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stderr.Fd())) { + return &promptTerminal{ + input: os.Stdin, + output: os.Stderr, + }, nil + } + + if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil { + return &promptTerminal{ + input: tty, + output: tty, + close: tty.Close, + }, nil + } + + return nil, fmt.Errorf("interactive terminal required") +} + +func (p *promptTerminal) Close() error { + if p == nil || p.close == nil { + return nil + } + return p.close() +} + +func (p *promptTerminal) lineCount() int { + if p != nil { + if lines := promptTerminalLines(p.output); lines > 0 { + return lines + } + if p.input != p.output { + if lines := promptTerminalLines(p.input); lines > 0 { + return lines + } + } + } + + return api.GetTerminalLines() +} + +func (p *promptTerminal) columnCount() int { + if p != nil { + if columns := promptTerminalColumns(p.output); columns > 0 { + return columns + } + if p.input != p.output { + if columns := promptTerminalColumns(p.input); columns > 0 { + return columns + } + } + } + + return api.GetTerminalWidth() +} + +func (p *promptTerminal) clearScreen() { + if p == nil || p.output == nil { + return + } + _, _ = io.WriteString(p.output, promptClearScreenSequence) +} + +func (p *promptTerminal) cursorRow() (int, bool) { + if p == nil || p.input == nil || p.output == nil { + return 0, false + } + + fd := int(p.input.Fd()) + oldState, err := term.MakeRaw(fd) + if err != nil { + return 0, false + } + defer func() { + _ = term.Restore(fd, oldState) + }() + + if _, err := io.WriteString(p.output, promptCursorQuerySequence); err != nil { + return 0, false + } + + response, ok := readPromptCursorResponse(fd, promptCursorQueryTimeout) + if !ok { + return 0, false + } + + row, _, ok := parsePromptCursorResponse(response) + return row, ok +} + +type promptSession struct { + terminal *promptTerminal + releaseTerminal func() + tookOverRender bool +} + +func newPromptSession() (*promptSession, error) { + terminal, err := openPromptTerminal() + if err != nil { + return nil, err + } + + releaseTerminal, tookOverRender := task.AcquirePromptTerminal() + + return &promptSession{ + terminal: terminal, + releaseTerminal: releaseTerminal, + tookOverRender: tookOverRender, + }, nil +} + +func (s *promptSession) Close() { + if s == nil { + return + } + + if s.releaseTerminal != nil { + s.releaseTerminal() + s.releaseTerminal = nil + } + if s.terminal != nil { + _ = s.terminal.Close() + s.terminal = nil + } +} + +func (s *promptSession) prepareSelectPrompt(promptHeight int) { + if s == nil || s.terminal == nil || promptHeight <= 0 { + return + } + + if s.tookOverRender { + s.terminal.clearScreen() + return + } + + if row, ok := s.terminal.cursorRow(); ok && shouldClearPromptScreen(promptHeight, s.terminal.lineCount(), row) { + s.terminal.clearScreen() + } +} + +// PromptChoose renders a minimal arrow-key choice prompt. +func PromptChoose[T any](items []T) (T, bool) { + return Prompt(items, PromptSelectOptions[T]{}) +} + +// PromptSelect renders a configurable arrow-key choice prompt. +func PromptSelect[T any](items []T, opts PromptSelectOptions[T]) (T, bool) { + return Prompt(items, opts) +} + +// PromptMultiSelect renders a configurable ANSI multi-select prompt. +func PromptMultiSelect[T any](items []T, opts PromptMultiSelectOptions[T]) ([]T, bool) { + if len(items) == 0 { + return nil, false + } + + session, err := newPromptSession() + if err != nil { + return nil, false + } + defer session.Close() + + title := promptTitle(opts.Title, "Choose one or more options") + labels, pageSize, promptHeight := preparePromptChoices(session, title, opts.PageSize, items, opts.Render) + session.prepareSelectPrompt(promptHeight) + + limit := opts.Limit + if limit <= 0 || limit > len(items) { + limit = len(items) + } + + result, err := gumchoose.Run(session.terminal.input, session.terminal.output, labels, gumchoose.Options{ + Header: title, + Height: pageSize, + Cursor: "> ", + ShowHelp: false, + Limit: limit, + Ordered: opts.Ordered, + InitialIndex: 0, + }) + if err != nil { + if errors.Is(err, tea.ErrInterrupted) { + return nil, false + } + return nil, false + } + if !result.Submitted { + return nil, false + } + + selected := make([]T, 0, len(result.Indexes)) + for _, index := range result.Indexes { + if index >= 0 && index < len(items) { + selected = append(selected, items[index]) + } + } + + return selected, true +} + +// Prompt is the shared ANSI list prompt runner used by PromptChoose and PromptSelect. +func Prompt[T any](items []T, opts PromptSelectOptions[T]) (T, bool) { + var zero T + if len(items) == 0 { + return zero, false + } + + session, err := newPromptSession() + if err != nil { + return zero, false + } + defer session.Close() + + title := promptTitle(opts.Title, "Choose an option") + labels, pageSize, promptHeight := preparePromptChoices(session, title, opts.PageSize, items, opts.Render) + selectedIndex := clampPromptIndex(opts.InitialIndex, len(items)) + session.prepareSelectPrompt(promptHeight) + + result, err := gumchoose.Run(session.terminal.input, session.terminal.output, labels, gumchoose.Options{ + Header: title, + Height: pageSize, + Cursor: "> ", + ShowHelp: false, + Limit: 1, + InitialIndex: selectedIndex, + }) + if err != nil { + if errors.Is(err, tea.ErrInterrupted) { + return zero, false + } + return zero, false + } + if !result.Submitted || len(result.Indexes) == 0 { + return zero, false + } + + return items[result.Indexes[0]], true +} + +// PromptText renders an ANSI text entry prompt. +func PromptText(opts PromptTextOptions) (string, bool) { + session, err := newPromptSession() + if err != nil { + return "", false + } + defer session.Close() + + value := opts.Default + + field := huh.NewInput(). + Title(promptTitle(opts.Title, "Enter a value")). + Prompt("> "). + Placeholder(opts.Placeholder). + Value(&value) + + if opts.Secret { + field.EchoMode(huh.EchoModePassword) + } + if opts.Validate != nil { + field.Validate(opts.Validate) + } + + form := newPromptForm(session, field) + if err := form.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return "", false + } + return "", false + } + + return value, true +} + +func newPromptForm(session *promptSession, field huh.Field) *huh.Form { + return huh.NewForm( + huh.NewGroup(field), + ). + WithInput(session.terminal.input). + WithOutput(session.terminal.output). + WithShowHelp(false) +} + +func promptTitle(title, fallback string) string { + if strings.TrimSpace(title) == "" { + return fallback + } + return title +} + +func preparePromptChoices[T any](session *promptSession, title string, requestedPageSize int, items []T, render func(T) api.Textable) ([]string, int, int) { + titleHeight := promptRenderedLineCount(title, session.terminal.columnCount()) + pageSize := clampPromptPageSize(requestedPageSize, len(items), session.terminal.lineCount(), titleHeight) + promptHeight := promptSelectHeight(pageSize, len(items), titleHeight) + + labels := make([]string, 0, len(items)) + for _, item := range items { + labels = append(labels, defaultPromptLabel(item, render)) + } + + return labels, pageSize, promptHeight +} + +func defaultPromptLabel[T any](item T, render func(T) api.Textable) string { + if render != nil { + if rendered := render(item); rendered != nil { + if label := sanitizePromptLabel(rendered.String()); label != "" { + return label + } + } + } + + if label, ok := promptPrettyLabel(any(item)); ok && label != "" { + return label + } + + return sanitizePromptLabel(fmt.Sprintf("%v", item)) +} + +func promptPrettyLabel(item any) (string, bool) { + if isNilPromptValue(item) { + return "", false + } + + pretty, ok := item.(promptPretty) + if !ok { + return "", false + } + + return sanitizePromptLabel(pretty.Pretty().String()), true +} + +func isNilPromptValue(value any) bool { + if value == nil { + return true + } + + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} + +func sanitizePromptLabel(label string) string { + label = strings.Join(strings.Fields(label), " ") + return label +} + +func clampPromptIndex(index, itemCount int) int { + if itemCount == 0 { + return 0 + } + if index < 0 { + return 0 + } + if index >= itemCount { + return itemCount - 1 + } + return index +} + +func promptTerminalLines(file *os.File) int { + if file == nil { + return 0 + } + + _, lines, err := term.GetSize(int(file.Fd())) + if err != nil || lines <= 0 { + return 0 + } + + return lines +} + +func promptTerminalColumns(file *os.File) int { + if file == nil { + return 0 + } + + columns, _, err := term.GetSize(int(file.Fd())) + if err != nil || columns <= 0 { + return 0 + } + + return columns +} + +func promptRenderedLineCount(text string, width int) int { + text = strings.TrimSpace(text) + if text == "" { + return 0 + } + + lines := 0 + for _, line := range strings.Split(text, "\n") { + if line == "" { + lines++ + continue + } + if width <= 0 { + lines++ + continue + } + lineLength := len([]rune(line)) + lines += (lineLength-1)/width + 1 + } + + return lines +} + +func promptSelectHeight(pageSize, itemCount, titleHeight int) int { + height := pageSize + titleHeight + if itemCount > pageSize { + height++ + } + return height +} + +func promptAvailableLines(terminalLines, cursorRow int) int { + if terminalLines <= 0 { + return 0 + } + if cursorRow <= 0 { + return terminalLines + } + + availableLines := terminalLines - cursorRow + if availableLines < 0 { + return 0 + } + + return availableLines +} + +func shouldClearPromptScreen(pageSize, terminalLines, cursorRow int) bool { + if pageSize <= 0 || terminalLines <= 0 { + return false + } + + return pageSize > promptAvailableLines(terminalLines, cursorRow) +} + +func readPromptCursorResponse(fd int, timeout time.Duration) ([]byte, bool) { + deadline := time.Now().Add(timeout) + buf := make([]byte, 0, 32) + tmp := []byte{0} + + for time.Now().Before(deadline) { + remaining := time.Until(deadline) + timeoutMillis := 1 + if remaining > 0 { + timeoutMillis = int(remaining / time.Millisecond) + if timeoutMillis < 1 { + timeoutMillis = 1 + } + } + + pollfds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} + ready, err := unix.Poll(pollfds, timeoutMillis) + if err != nil { + if err == unix.EINTR { + continue + } + return nil, false + } + if ready == 0 || pollfds[0].Revents&unix.POLLIN == 0 { + continue + } + + n, err := unix.Read(fd, tmp) + if err != nil { + if err == unix.EINTR { + continue + } + return nil, false + } + if n == 0 { + continue + } + + buf = append(buf, tmp[0]) + if tmp[0] == 'R' { + return buf, true + } + } + + return nil, false +} + +func parsePromptCursorResponse(response []byte) (row, col int, ok bool) { + start := strings.LastIndex(string(response), "\x1b[") + end := strings.LastIndexByte(string(response), 'R') + if start == -1 || end == -1 || end <= start { + return 0, 0, false + } + + if _, err := fmt.Sscanf(string(response[start:end+1]), "\x1b[%d;%dR", &row, &col); err != nil { + return 0, 0, false + } + if row < 1 || col < 1 { + return 0, 0, false + } + + return row, col, true +} + +func clampPromptPageSize(pageSize, itemCount, terminalLines, chromeHeight int) int { + if itemCount == 0 { + return 0 + } + + maxPageSize := itemCount + if terminalLines > 0 && itemCount+chromeHeight > terminalLines { + maxPageSize = terminalLines - chromeHeight - 1 + if maxPageSize < 1 { + maxPageSize = 1 + } + } + + minPageSize := promptMinSelectPageSize + if minPageSize > itemCount { + minPageSize = itemCount + } + if maxPageSize > 0 && maxPageSize < minPageSize { + minPageSize = maxPageSize + } + + if pageSize <= 0 { + pageSize = maxPageSize + } else if pageSize > maxPageSize { + pageSize = maxPageSize + } + if pageSize > itemCount { + pageSize = itemCount + } + if pageSize < minPageSize { + pageSize = minPageSize + } + return pageSize +} diff --git a/prompt_terminal_ioctl_darwin_test.go b/prompt_terminal_ioctl_darwin_test.go new file mode 100644 index 00000000..efb5d251 --- /dev/null +++ b/prompt_terminal_ioctl_darwin_test.go @@ -0,0 +1,19 @@ +package clicky + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func getTerminalFlags(t *testing.T, fd int) terminalFlags { + t.Helper() + termios, err := unix.IoctlGetTermios(fd, unix.TIOCGETA) + require.NoError(t, err, "failed to get termios") + return terminalFlags{ + iflag: uint64(termios.Iflag), + oflag: uint64(termios.Oflag), + lflag: uint64(termios.Lflag), + } +} diff --git a/prompt_terminal_ioctl_linux_test.go b/prompt_terminal_ioctl_linux_test.go new file mode 100644 index 00000000..92c20c06 --- /dev/null +++ b/prompt_terminal_ioctl_linux_test.go @@ -0,0 +1,19 @@ +package clicky + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func getTerminalFlags(t *testing.T, fd int) terminalFlags { + t.Helper() + termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) + require.NoError(t, err, "failed to get termios") + return terminalFlags{ + iflag: uint64(termios.Iflag), + oflag: uint64(termios.Oflag), + lflag: uint64(termios.Lflag), + } +} diff --git a/prompt_test.go b/prompt_test.go new file mode 100644 index 00000000..5141188c --- /dev/null +++ b/prompt_test.go @@ -0,0 +1,449 @@ +package clicky + +import ( + "bytes" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "testing" + "time" + + "github.com/creack/pty" + "github.com/flanksource/clicky/api" + clickytext "github.com/flanksource/clicky/text" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +type terminalFlags struct { + iflag uint64 + oflag uint64 + lflag uint64 +} + +const criticalLflagMask = unix.ISIG | unix.ICANON | unix.ECHO | unix.IEXTEN +const criticalIflagMask = unix.ICRNL | unix.IXON | unix.BRKINT +const criticalOflagMask = unix.OPOST + +var promptHelperBinary string + +func TestMain(m *testing.M) { + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { + tmpFile, err := os.CreateTemp("", "prompt_test_helper_*") + if err != nil { + panic(err) + } + promptHelperBinary = tmpFile.Name() + _ = tmpFile.Close() + + cmd := exec.Command("go", "build", "-o", promptHelperBinary, "./testdata/prompt_test_helper.go") + cmd.Dir = findProjectRoot() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic("failed to build prompt test helper: " + err.Error()) + } + } + + code := m.Run() + if promptHelperBinary != "" { + _ = os.Remove(promptHelperBinary) + } + os.Exit(code) +} + +func findProjectRoot() string { + dir, err := os.Getwd() + if err != nil { + panic("failed to get working directory: " + err.Error()) + } + + current := dir + for { + if _, err := os.Stat(filepath.Join(current, "go.mod")); err == nil { + return current + } + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } + + return dir +} + +type capturedPTY struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (c *capturedPTY) Write(p []byte) (int, error) { + c.mu.Lock() + defer c.mu.Unlock() + return c.buf.Write(p) +} + +func (c *capturedPTY) String() string { + c.mu.Lock() + defer c.mu.Unlock() + return c.buf.String() +} + +type promptPrettyItem struct { + label string +} + +func (p promptPrettyItem) Pretty() api.Text { + return api.Text{Content: p.label} +} + +type promptStringerItem struct { + label string +} + +func (p promptStringerItem) String() string { + return p.label +} + +func TestDefaultPromptLabelUsesPrettyFirst(t *testing.T) { + got := defaultPromptLabel(promptPrettyItem{label: "pretty-label"}, nil) + if got != "pretty-label" { + t.Fatalf("expected pretty label, got %q", got) + } +} + +func TestDefaultPromptLabelFallsBackToFmt(t *testing.T) { + got := defaultPromptLabel(promptStringerItem{label: "stringer-label"}, nil) + if got != "stringer-label" { + t.Fatalf("expected stringer label, got %q", got) + } +} + +func TestClampPromptPageSizeUsesTerminalHeightWhenUnset(t *testing.T) { + got := clampPromptPageSize(0, 50, 18, 1) + if got != 16 { + t.Fatalf("expected page size 16, got %d", got) + } +} + +func TestClampPromptPageSizeCapsRequestedSizeToTerminalHeight(t *testing.T) { + got := clampPromptPageSize(40, 50, 18, 1) + if got != 16 { + t.Fatalf("expected page size 16, got %d", got) + } +} + +func TestClampPromptPageSizeKeepsRequestedSizeWithinAvailableHeight(t *testing.T) { + got := clampPromptPageSize(6, 50, 18, 1) + if got != 6 { + t.Fatalf("expected page size 6, got %d", got) + } +} + +func TestClampPromptPageSizeUsesMinimumSelectHeight(t *testing.T) { + got := clampPromptPageSize(1, 50, 18, 1) + if got != 5 { + t.Fatalf("expected page size 5, got %d", got) + } +} + +func TestPromptSelectHeightIncludesTitleAndPaginator(t *testing.T) { + got := promptSelectHeight(5, 8, 1) + if got != 7 { + t.Fatalf("expected prompt height 7, got %d", got) + } +} + +func TestPromptRenderedLineCount(t *testing.T) { + got := promptRenderedLineCount("Choose the next rollout action", 80) + if got != 1 { + t.Fatalf("expected rendered line count 1, got %d", got) + } +} + +func TestPromptAvailableLinesUsesRemainingRows(t *testing.T) { + got := promptAvailableLines(24, 10) + if got != 14 { + t.Fatalf("expected 14 available lines, got %d", got) + } +} + +func TestShouldClearPromptScreenWhenPageSizeExceedsRemainingRows(t *testing.T) { + if !shouldClearPromptScreen(8, 24, 18) { + t.Fatalf("expected prompt to clear when page size exceeds remaining rows") + } +} + +func TestShouldNotClearPromptScreenWhenPageFitsRemainingRows(t *testing.T) { + if shouldClearPromptScreen(6, 24, 18) { + t.Fatalf("expected prompt to fit without clearing") + } +} + +func TestPrepareSelectPromptClearsScreenAfterTaskTakeover(t *testing.T) { + output, err := os.CreateTemp("", "prompt_output_*") + require.NoError(t, err) + defer os.Remove(output.Name()) + defer output.Close() + + session := &promptSession{ + terminal: &promptTerminal{output: output}, + tookOverRender: true, + } + + session.prepareSelectPrompt(8) + + _, err = output.Seek(0, 0) + require.NoError(t, err) + + data, err := io.ReadAll(output) + require.NoError(t, err) + require.Equal(t, promptClearScreenSequence, string(data)) +} + +func TestPromptChooseConfirmsSelectionAndRestoresTerminal(t *testing.T) { + skipPromptPTYTests(t) + + ptmx, capture, before, cmd := startPromptHelper(t, "choose_confirm") + defer ptmx.Close() + + time.Sleep(500 * time.Millisecond) + _, err := ptmx.Write([]byte("\x1b[B")) + require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + _, err = ptmx.Write([]byte{'\r'}) + require.NoError(t, err) + + requireExitWithin(t, cmd, 10*time.Second) + after := getTerminalFlags(t, int(ptmx.Fd())) + assertFlagsMatch(t, before, after) + + output := clickytext.StripANSI(capture.String()) + require.Contains(t, output, "RESULT=beta OK=true") +} + +func TestPromptChooseCancel(t *testing.T) { + skipPromptPTYTests(t) + + ptmx, capture, _, cmd := startPromptHelper(t, "choose_cancel") + defer ptmx.Close() + + time.Sleep(500 * time.Millisecond) + _, err := ptmx.Write([]byte{27}) + require.NoError(t, err) + + requireExitWithin(t, cmd, 10*time.Second) + output := clickytext.StripANSI(capture.String()) + require.Contains(t, output, "RESULT= OK=false") +} + +func TestPromptMultiSelectConfirmsSelections(t *testing.T) { + skipPromptPTYTests(t) + + ptmx, capture, _, cmd := startPromptHelper(t, "multi_confirm") + defer ptmx.Close() + + time.Sleep(500 * time.Millisecond) + _, err := ptmx.Write([]byte("x")) + require.NoError(t, err) + _, err = ptmx.Write([]byte("\x1b[B")) + require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + _, err = ptmx.Write([]byte("x")) + require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + _, err = ptmx.Write([]byte{'\r'}) + require.NoError(t, err) + + requireExitWithin(t, cmd, 10*time.Second) + output := clickytext.StripANSI(capture.String()) + require.Contains(t, output, "RESULT=alpha,beta OK=true") +} + +func TestPromptTextValidation(t *testing.T) { + skipPromptPTYTests(t) + + ptmx, capture, _, cmd := startPromptHelper(t, "text_validate") + defer ptmx.Close() + + waitForCapturedText(t, capture, "> ") + _, err := ptmx.Write([]byte{'\r'}) + require.NoError(t, err) + waitForCapturedText(t, capture, "value required") + + _, err = ptmx.Write([]byte("moshe\r")) + require.NoError(t, err) + + requireExitWithin(t, cmd, 10*time.Second) + output := clickytext.StripANSI(capture.String()) + require.Contains(t, output, "RESULT=moshe OK=true") +} + +func TestPromptTextSecretDoesNotEchoInput(t *testing.T) { + skipPromptPTYTests(t) + + ptmx, capture, _, cmd := startPromptHelper(t, "secret") + defer ptmx.Close() + + waitForCapturedText(t, capture, "> ") + _, err := ptmx.Write([]byte("shh\r")) + require.NoError(t, err) + + requireExitWithin(t, cmd, 10*time.Second) + output := clickytext.StripANSI(capture.String()) + require.Contains(t, output, "RESULT_LEN=3 OK=true") + if strings.Contains(output, "shh") { + t.Fatalf("secret input was echoed in prompt output: %q", output) + } +} + +func TestPromptStopsTaskRenderingBeforePrompt(t *testing.T) { + skipPromptPTYTests(t) + + ptmx, capture, _, cmd := startPromptHelper(t, "task_takeover") + defer ptmx.Close() + + time.Sleep(800 * time.Millisecond) + _, err := ptmx.Write([]byte("\x1b[B")) + require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + _, err = ptmx.Write([]byte{'\r'}) + require.NoError(t, err) + + requireExitWithin(t, cmd, 10*time.Second) + output := clickytext.StripANSI(capture.String()) + + if !strings.Contains(output, "TAKEOVER-MARKER") { + t.Fatalf("expected task output before prompt takeover, got %q", output) + } + require.Contains(t, output, "stop-marker") + require.Contains(t, output, "OK=true") +} + +func skipPromptPTYTests(t *testing.T) { + t.Helper() + if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { + t.Skip("PTY prompt tests only run on darwin and linux") + } + if promptHelperBinary == "" { + t.Skip("prompt helper not built") + } +} + +func startPromptHelper(t *testing.T, mode string) (*os.File, *capturedPTY, terminalFlags, *exec.Cmd) { + t.Helper() + + cmd := exec.Command(promptHelperBinary) + cmd.Env = append(os.Environ(), + "PROMPT_MODE="+mode, + "TERM=xterm-256color", + ) + + ptmx, err := pty.Start(cmd) + require.NoError(t, err) + + capture := &capturedPTY{} + go func() { + buffer := make([]byte, 4096) + pending := "" + + for { + n, err := ptmx.Read(buffer) + if n > 0 { + chunk := buffer[:n] + _, _ = capture.Write(chunk) + pending += string(chunk) + pending = replyToTerminalQueries(t, ptmx, pending) + } + if err != nil { + return + } + } + }() + + before := getTerminalFlags(t, int(ptmx.Fd())) + return ptmx, capture, before, cmd +} + +func replyToTerminalQueries(t *testing.T, ptmx *os.File, pending string) string { + t.Helper() + + replies := map[string]string{ + "\x1b[6n": "\x1b[1;1R", + "\x1b]11;?\x1b\\": "\x1b]11;rgb:0000/0000/0000\x1b\\", + } + + for query, reply := range replies { + for { + index := strings.Index(pending, query) + if index == -1 { + break + } + _, err := ptmx.Write([]byte(reply)) + require.NoError(t, err) + pending = pending[:index] + pending[index+len(query):] + } + } + + if len(pending) > 128 { + pending = pending[len(pending)-128:] + } + + return pending +} + +func waitForCapturedText(t *testing.T, capture *capturedPTY, want string) { + t.Helper() + + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + if strings.Contains(clickytext.StripANSI(capture.String()), want) { + return + } + time.Sleep(20 * time.Millisecond) + } + + t.Fatalf("timeout waiting for %q in output: %q", want, clickytext.StripANSI(capture.String())) +} + +func requireExitWithin(t *testing.T, cmd *exec.Cmd, timeout time.Duration) { + t.Helper() + + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + select { + case err := <-done: + if err == nil { + return + } + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 { + t.Fatalf("helper exited with error: %v", err) + } + case <-time.After(timeout): + _ = cmd.Process.Signal(syscall.SIGKILL) + t.Fatal("timeout waiting for prompt helper to exit") + } +} + +func assertFlagsMatch(t *testing.T, before, after terminalFlags) { + t.Helper() + + if before.lflag&criticalLflagMask != after.lflag&criticalLflagMask { + t.Fatalf("lflag mismatch: before=%#x after=%#x", before.lflag&criticalLflagMask, after.lflag&criticalLflagMask) + } + if before.iflag&criticalIflagMask != after.iflag&criticalIflagMask { + t.Fatalf("iflag mismatch: before=%#x after=%#x", before.iflag&criticalIflagMask, after.iflag&criticalIflagMask) + } + if before.oflag&criticalOflagMask != after.oflag&criticalOflagMask { + t.Fatalf("oflag mismatch: before=%#x after=%#x", before.oflag&criticalOflagMask, after.oflag&criticalOflagMask) + } +} diff --git a/rpc/converter.go b/rpc/converter.go index 729d7da9..8e16d21c 100644 --- a/rpc/converter.go +++ b/rpc/converter.go @@ -165,6 +165,9 @@ func (c *Converter) ConvertCommand(cmd *cobra.Command) (*RPCOperation, error) { if df := clicky.GetDataFunc(cmd); df != nil { operation.DataFunc = df } + if lf := clicky.GetLookupFunc(cmd); lf != nil { + operation.LookupFunc = lf + } return operation, nil } diff --git a/rpc/executor.go b/rpc/executor.go index d50e37ab..82a58732 100644 --- a/rpc/executor.go +++ b/rpc/executor.go @@ -75,12 +75,15 @@ func NewCommandExecutor(service *RPCService, config *ExecutorConfig) *CommandExe // Supports templated paths like /api/v1/policy/{id} matched against // concrete paths like /api/v1/policy/12345. func (e *CommandExecutor) FindOperation(method, path string) *RPCOperation { + return e.findOperationForMethod(method, path) +} + +func (e *CommandExecutor) findOperationForMethod(method, path string) *RPCOperation { key := strings.ToUpper(method) + ":" + path if op := e.operations[key]; op != nil { return op } - // Try template matching for paths with {param} segments for _, op := range e.operations { if !strings.EqualFold(op.Method, method) { continue @@ -89,6 +92,49 @@ func (e *CommandExecutor) FindOperation(method, path string) *RPCOperation { return op } } + + return nil +} + +func (e *CommandExecutor) FindLookupOperation(method, path string) *RPCOperation { + if strings.EqualFold(method, http.MethodHead) { + if op := e.findLookupOperationForMethod(http.MethodGet, path); op != nil { + return op + } + } + + for _, candidateMethod := range []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + http.MethodPatch, + } { + if op := e.findLookupOperationForMethod(candidateMethod, path); op != nil { + return op + } + } + + return nil +} + +func (e *CommandExecutor) findLookupOperationForMethod(method, path string) *RPCOperation { + preferredMethods := []string{ + method, + } + + for _, preferredMethod := range preferredMethods { + for i := range e.service.Operations { + op := &e.service.Operations[i] + if op.LookupFunc == nil || !strings.EqualFold(op.Method, preferredMethod) { + continue + } + if matchTemplatePath(op.Path, path) { + return op + } + } + } + return nil } diff --git a/rpc/filter_lookup_test.go b/rpc/filter_lookup_test.go new file mode 100644 index 00000000..386633a8 --- /dev/null +++ b/rpc/filter_lookup_test.go @@ -0,0 +1,167 @@ +package rpc + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/flanksource/clicky" + "github.com/flanksource/clicky/api" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type rpcFilterEntity struct { + ID string + Name string +} + +func (e rpcFilterEntity) GetID() string { return e.ID } +func (e rpcFilterEntity) GetName() string { return e.Name } + +type rpcFilterOpts struct { + Owner string `flag:"owner"` + Status string `flag:"status"` +} + +type rpcOwnerFilter struct{} + +func (rpcOwnerFilter) Key() string { return "owner" } +func (rpcOwnerFilter) Label() string { return "Owner" } + +func (rpcOwnerFilter) Lookup(opts *rpcFilterOpts) (map[string]api.Textable, error) { + if opts.Owner == "" { + return nil, nil + } + opts.Owner = "team/" + opts.Owner + return map[string]api.Textable{ + opts.Owner: api.Text{Content: strings.TrimPrefix(opts.Owner, "team/")}, + }, nil +} + +func (rpcOwnerFilter) Options(opts rpcFilterOpts) map[string]api.Textable { + return map[string]api.Textable{ + "team/platform": api.Text{Content: "Platform"}, + } +} + +type rpcStatusFilter struct{} + +func (rpcStatusFilter) Key() string { return "status" } +func (rpcStatusFilter) Label() string { return "Status" } + +func (rpcStatusFilter) Lookup(opts *rpcFilterOpts) (map[string]api.Textable, error) { + if opts.Status == "" { + return nil, nil + } + + opts.Status = "status:" + opts.Status + return map[string]api.Textable{ + opts.Status: api.Text{Content: "Healthy", Style: "font-semibold"}, + }, nil +} + +func (rpcStatusFilter) Options(opts rpcFilterOpts) map[string]api.Textable { + if opts.Owner == "team/platform" { + return map[string]api.Textable{ + "status:healthy": api.Text{Content: "Healthy", Style: "font-semibold"}, + } + } + return map[string]api.Textable{ + "status:healthy": api.Text{Content: "Healthy", Style: "font-semibold"}, + "status:degraded": api.Text{Content: "Degraded"}, + } +} + +func TestSwaggerServer_FilterLookupRoutes(t *testing.T) { + root := &cobra.Command{Use: "testapp", Short: "test app"} + + bulkExecutions := 0 + clicky.RegisterEntity(clicky.Entity[rpcFilterEntity, rpcFilterOpts]{ + Name: "rpc-filter-entity", + Filters: []clicky.Filter[rpcFilterOpts]{rpcOwnerFilter{}, rpcStatusFilter{}}, + List: func(opts rpcFilterOpts) ([]rpcFilterEntity, error) { + return []rpcFilterEntity{{ID: "1", Name: opts.Status}}, nil + }, + BulkActions: []clicky.BulkAction[rpcFilterEntity, rpcFilterOpts]{ + { + Name: "bulk-suspend", + Short: "Suspend matching entities", + Run: func(ids []string, flags map[string]string) (any, error) { + bulkExecutions++ + return ids, nil + }, + RunFilter: func(opts rpcFilterOpts, flags map[string]string) (any, error) { + bulkExecutions++ + return map[string]string{"status": opts.Status}, nil + }, + }, + }, + }) + clicky.GenerateCLI(root) + + server := NewSwaggerServer(&ServeConfig{ + Host: "localhost", + Port: 8080, + Executor: &ExecutorConfig{ + Enabled: true, + SkipPreRun: true, + PathPrefix: "/api/v1", + }, + }, root, &OpenAPIConfig{}) + + mux := http.NewServeMux() + server.RegisterRoutes(mux) + + t.Run("GET list lookup returns clicky metadata", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1/rpc-filter-entity?owner=platform&status=healthy&__lookup=filters", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json+clicky", w.Header().Get("Content-Type")) + + var payload map[string]any + err := json.Unmarshal(w.Body.Bytes(), &payload) + require.NoError(t, err) + + assert.Contains(t, w.Body.String(), `"status:healthy"`) + assert.NotContains(t, w.Body.String(), `"status:degraded"`) + }) + + t.Run("HEAD returns metadata headers with no body", func(t *testing.T) { + req := httptest.NewRequest(http.MethodHead, "/api/v1/rpc-filter-entity?owner=platform&status=healthy", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json+clicky", w.Header().Get("Content-Type")) + assert.Empty(t, w.Body.String()) + }) + + t.Run("GET companion lookup on bulk route does not execute action", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1/rpc-filter-entity/123/bulk-suspend?owner=platform&status=healthy&__lookup=filters", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, 0, bulkExecutions) + assert.Contains(t, w.Body.String(), `"status:healthy"`) + }) + + t.Run("GET companion route without lookup does not execute action", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1/rpc-filter-entity/123/bulk-suspend?owner=platform&status=healthy", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Equal(t, 0, bulkExecutions) + }) +} diff --git a/rpc/serve.go b/rpc/serve.go index f2eade98..e2052a99 100644 --- a/rpc/serve.go +++ b/rpc/serve.go @@ -407,33 +407,45 @@ func (s *SwaggerServer) registerExecutionRoutes(mux *http.ServeMux) { registered := make(map[string]string) routeCount := 0 - // Register a route for each operation - for _, op := range s.executor.service.Operations { - path := op.Path - method := strings.ToUpper(op.Method) - - // Replace spaces in path segments with hyphens - path = strings.ReplaceAll(path, " ", "-") - - // Sanitize path parameter names: Go's ServeMux requires alphanumeric wildcard names + registerRoute := func(method, path, opName string) bool { sanitized, ok := sanitizePathParams(path) if !ok { fmt.Printf("⚠️ Warning: Skipping route with invalid path params: %s %s\n", method, path) - continue + return false } - // Register the route with method prefix (Go 1.22+ ServeMux) pattern := method + " " + sanitized if existingOp, found := registered[pattern]; found { fmt.Printf("⚠️ Warning: Duplicate endpoint detected\n") fmt.Printf(" Path: %s\n", pattern) fmt.Printf(" Already registered by: %s\n", existingOp) - fmt.Printf(" Skipping: %s\n", op.Name) - continue + fmt.Printf(" Skipping: %s\n", opName) + return false } - registered[pattern] = op.Name + + registered[pattern] = opName mux.HandleFunc(pattern, s.handleExecuteCommand) routeCount++ + return true + } + + // Register a route for each operation + for _, op := range s.executor.service.Operations { + path := op.Path + method := strings.ToUpper(op.Method) + + // Replace spaces in path segments with hyphens + path = strings.ReplaceAll(path, " ", "-") + registerRoute(method, path, op.Name) + + if op.LookupFunc == nil { + continue + } + + registerRoute(http.MethodHead, path, op.Name+" lookup") + if !strings.EqualFold(method, http.MethodGet) { + registerRoute(http.MethodGet, path, op.Name+" lookup") + } } fmt.Printf("✅ Registered %d executor routes\n", routeCount) } @@ -539,7 +551,7 @@ func (s *SwaggerServer) executeCommandCore(r *http.Request) (any, *ExecutionResp func (s *SwaggerServer) handleExecuteCommand(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept") if r.Method == "OPTIONS" { @@ -547,6 +559,25 @@ func (s *SwaggerServer) handleExecuteCommand(w http.ResponseWriter, r *http.Requ return } + lookupRequested := isLookupRequest(r) + op := s.executor.FindOperation(r.Method, r.URL.Path) + if lookupRequested { + if op == nil || op.LookupFunc == nil { + op = s.executor.FindLookupOperation(r.Method, r.URL.Path) + } + if op == nil || op.LookupFunc == nil { + http.Error(w, fmt.Sprintf("No lookup found for %s %s", r.Method, r.URL.Path), http.StatusNotFound) + return + } + s.handleLookupCommand(w, r, op) + return + } + + if op == nil { + http.Error(w, fmt.Sprintf("No operation found for %s %s", r.Method, r.URL.Path), http.StatusNotFound) + return + } + // Execute command and get data + metadata data, metadata, statusCode, _ := s.executeCommandCore(r) @@ -568,6 +599,38 @@ func (s *SwaggerServer) handleExecuteCommand(w http.ResponseWriter, r *http.Requ s.writeFormattedResponse(w, data, opts, statusCode) } +func isLookupRequest(r *http.Request) bool { + if strings.EqualFold(r.Method, http.MethodHead) { + return true + } + return r.URL.Query().Get("__lookup") == "filters" +} + +func (s *SwaggerServer) handleLookupCommand(w http.ResponseWriter, r *http.Request, op *RPCOperation) { + req, err := s.executor.ExtractRequestFromHTTP(r, op) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to extract parameters: %v", err), http.StatusBadRequest) + return + } + + data, err := op.LookupFunc(req.Flags, req.Args) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json+clicky") + w.WriteHeader(http.StatusOK) + + if strings.EqualFold(r.Method, http.MethodHead) { + return + } + + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, fmt.Sprintf("failed to encode lookup response: %v", err), http.StatusInternalServerError) + } +} + // writeFormattedResponse formats data using the FormatManager and writes it as // the raw response body with the appropriate Content-Type. func (s *SwaggerServer) writeFormattedResponse(w http.ResponseWriter, data any, opts formatOptions, statusCode int) { @@ -618,7 +681,7 @@ func extractFormatOpts(r *http.Request) formatOptions { case "application/json": opts.Format = "json" return opts - case "application/clicky+json": + case "application/clicky+json", "application/json+clicky": opts.Format = "clicky-json" return opts case "application/yaml", "text/yaml", "application/x-yaml": @@ -650,7 +713,7 @@ func extractFormatOpts(r *http.Request) formatOptions { func formatToContentType(format string) string { switch format { case "clicky-json": - return "application/clicky+json" + return "application/json+clicky" case "yaml", "yml": return "application/yaml" case "csv": diff --git a/rpc/serve_test.go b/rpc/serve_test.go index 60065c4a..ee6cce15 100644 --- a/rpc/serve_test.go +++ b/rpc/serve_test.go @@ -406,6 +406,15 @@ func TestExtractFormatOpts_ClickyJSON(t *testing.T) { }(), want: "clicky-json", }, + { + name: "Accept: application/json+clicky", + req: func() *http.Request { + r := httptest.NewRequest("GET", "/x", nil) + r.Header.Set("Accept", "application/json+clicky") + return r + }(), + want: "clicky-json", + }, { name: "Accept list with clicky+json and weight", req: func() *http.Request { @@ -431,7 +440,7 @@ func TestExtractFormatOpts_ClickyJSON(t *testing.T) { } func TestFormatToContentType_ClickyJSON(t *testing.T) { - assert.Equal(t, "application/clicky+json", formatToContentType("clicky-json")) + assert.Equal(t, "application/json+clicky", formatToContentType("clicky-json")) assert.Equal(t, "text/html; charset=utf-8", formatToContentType("html-react")) assert.Equal(t, "application/json", formatToContentType("json")) } diff --git a/rpc/types.go b/rpc/types.go index 466a1b03..ccc970bb 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -17,6 +17,7 @@ type RPCOperation struct { Method string `json:"method,omitempty"` // HTTP method Tags []string `json:"tags,omitempty"` // For grouping DataFunc DataFunc `json:"-"` // Direct data provider, bypasses stdout capture + LookupFunc DataFunc `json:"-"` // Direct filter metadata provider } // RPCParameter represents a parameter in an RPC operation diff --git a/sub_command_test.go b/sub_command_test.go index 21dcfd37..b8bd29d3 100644 --- a/sub_command_test.go +++ b/sub_command_test.go @@ -1,6 +1,7 @@ package clicky import ( + "sync" "testing" "github.com/spf13/cobra" @@ -23,6 +24,8 @@ func resetEntityRegistry(t *testing.T) { entityRegistryMu.Lock() entityRegistry = nil entityRegistryMu.Unlock() + dataFuncRegistry = sync.Map{} + lookupFuncRegistry = sync.Map{} pendingSubCommandsMu.Lock() pendingSubCommands = nil pendingSubCommandsMu.Unlock() diff --git a/task/group.go b/task/group.go index 9288e8a5..733a6ed6 100644 --- a/task/group.go +++ b/task/group.go @@ -214,7 +214,7 @@ func (g *TypedGroup[T]) WaitFor() *WaitResult { result.Duration = g.Duration() // For plain render mode, force a final render - if g.manager != nil && g.manager.noProgress.Load() { + if g.manager != nil && g.manager.noProgress.Load() && !g.manager.noRender.Load() { g.manager.PlainRender() } diff --git a/task/manager.go b/task/manager.go index 1dd7f0be..948c4cd3 100644 --- a/task/manager.go +++ b/task/manager.go @@ -15,6 +15,7 @@ import ( "github.com/flanksource/commons/collections" flanksourceContext "github.com/flanksource/commons/context" "github.com/flanksource/commons/logger" + "github.com/google/uuid" "golang.org/x/sync/semaphore" "golang.org/x/term" ) @@ -37,6 +38,7 @@ type Manager struct { onInterrupt func() // optional cleanup callback noColor atomic.Bool // Disable colored output noProgress atomic.Bool // Disable progress display + noRender atomic.Bool // Disable all task rendering // Priority queue for task scheduling taskQueue *collections.Queue[*Task] @@ -52,6 +54,7 @@ type Manager struct { renderDone chan struct{} renderStopped sync.Once renderStarted sync.Once + renderOwnsTTY bool // Terminal state originalTermState *term.State @@ -266,6 +269,16 @@ func SetNoProgress(noProgress bool) { global.noProgress.Store(noProgress) } +// SetNoRender enables or disables all task rendering, including final summaries. +func SetNoRender(noRender bool) { + global.noRender.Store(noRender) +} + +// IsNoRender reports whether task rendering is currently disabled. +func IsNoRender() bool { + return global.noRender.Load() +} + // SetMaxConcurrent sets the maximum number of concurrent tasks func SetMaxConcurrent(max int) { global.mu.Lock() @@ -315,6 +328,7 @@ func (tm *Manager) newTask(name string, opts ...Option) *Task { task := &Task{ name: name, + id: uuid.NewString(), status: StatusPending, progress: 0, maxValue: 100, @@ -355,11 +369,13 @@ func (tm *Manager) newTask(name string, opts ...Option) *Task { } func (tm *Manager) enqueue(task *Task) *Task { - tm.renderStarted.Do(func() { - if !tm.noProgress.Load() { - tm.startRenderLoop() - } - }) + if !tm.noRender.Load() { + tm.renderStarted.Do(func() { + if !tm.noProgress.Load() { + tm.startRenderLoop() + } + }) + } if task.identity != "" { if existing, ok := tm.tasksByIdentity.Load(task.identity); ok { diff --git a/task/manager_lifecycle.go b/task/manager_lifecycle.go index 27197cf6..b47aeda5 100644 --- a/task/manager_lifecycle.go +++ b/task/manager_lifecycle.go @@ -12,9 +12,17 @@ import ( // startRenderLoop starts the unified render loop for both interactive and non-interactive modes. // Must not be called concurrently with stopRender. func (tm *Manager) startRenderLoop() { + if tm.noRender.Load() { + return + } + if tm.isInteractive && !globalANSITerminal.tryAcquireTaskRenderer(tm) { + return + } + tm.mu.Lock() tm.stopRenderCh = make(chan struct{}) tm.renderDone = make(chan struct{}) + tm.renderOwnsTTY = tm.isInteractive tm.mu.Unlock() go tm.renderLoop() } @@ -27,6 +35,7 @@ func (tm *Manager) renderLoop() { defer func() { if r := recover(); r != nil { tm.cleanupTerminal() + tm.releaseRenderTerminal() panic(r) } }() @@ -71,14 +80,17 @@ func (tm *Manager) stopRender() { close(ch) <-done } - tm.renderFinal() + if !tm.noRender.Load() { + tm.renderFinal() + } tm.cleanupTerminal() + tm.releaseRenderTerminal() }) } // cleanupTerminal restores terminal to a clean state func (tm *Manager) cleanupTerminal() { - if !tm.isInteractive || tm.noProgress.Load() { + if !tm.isInteractive || tm.noProgress.Load() || !tm.ownsRenderTerminal() { return } output := tm.renderer.Output() @@ -93,6 +105,9 @@ func (tm *Manager) cleanupTerminal() { // renderFinal outputs the final task status func (tm *Manager) renderFinal() { + if tm.noRender.Load() { + return + } tm.mu.RLock() if len(tm.tasks) == 0 { tm.mu.RUnlock() @@ -109,3 +124,20 @@ func (tm *Manager) renderFinal() { fmt.Fprintln(os.Stderr, rendered.ANSI()) } } + +func (tm *Manager) ownsRenderTerminal() bool { + tm.mu.RLock() + defer tm.mu.RUnlock() + return tm.renderOwnsTTY +} + +func (tm *Manager) releaseRenderTerminal() { + tm.mu.Lock() + ownsTTY := tm.renderOwnsTTY + tm.renderOwnsTTY = false + tm.mu.Unlock() + + if ownsTTY { + globalANSITerminal.releaseTaskRenderer(tm) + } +} diff --git a/task/manager_wait.go b/task/manager_wait.go index cc627146..19b31e0a 100644 --- a/task/manager_wait.go +++ b/task/manager_wait.go @@ -35,6 +35,36 @@ func CancelAll() { } } +// StopTask cancels a specific pending or running task by immutable ID. +func StopTask(id string) bool { + if global == nil || id == "" { + return false + } + + global.mu.RLock() + tasks := make([]*Task, len(global.tasks)) + copy(tasks, global.tasks) + global.mu.RUnlock() + + for _, task := range tasks { + if task == nil || task.ID() != id { + continue + } + + task.mu.Lock() + runnable := task.status == StatusPending || task.status == StatusRunning + task.mu.Unlock() + if !runnable { + return false + } + + task.Cancel() + return true + } + + return false +} + // ClearTasks removes all completed tasks from the task list func ClearTasks() { global.mu.Lock() @@ -157,7 +187,7 @@ func Debug() string { defer global.mu.RUnlock() var result string - result += fmt.Sprintf("Task Manager: {no-color=%v, no-progress=%v, workers=%v}\n", global.noColor.Load(), global.noProgress.Load(), global.workersActive.Load()) + result += fmt.Sprintf("Task Manager: {no-color=%v, no-progress=%v, no-render=%v, workers=%v}\n", global.noColor.Load(), global.noProgress.Load(), global.noRender.Load(), global.workersActive.Load()) result += fmt.Sprintf(" Total Tasks: %d\n", len(global.tasks)) result += fmt.Sprintf(" Active Workers: %d\n", global.workersActive.Load()) result += " Task Details:\n" @@ -201,7 +231,7 @@ func WaitForAllTasks() { } // For plain render mode, force a final render - if global.noProgress.Load() { + if global.noProgress.Load() && !global.noRender.Load() { global.PlainRender() } } diff --git a/task/no_render_test.go b/task/no_render_test.go new file mode 100644 index 00000000..494724d0 --- /dev/null +++ b/task/no_render_test.go @@ -0,0 +1,59 @@ +package task + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/flanksource/clicky/text" + flanksourceContext "github.com/flanksource/commons/context" +) + +func TestNoRenderSuppressesFinalOutput(t *testing.T) { + originalStderr := os.Stderr + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("failed to create stderr pipe: %v", err) + } + t.Cleanup(func() { + os.Stderr = originalStderr + _ = w.Close() + _ = r.Close() + }) + os.Stderr = w + + var output bytes.Buffer + done := make(chan struct{}) + go func() { + _, _ = io.Copy(&output, r) + close(done) + }() + + manager := newTestManager(1) + manager.noRender.Store(true) + + task := manager.newTask("hidden") + task.runFunc = func(flanksourceContext.Context, *Task) error { + time.Sleep(20 * time.Millisecond) + return nil + } + manager.enqueue(task) + + select { + case <-task.doneChan: + case <-time.After(2 * time.Second): + t.Fatal("timeout waiting for task completion") + } + + manager.stopRender() + + _ = w.Close() + <-done + + if got := strings.TrimSpace(text.StripANSI(output.String())); got != "" { + t.Fatalf("expected no rendered task output, got %q", got) + } +} diff --git a/task/options.go b/task/options.go index 808dda0f..fdfe8b09 100644 --- a/task/options.go +++ b/task/options.go @@ -104,6 +104,7 @@ func WithIdentity(identity string) Option { // ManagerOptions contains configuration options for TaskManager type ManagerOptions struct { NoProgress bool // Disable progress display + NoRender bool // Disable all task rendering MaxConcurrent int // Maximum concurrent tasks (0 = unlimited) GracefulTimeout time.Duration // Timeout for graceful shutdown @@ -116,6 +117,7 @@ type ManagerOptions struct { func DefaultManagerOptions() *ManagerOptions { return &ManagerOptions{ NoProgress: false, + NoRender: false, MaxConcurrent: 1, GracefulTimeout: 10 * time.Second, MaxRetries: 3, @@ -126,6 +128,7 @@ func DefaultManagerOptions() *ManagerOptions { // Apply configures a TaskManager with these options func (opts *ManagerOptions) Apply() { SetNoProgress(opts.NoProgress) + SetNoRender(opts.NoRender) SetMaxConcurrent(opts.MaxConcurrent) SetGracefulTimeout(opts.GracefulTimeout) diff --git a/task/snapshot.go b/task/snapshot.go index 68cc7f49..8c7dbbab 100644 --- a/task/snapshot.go +++ b/task/snapshot.go @@ -29,7 +29,7 @@ type TaskSnapshot struct { // SnapshotTask creates a TaskSnapshot from a Task. func SnapshotTask(t *Task, groupName string) TaskSnapshot { snap := TaskSnapshot{ - ID: t.Name(), + ID: t.ID(), Name: t.Name(), Type: "task", Group: groupName, diff --git a/task/stop_test.go b/task/stop_test.go new file mode 100644 index 00000000..0ccfb89c --- /dev/null +++ b/task/stop_test.go @@ -0,0 +1,113 @@ +package task + +import ( + "sync/atomic" + "testing" + "time" + + flanksourceContext "github.com/flanksource/commons/context" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSnapshotTaskIDIsStableAcrossRename(t *testing.T) { + tm := newTestManager(1) + task := tm.newTask("first-name", WithIdentity("dedupe-key")) + + require.NotEmpty(t, task.ID()) + assert.NotEqual(t, "dedupe-key", task.ID()) + + before := task.ID() + task.SetName("second-name") + after := task.ID() + + assert.Equal(t, before, after) + assert.Equal(t, before, SnapshotTask(task, "group").ID) +} + +func TestStopTaskCancelsRunningTaskByID(t *testing.T) { + originalGlobal := global + global = newTestManager(1) + t.Cleanup(func() { + global.stopRender() + global = originalGlobal + }) + + started := make(chan struct{}) + task := StartTask[string]("blocking", func(ctx flanksourceContext.Context, _ *Task) (string, error) { + close(started) + <-ctx.Done() + return "", ctx.Err() + }) + + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("task did not start") + } + + require.True(t, StopTask(task.ID())) + + wait := task.WaitFor() + assert.Equal(t, StatusCancelled, wait.Status) +} + +func TestStopTaskCancelsPendingTaskByID(t *testing.T) { + originalGlobal := global + global = newTestManager(1) + t.Cleanup(func() { + global.stopRender() + global = originalGlobal + }) + + started := make(chan struct{}) + release := make(chan struct{}) + first := StartTask[string]("first", func(ctx flanksourceContext.Context, _ *Task) (string, error) { + close(started) + select { + case <-release: + return "done", nil + case <-ctx.Done(): + return "", ctx.Err() + } + }) + + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("first task did not start") + } + + var secondRan atomic.Bool + second := StartTask[string]("second", func(flanksourceContext.Context, *Task) (string, error) { + secondRan.Store(true) + return "done", nil + }) + + require.True(t, StopTask(second.ID())) + close(release) + + first.WaitFor() + wait := second.WaitFor() + assert.Equal(t, StatusCancelled, wait.Status) + assert.False(t, secondRan.Load()) +} + +func TestStopTaskReturnsFalseForUnknownOrCompletedTask(t *testing.T) { + originalGlobal := global + global = newTestManager(1) + t.Cleanup(func() { + global.stopRender() + global = originalGlobal + }) + + task := StartTask[string]("done", func(flanksourceContext.Context, *Task) (string, error) { + return "ok", nil + }) + wait := task.WaitFor() + require.Equal(t, StatusSuccess, wait.Status) + + assert.False(t, StopTask("")) + assert.False(t, StopTask("missing")) + assert.False(t, StopTask(task.ID())) +} diff --git a/task/task.go b/task/task.go index f44b2f77..8c69f8df 100644 --- a/task/task.go +++ b/task/task.go @@ -200,6 +200,7 @@ type Task struct { name string description string modelName string + id string prompt string identity string // Unique identifier for task deduplication @@ -257,6 +258,11 @@ func (t *Task) Identity() string { return t.identity } +// ID returns the task's immutable UUID. +func (t *Task) ID() string { + return t.id +} + // Context returns the task's context for cancellation func (t *Task) Context() context.Context { return t.ctx @@ -276,6 +282,7 @@ func (t *Task) Cancel() { if t.cancel != nil { t.cancel() } + t.dirty.Store(true) t.signalDone() // Signal task completion t.mu.Unlock() } else { diff --git a/task/terminal_owner.go b/task/terminal_owner.go new file mode 100644 index 00000000..20d72785 --- /dev/null +++ b/task/terminal_owner.go @@ -0,0 +1,131 @@ +package task + +import "sync" + +type ansiTerminalOwner string + +const ( + ansiTerminalOwnerNone ansiTerminalOwner = "" + ansiTerminalOwnerTaskRenderer ansiTerminalOwner = "task-renderer" + ansiTerminalOwnerPrompt ansiTerminalOwner = "prompt" +) + +type ansiTerminalController struct { + mu sync.Mutex + cond *sync.Cond + owner ansiTerminalOwner + manager *Manager + promptWaiters int +} + +func newANSITerminalController() *ansiTerminalController { + controller := &ansiTerminalController{} + controller.cond = sync.NewCond(&controller.mu) + return controller +} + +var globalANSITerminal = newANSITerminalController() + +func (c *ansiTerminalController) tryAcquireTaskRenderer(manager *Manager) bool { + c.mu.Lock() + defer c.mu.Unlock() + + if c.owner != ansiTerminalOwnerNone || c.promptWaiters > 0 { + return false + } + + c.owner = ansiTerminalOwnerTaskRenderer + c.manager = manager + return true +} + +func (c *ansiTerminalController) beginPromptAcquire() *Manager { + c.mu.Lock() + defer c.mu.Unlock() + + c.promptWaiters++ + if c.owner == ansiTerminalOwnerTaskRenderer { + return c.manager + } + + return nil +} + +func (c *ansiTerminalController) cancelPromptAcquire() { + c.mu.Lock() + defer c.mu.Unlock() + + if c.promptWaiters == 0 { + return + } + + c.promptWaiters-- + if c.promptWaiters == 0 { + c.cond.Broadcast() + } +} + +func (c *ansiTerminalController) acquirePrompt() { + c.mu.Lock() + defer c.mu.Unlock() + + for c.owner != ansiTerminalOwnerNone { + c.cond.Wait() + } + + c.owner = ansiTerminalOwnerPrompt + c.manager = nil + if c.promptWaiters > 0 { + c.promptWaiters-- + } +} + +func (c *ansiTerminalController) releaseTaskRenderer(manager *Manager) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.owner != ansiTerminalOwnerTaskRenderer || c.manager != manager { + return + } + + c.owner = ansiTerminalOwnerNone + c.manager = nil + c.cond.Broadcast() +} + +func (c *ansiTerminalController) releasePrompt() { + c.mu.Lock() + defer c.mu.Unlock() + + if c.owner != ansiTerminalOwnerPrompt { + return + } + + c.owner = ansiTerminalOwnerNone + c.manager = nil + c.cond.Broadcast() +} + +// AcquirePromptTerminal waits for exclusive ANSI terminal ownership for prompt +// rendering, stopping the active task renderer first when necessary. +// The second return value reports whether the prompt displaced an active task renderer. +func AcquirePromptTerminal() (func(), bool) { + manager := globalANSITerminal.beginPromptAcquire() + acquired := false + defer func() { + if !acquired { + globalANSITerminal.cancelPromptAcquire() + } + }() + + if manager != nil { + manager.stopRender() + } + + globalANSITerminal.acquirePrompt() + acquired = true + + return func() { + globalANSITerminal.releasePrompt() + }, manager != nil +} diff --git a/task/worker.go b/task/worker.go index 5739156f..d28dbb19 100644 --- a/task/worker.go +++ b/task/worker.go @@ -32,6 +32,18 @@ func (w *worker) run() { continue } + task.mu.Lock() + skip := task.status == StatusCancelled + task.mu.Unlock() + if skip { + task.completed.Store(true) + task.signalDone() + if task.identity != "" { + w.manager.tasksByIdentity.Delete(task.identity) + } + continue + } + // Check dependencies if !w.checkDependencies(task) { // Dependencies not met, re-enqueue with delay diff --git a/testdata/prompt_test_helper.go b/testdata/prompt_test_helper.go new file mode 100644 index 00000000..68159dde --- /dev/null +++ b/testdata/prompt_test_helper.go @@ -0,0 +1,103 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + flanksourceContext "github.com/flanksource/commons/context" + + "github.com/flanksource/clicky" + "github.com/flanksource/clicky/task" +) + +func main() { + mode := os.Getenv("PROMPT_MODE") + if mode == "" { + fmt.Fprintln(os.Stderr, "PROMPT_MODE not set") + os.Exit(2) + } + + task.SetNoColor(true) + task.SetNoProgress(false) + + switch mode { + case "choose_confirm": + runChoose() + case "choose_cancel": + runChoose() + case "multi_confirm": + runMultiChoose() + case "text_validate": + runValidatedText() + case "secret": + runSecretText() + case "task_takeover": + runTaskTakeover() + default: + fmt.Fprintf(os.Stderr, "unknown PROMPT_MODE: %s\n", mode) + os.Exit(2) + } +} + +func runChoose() { + value, ok := clicky.PromptSelect([]string{"alpha", "beta", "gamma"}, clicky.PromptSelectOptions[string]{ + Title: "Pick one", + }) + fmt.Fprintf(os.Stderr, "\nRESULT=%s OK=%v\n", value, ok) +} + +func runValidatedText() { + value, ok := clicky.PromptText(clicky.PromptTextOptions{ + Title: "Name", + Validate: func(value string) error { + if strings.TrimSpace(value) == "" { + return errors.New("value required") + } + return nil + }, + }) + fmt.Fprintf(os.Stderr, "\nRESULT=%s OK=%v\n", value, ok) +} + +func runMultiChoose() { + values, ok := clicky.PromptMultiSelect([]string{"alpha", "beta", "gamma"}, clicky.PromptMultiSelectOptions[string]{ + Title: "Pick checks", + Limit: 2, + }) + fmt.Fprintf(os.Stderr, "\nRESULT=%s OK=%v\n", strings.Join(values, ","), ok) +} + +func runSecretText() { + value, ok := clicky.PromptText(clicky.PromptTextOptions{ + Title: "Secret", + Secret: true, + }) + fmt.Fprintf(os.Stderr, "\nRESULT_LEN=%d OK=%v\n", len(value), ok) +} + +func runTaskTakeover() { + release := make(chan struct{}) + clicky.StartTask[string]("rendering", func(ctx flanksourceContext.Context, t *clicky.Task) (string, error) { + t.SetDescription("before prompt TAKEOVER-MARKER") + select { + case <-release: + t.SetDescription("after prompt TAKEOVER-MARKER") + return "done", nil + case <-ctx.Done(): + return "", ctx.Err() + } + }) + + time.Sleep(600 * time.Millisecond) + + value, ok := clicky.PromptSelect([]string{"continue-marker", "stop-marker"}, clicky.PromptSelectOptions[string]{ + Title: "Take over terminal", + }) + + close(release) + _ = clicky.WaitForGlobalCompletion() + fmt.Fprintf(os.Stderr, "\nRESULT=%s OK=%v\n", value, ok) +}