215 lines
6.5 KiB
Go
215 lines
6.5 KiB
Go
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() []Declaration {
|
|
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 []Declaration{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 {
|
|
snapshot := a.routeSnapshot()
|
|
route := resolveEnteredRoute(targets, snapshot)
|
|
if route.kind == routeUnknown {
|
|
return a.renderHelpOutcomeWithSession(targets, streams, session)
|
|
}
|
|
options := commandmeta.ParseOptions{}
|
|
switch route.kind {
|
|
case routeCanonical:
|
|
options.FlagAliases = snapshot.flagAliases[route.canonicalPath.Key()].neutralRecords()
|
|
case routeDiscovery:
|
|
options.FlagAliases = snapshot.discoveryFlags[route.discoveryPath.Key()].neutralRecords()
|
|
}
|
|
session = internalcobra.Prepare(snapshot.metadata, options)
|
|
return a.renderHelpViewOutcomeWithSession(a.helpView(route, snapshot), streams, session)
|
|
}
|
|
|
|
func (a *App) renderHelpOutcomeWithSession(path []string, streams Streams, session *internalcobra.Session) Outcome {
|
|
return a.renderHelpViewOutcomeWithSession(commandmeta.HelpView{EnteredPath: path, CanonicalPath: path}, streams, session)
|
|
}
|
|
|
|
func (a *App) renderHelpViewOutcomeWithSession(view commandmeta.HelpView, streams Streams, session *internalcobra.Session) Outcome {
|
|
content, failure := session.RenderHelpView(view)
|
|
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
|
|
}
|