Add native configuration display
This commit is contained in:
@@ -6,8 +6,8 @@ and shared command infrastructure across related applications.
|
||||
The current Go library provides an instance-owned command registry, typed flag
|
||||
and operand bindings, Cobra-backed routing, invocation outcomes, generated help,
|
||||
application or CommandKit version reporting, configuration scope discovery, and
|
||||
ConfigKit-backed validation. Effective display, source and explanation reports,
|
||||
and PromptKit commands remain planned.
|
||||
ConfigKit-backed validation and native effective YAML display. Source and
|
||||
explanation reports and PromptKit commands remain planned.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
12
config.go
12
config.go
@@ -131,8 +131,8 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
||||
return fmt.Errorf("command path %q is already registered", "config")
|
||||
}
|
||||
|
||||
commands := make([]Command, 0, 2)
|
||||
installed := make([]ConfigOperation, 0, 1)
|
||||
commands := make([]Command, 0, 3)
|
||||
installed := make([]ConfigOperation, 0, 2)
|
||||
if configOperationDeclared(normalized.Scopes, ConfigValidate) {
|
||||
validate, err := newConfigValidateCommand(normalized)
|
||||
if err != nil {
|
||||
@@ -141,6 +141,14 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
||||
commands = append(commands, validate)
|
||||
installed = append(installed, ConfigValidate)
|
||||
}
|
||||
if configOperationDeclared(normalized.Scopes, ConfigShow) {
|
||||
show, err := newConfigShowCommand(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commands = append(commands, show)
|
||||
installed = append(installed, ConfigShow)
|
||||
}
|
||||
command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
|
||||
if outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("text inventory outcome = %#v", outcome)
|
||||
}
|
||||
wantText := "app: validate\npromptkit:\nmetadata:\nzeta:\n"
|
||||
wantText := "app: validate, show\npromptkit:\nmetadata:\nzeta: show\n"
|
||||
if textOutput.String() != wantText {
|
||||
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,129 @@ import (
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type effectiveYAML string
|
||||
|
||||
func (document effectiveYAML) WriteEffective(writer io.Writer) error {
|
||||
_, err := io.WriteString(writer, string(document))
|
||||
return err
|
||||
}
|
||||
|
||||
func TestConfigShowWritesNativeSingleScopeAndFramesAggregateYAML(t *testing.T) {
|
||||
const native = "%YAML 1.1\n# authored comment\n---\nvalue: null\nitems: []\nmissing: {}\n"
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var calls []string
|
||||
suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "zeta")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("%YAML 1.1\n# zeta comment\nvalue: zeta\n")}}, nil
|
||||
}},
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "app")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML(native)}}, nil
|
||||
}},
|
||||
}}
|
||||
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var single bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "show"}, commandkit.Streams{Out: &single, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || single.String() != native {
|
||||
t.Fatalf("single show outcome=%#v output=%q", outcome, single.String())
|
||||
}
|
||||
calls = nil
|
||||
outcome = app.Execute(context.Background(), []string{"config", "show", "--format", "text"}, commandkit.Streams{Out: &single, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || len(calls) != 0 {
|
||||
t.Fatalf("text format outcome=%#v acquisitions=%v", outcome, calls)
|
||||
}
|
||||
|
||||
calls = nil
|
||||
var aggregate bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &aggregate, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(calls, []string{"app", "zeta"}) {
|
||||
t.Fatalf("aggregate outcome=%#v acquisitions=%v", outcome, calls)
|
||||
}
|
||||
if !strings.Contains(aggregate.String(), "%YAML 1.1\n# authored comment\n---\n# scope: app\n") || !strings.Contains(aggregate.String(), "...\n%YAML 1.1\n# zeta comment\n---\n# scope: zeta\nvalue: zeta\n") {
|
||||
t.Fatalf("aggregate framing = %q", aggregate.String())
|
||||
}
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(aggregate.Bytes()))
|
||||
var first, second map[string]any
|
||||
if err := decoder.Decode(&first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := decoder.Decode(&second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
t.Fatalf("third YAML document decode error = %v, value=%#v", err, extra)
|
||||
}
|
||||
if first["value"] != nil || first["items"] == nil || first["missing"] == nil || second["value"] != "zeta" {
|
||||
t.Fatalf("decoded documents = %#v and %#v", first, second)
|
||||
}
|
||||
if _, exists := first["absent"]; exists {
|
||||
t.Fatalf("absent field was added to native document: %#v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigShowAcquiresAllBeforeOutputAndTracksWriterFailures(t *testing.T) {
|
||||
failure := errors.New("second scope failed")
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
calls := 0
|
||||
suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil
|
||||
}},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "provider warning")}}, failure
|
||||
}},
|
||||
}}
|
||||
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if !errors.Is(outcome.Cause, failure) || outcome.ReportIncomplete || output.Len() != 0 || calls != 2 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != configkit.SeverityWarning {
|
||||
t.Fatalf("acquisition failure outcome=%#v output=%q calls=%d", outcome, output.String(), calls)
|
||||
}
|
||||
|
||||
app, _ = commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil
|
||||
}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrClosedPipe) || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) || len(partial.written) == 0 {
|
||||
t.Fatalf("writer failure outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
type configKitShowPartialWriter struct {
|
||||
written []byte
|
||||
failure error
|
||||
}
|
||||
|
||||
func (writer *configKitShowPartialWriter) Write(data []byte) (int, error) {
|
||||
n := len(data)
|
||||
if n > 4 {
|
||||
n = 4
|
||||
}
|
||||
writer.written = append(writer.written, data[:n]...)
|
||||
return n, writer.failure
|
||||
}
|
||||
|
||||
func TestConfigKitScopeUsesDefaultsOverlayAndRecordedResolution(t *testing.T) {
|
||||
type configuration struct {
|
||||
Mode string `yaml:"mode"`
|
||||
|
||||
22
docs/cli.md
22
docs/cli.md
@@ -125,6 +125,28 @@ empty value was explicitly supplied, so a scope callback can map those inputs to
|
||||
its own ConfigKit load policy. Help and invalid syntax do not call scope
|
||||
providers.
|
||||
|
||||
### Show effective configuration
|
||||
|
||||
`config show` writes the provider's native effective YAML for one scope. Its
|
||||
default scope is `app`; `--format yaml` is accepted and YAML is the only initial
|
||||
format. Single-scope output is passed through unchanged, without a CommandKit
|
||||
header or document marker.
|
||||
|
||||
```text
|
||||
example config show
|
||||
example config show --scope promptkit
|
||||
example config show --scope all
|
||||
```
|
||||
|
||||
`--scope all` acquires every selected scope and checks each effective writer
|
||||
before writing output. It then emits one YAML document per scope in discovery
|
||||
order, with a `# scope: NAME` comment after each document marker. Existing native
|
||||
markers and leading comments are retained; documents without a marker receive
|
||||
one. Scope labels containing line control characters are quoted to keep them on
|
||||
one comment line. A provider failure leaves the output stream empty. A rendering
|
||||
or output failure may leave an aggregate prefix, and the outcome reports
|
||||
incomplete report delivery.
|
||||
|
||||
## Outcome status
|
||||
|
||||
CommandKit usage failures use status `2`; operational failures use status `1`.
|
||||
|
||||
@@ -4,9 +4,9 @@ CommandKit currently contains adopted repository policies, accepted architectura
|
||||
decisions, implementation plans, identity and outcome primitives, typed command
|
||||
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
||||
adapter, public `App.Execute` coordination, and generated help/version commands.
|
||||
Configuration suite registration, scope discovery, ConfigKit integration, and
|
||||
validation are implemented; other configuration and PromptKit operations remain
|
||||
pending. The
|
||||
Configuration suite registration, scope discovery, ConfigKit integration,
|
||||
validation, and native effective YAML display are implemented; other
|
||||
configuration and PromptKit operations remain pending. The
|
||||
[README](../README.md) provides project orientation; the
|
||||
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
||||
canonical documentation owners.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Initial CommandKit implementation plan
|
||||
|
||||
Status: implementation underway; Stages 1-8 complete and Stages 9-14 pending.
|
||||
Status: implementation underway; Stages 1-9 complete and Stages 10-14 pending.
|
||||
|
||||
Date: 2026-10-07.
|
||||
|
||||
@@ -606,7 +606,7 @@ development orientation, and CLI reference describe the implemented boundary.
|
||||
|
||||
## Stage 9: implement native effective YAML display
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 8.
|
||||
**Status:** complete. **Prerequisite:** Stage 8.
|
||||
|
||||
1. Implement `config show`; its only initial format is yaml (default and explicit).
|
||||
Single scope writes Effective.WriteEffective directly to Out with no envelope,
|
||||
@@ -633,6 +633,16 @@ labels and native values. Cover an already marked native document, missing/null/
|
||||
empty structures, a second acquisition failure with no prefix, and a writer failure
|
||||
after output begins. These protect selected behavior, not encoder whitespace.
|
||||
|
||||
**Completion evidence:** installed `config show` when a scope declares it, with
|
||||
YAML as its only format. Single-scope output passes through the native effective
|
||||
writer; aggregate output acquires and checks every view before emitting framed
|
||||
native documents in scope order. Existing markers, leading comments and YAML
|
||||
directives are retained, including explicit document termination where needed
|
||||
between directive-bearing documents. Checks cover native single-scope bytes,
|
||||
aggregate document decoding and values, invalid formats, acquisition failure with
|
||||
empty output, and incomplete delivery after a writer failure. `go test ./...`
|
||||
passes; README, development orientation, and CLI reference describe the feature.
|
||||
|
||||
## Stage 10: implement source inventory reports
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 9.
|
||||
|
||||
@@ -80,6 +80,9 @@ func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], opera
|
||||
if err := checkConfigFormat(input.Format); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if operation == ConfigShow && input.Format != "yaml" {
|
||||
return nil, fmt.Errorf("unsupported configuration show format %q", input.Format)
|
||||
}
|
||||
if input.Scope == "all" {
|
||||
if operation != ConfigValidate && operation != ConfigShow {
|
||||
return nil, fmt.Errorf("scope all is supported only for validate and show")
|
||||
|
||||
177
show.go
Normal file
177
show.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func newConfigShowCommand[C any](suite ConfigSuite[C]) (Command, error) {
|
||||
spec := CommandSpec[configInput[C]]{
|
||||
Name: string(ConfigShow),
|
||||
Summary: "Show effective configuration as YAML.",
|
||||
Flags: configInputFlags(suite, "yaml"),
|
||||
CheckInput: func(input configInput[C]) error {
|
||||
_, err := preflightConfig(suite, input, ConfigShow)
|
||||
return err
|
||||
},
|
||||
Run: func(invocation Invocation, input configInput[C]) Outcome {
|
||||
return runConfigShow(invocation, suite, input)
|
||||
},
|
||||
}
|
||||
return NewCommand(spec)
|
||||
}
|
||||
|
||||
type effectiveDocument struct {
|
||||
name string
|
||||
view ConfigView
|
||||
}
|
||||
|
||||
func runConfigShow[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||
scopeNames, err := selectConfigScopes(suite, input, ConfigShow)
|
||||
if err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
contexts := contextsForScopes(suite, input, scopeNames)
|
||||
documents := make([]effectiveDocument, 0, len(scopeNames))
|
||||
diagnostics := make([]Diagnostic, 0)
|
||||
for _, name := range scopeNames {
|
||||
scope, ok := findConfigScope(suite.Scopes, name)
|
||||
if !ok || scope.Acquire == nil {
|
||||
return showFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, false)
|
||||
}
|
||||
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigShow, Selection: input.Selection}, contexts[name])
|
||||
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
||||
if acquireErr != nil {
|
||||
return showFailure(acquireErr, diagnostics, false)
|
||||
}
|
||||
if acquired.Value.Effective == nil {
|
||||
return showFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", name), diagnostics, false)
|
||||
}
|
||||
documents = append(documents, effectiveDocument{name: name, view: acquired.Value})
|
||||
}
|
||||
|
||||
if len(documents) == 1 && input.Scope != "all" {
|
||||
checked := &checkedWriter{writer: invocation.Streams.Out}
|
||||
renderErr := documents[0].view.Effective.WriteEffective(checked)
|
||||
if checked.failure != nil {
|
||||
if renderErr == nil {
|
||||
renderErr = checked.failure
|
||||
} else {
|
||||
renderErr = errors.Join(renderErr, checked.failure)
|
||||
}
|
||||
}
|
||||
if renderErr != nil {
|
||||
outcome := showFailure(renderErr, diagnostics, true)
|
||||
outcome.DeliveryError = checked.failure
|
||||
return outcome
|
||||
}
|
||||
outcome := Success()
|
||||
outcome.Diagnostics = diagnostics
|
||||
return outcome
|
||||
}
|
||||
|
||||
written := false
|
||||
for index, document := range documents {
|
||||
var native bytes.Buffer
|
||||
if err := document.view.Effective.WriteEffective(&native); err != nil {
|
||||
return showFailure(err, diagnostics, written)
|
||||
}
|
||||
if index > 0 && hasYAMLDirective(native.Bytes()) {
|
||||
if err := writeReport(invocation.Streams.Out, []byte("...\n")); err != nil {
|
||||
outcome := showFailure(err, diagnostics, true)
|
||||
outcome.DeliveryError = err
|
||||
return outcome
|
||||
}
|
||||
}
|
||||
framed := frameEffectiveYAML(document.name, native.Bytes())
|
||||
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
|
||||
framed = append(framed, '\n')
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, framed); err != nil {
|
||||
outcome := showFailure(err, diagnostics, true)
|
||||
outcome.DeliveryError = err
|
||||
return outcome
|
||||
}
|
||||
written = true
|
||||
}
|
||||
outcome := Success()
|
||||
outcome.Diagnostics = diagnostics
|
||||
return outcome
|
||||
}
|
||||
|
||||
func hasYAMLDirective(native []byte) bool {
|
||||
lines := bytes.Split(native, []byte("\n"))
|
||||
for _, line := range lines {
|
||||
trimmed := bytes.TrimSpace(line)
|
||||
if bytes.Equal(trimmed, []byte("---")) || bytes.HasPrefix(trimmed, []byte("--- #")) {
|
||||
return false
|
||||
}
|
||||
if bytes.HasPrefix(trimmed, []byte("%YAML ")) || bytes.HasPrefix(trimmed, []byte("%TAG ")) {
|
||||
return true
|
||||
}
|
||||
if len(trimmed) > 0 && trimmed[0] != '#' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func frameEffectiveYAML(scope string, native []byte) []byte {
|
||||
label := scope
|
||||
for _, r := range scope {
|
||||
if unicode.IsControl(r) {
|
||||
label = strconv.Quote(scope)
|
||||
break
|
||||
}
|
||||
}
|
||||
comment := []byte("# scope: " + label + "\n")
|
||||
lines := bytes.SplitAfter(native, []byte("\n"))
|
||||
position := 0
|
||||
preambleEnd := 0
|
||||
hasDirective := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(string(line))
|
||||
if trimmed == "---" || strings.HasPrefix(trimmed, "--- #") {
|
||||
out := make([]byte, 0, len(native)+len(comment))
|
||||
out = append(out, native[:position+len(line)]...)
|
||||
if len(line) > 0 && line[len(line)-1] != '\n' {
|
||||
out = append(out, '\n')
|
||||
}
|
||||
out = append(out, comment...)
|
||||
out = append(out, native[position+len(line):]...)
|
||||
return out
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "%YAML ") || strings.HasPrefix(trimmed, "%TAG ") {
|
||||
hasDirective = true
|
||||
}
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "%YAML ") || strings.HasPrefix(trimmed, "%TAG ") {
|
||||
preambleEnd = position + len(line)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
position += len(line)
|
||||
}
|
||||
result := make([]byte, 0, len(native)+len(comment)+5)
|
||||
if hasDirective {
|
||||
result = append(result, native[:preambleEnd]...)
|
||||
}
|
||||
result = append(result, []byte("---\n")...)
|
||||
result = append(result, comment...)
|
||||
if hasDirective {
|
||||
result = append(result, native[preambleEnd:]...)
|
||||
} else {
|
||||
result = append(result, native...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func showFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
|
||||
outcome := Failure(cause)
|
||||
outcome.Diagnostics = diagnostics
|
||||
outcome.ReportIncomplete = reportStarted
|
||||
return outcome
|
||||
}
|
||||
Reference in New Issue
Block a user