package commandkit import ( "bytes" "errors" "fmt" "io" "runtime/debug" internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra" commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta" yaml "go.yaml.in/yaml/v4" ) const commandKitModulePath = "gitea.maximumdirect.net/eric/CommandKit" type helpInput struct { Targets Supplied[[]string] } type versionInput struct { Scope string Format string } type versionDocument struct { Name string `yaml:"name"` Version string `yaml:"version"` Revision string `yaml:"revision"` } func (a *App) builtinCommands() []Command { help := captureCommand(CommandSpec[helpInput]{ Name: "help", Summary: "Show help for commands.", Description: "Describe registered commands and their inputs.", Arguments: []Argument[helpInput]{RepeatArgument(ArgumentMeta{Name: "COMMAND", Usage: "registered command path"}, 0, -1, func(value string) (string, error) { return value, nil }, func(input *helpInput, value Supplied[[]string]) { input.Targets = value })}, }, func(current commandExecution, input helpInput) Outcome { return current.renderHelpTargets(input.Targets.Value) }) scopeDefault := "app" formatDefault := "text" version := NewCommand(CommandSpec[versionInput]{ Name: "version", Summary: "Show application or CommandKit version.", Description: "Report identity and build metadata without loading application configuration.", Flags: []Flag[versionInput]{ ValueFlag(FlagMeta{Name: "scope", Usage: "version subject"}, &scopeDefault, parseVersionScope, func(input *versionInput, value Supplied[string]) { input.Scope = value.Value }), ValueFlag(FlagMeta{Name: "format", Usage: "output format"}, &formatDefault, parseVersionFormat, func(input *versionInput, value Supplied[string]) { input.Format = value.Value }), }, Run: func(invocation Invocation, input versionInput) Outcome { identity := a.identity if input.Scope == "commandkit" { identity = commandKitBuildIdentity(readBuildInfo()) } if identity.Version == "" { identity.Version = "dev" } return renderVersionOutcome(identity, input.Format, invocation.Streams) }, }) return []Command{help, version} } func parseVersionScope(value string) (string, error) { if value != "app" && value != "commandkit" { return "", fmt.Errorf("scope must be app or commandkit, got %q", value) } return value, nil } func parseVersionFormat(value string) (string, error) { if value != "text" && value != "yaml" { return "", fmt.Errorf("format must be text or yaml, got %q", value) } return value, nil } func (a *App) renderHelpOutcome(path []string, streams Streams) Outcome { session := internalcobra.Prepare(a.routeSnapshot().metadata, commandmeta.ParseOptions{}) return a.renderHelpOutcomeWithSession(path, streams, session) } func (a *App) renderHelpTargets(targets []string, streams Streams) Outcome { session := internalcobra.Prepare(a.routeSnapshot().metadata, commandmeta.ParseOptions{}) return a.renderHelpTargetsWithSession(targets, streams, session) } func (a *App) renderHelpTargetsWithSession(targets []string, streams Streams, session *internalcobra.Session) Outcome { return a.renderHelpOutcomeWithSession(targets, streams, session) } func (a *App) renderHelpOutcomeWithSession(path []string, streams Streams, session *internalcobra.Session) Outcome { content, failure := session.RenderHelp(path) if failure != nil { if failure.Usage { return Usage(errors.New(failure.Message)) } return Failure(errors.New(failure.Message)) } if err := writeReport(streams.Out, []byte(content)); err != nil { return incompleteReport(err) } return Success() } func renderVersionOutcome(identity Identity, format string, streams Streams) Outcome { var content []byte switch format { case "text": name := identity.Name if identity.Version == "" { identity.Version = "dev" } line := name + " " + identity.Version if identity.Revision != "" { line += " (" + identity.Revision + ")" } content = []byte(line + "\n") case "yaml": if identity.Version == "" { identity.Version = "dev" } var buffer bytes.Buffer if err := encodeYAML(&buffer, versionDocument{Name: identity.Name, Version: identity.Version, Revision: identity.Revision}); err != nil { return Failure(err) } content = buffer.Bytes() default: return Usage(fmt.Errorf("unsupported version format %q", format)) } if err := writeReport(streams.Out, content); err != nil { return incompleteReport(err) } return Success() } func encodeYAML(writer io.Writer, document any) error { encoder := yaml.NewEncoder(writer) encoder.SetIndent(2) return encoder.Encode(document) } func writeReport(writer io.Writer, content []byte) error { checked := &checkedWriter{writer: writer} _, err := checked.Write(content) if err == nil { err = checked.failure } return err } func incompleteReport(err error) Outcome { outcome := Failure(err) outcome.ReportIncomplete = true outcome.DeliveryError = err return outcome } func readBuildInfo() *debug.BuildInfo { info, ok := debug.ReadBuildInfo() if !ok { return nil } return info } func commandKitBuildIdentity(info *debug.BuildInfo) Identity { identity := Identity{Name: "CommandKit", Version: "dev"} if info == nil { return identity } var module *debug.Module if info.Main.Path == commandKitModulePath { module = &info.Main } else { for _, dependency := range info.Deps { if dependency != nil && dependency.Path == commandKitModulePath { module = dependency break } } } if module == nil { return identity } version := module.Version if module.Replace != nil { version = module.Replace.Version } if version != "" && version != "(devel)" { identity.Version = version } return identity }