Add generated help and version commands
This commit is contained in:
208
builtins.go
Normal file
208
builtins.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||
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) installBuiltins() error {
|
||||
help, err := NewCommand(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 })},
|
||||
Run: func(invocation Invocation, input helpInput) Outcome {
|
||||
target, err := a.helpCommandTarget(input.Targets.Value)
|
||||
if err != nil {
|
||||
return Usage(err)
|
||||
}
|
||||
return a.renderHelpOutcome(target, invocation.Streams)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create built-in help command: %w", err)
|
||||
}
|
||||
scopeDefault := "app"
|
||||
formatDefault := "text"
|
||||
version, err := 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)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create built-in version command: %w", err)
|
||||
}
|
||||
a.commands["help"] = &commandNode{command: help, reserved: true}
|
||||
a.commands["version"] = &commandNode{command: version, reserved: true}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
if len(path) > 0 {
|
||||
if _, ok := a.commandAt(path); !ok {
|
||||
return Usage(fmt.Errorf("unknown help path %q", strings.Join(path, " ")))
|
||||
}
|
||||
}
|
||||
content, failure := internalcobra.RenderHelp(a.bridgeTree(), path)
|
||||
if failure != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (a *App) helpCommandTarget(targets []string) ([]string, error) {
|
||||
if len(targets) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if _, ok := a.commandAt(targets); !ok {
|
||||
return nil, fmt.Errorf("unknown help path %q", strings.Join(targets, " "))
|
||||
}
|
||||
return append([]string(nil), targets...), nil
|
||||
}
|
||||
Reference in New Issue
Block a user