Add native configuration display

This commit is contained in:
2026-10-07 13:55:43 +00:00
parent 3945f1c33a
commit bbd862311e
9 changed files with 351 additions and 10 deletions

View File

@@ -6,8 +6,8 @@ and shared command infrastructure across related applications.
The current Go library provides an instance-owned command registry, typed flag The current Go library provides an instance-owned command registry, typed flag
and operand bindings, Cobra-backed routing, invocation outcomes, generated help, and operand bindings, Cobra-backed routing, invocation outcomes, generated help,
application or CommandKit version reporting, configuration scope discovery, and application or CommandKit version reporting, configuration scope discovery, and
ConfigKit-backed validation. Effective display, source and explanation reports, ConfigKit-backed validation and native effective YAML display. Source and
and PromptKit commands remain planned. explanation reports and PromptKit commands remain planned.
## Documentation ## Documentation

View File

@@ -131,8 +131,8 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
return fmt.Errorf("command path %q is already registered", "config") return fmt.Errorf("command path %q is already registered", "config")
} }
commands := make([]Command, 0, 2) commands := make([]Command, 0, 3)
installed := make([]ConfigOperation, 0, 1) installed := make([]ConfigOperation, 0, 2)
if configOperationDeclared(normalized.Scopes, ConfigValidate) { if configOperationDeclared(normalized.Scopes, ConfigValidate) {
validate, err := newConfigValidateCommand(normalized) validate, err := newConfigValidateCommand(normalized)
if err != nil { if err != nil {
@@ -141,6 +141,14 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
commands = append(commands, validate) commands = append(commands, validate)
installed = append(installed, ConfigValidate) 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) command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed)
if err != nil { if err != nil {
return err return err

View File

@@ -47,7 +47,7 @@ func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
if outcome.Kind != OutcomeSuccess { if outcome.Kind != OutcomeSuccess {
t.Fatalf("text inventory outcome = %#v", outcome) 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 { if textOutput.String() != wantText {
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText) t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
} }

View File

@@ -12,8 +12,129 @@ import (
commandkit "gitea.maximumdirect.net/eric/CommandKit" commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit" 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) { func TestConfigKitScopeUsesDefaultsOverlayAndRecordedResolution(t *testing.T) {
type configuration struct { type configuration struct {
Mode string `yaml:"mode"` Mode string `yaml:"mode"`

View File

@@ -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 its own ConfigKit load policy. Help and invalid syntax do not call scope
providers. 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 ## Outcome status
CommandKit usage failures use status `2`; operational failures use status `1`. CommandKit usage failures use status `2`; operational failures use status `1`.

View File

@@ -4,9 +4,9 @@ CommandKit currently contains adopted repository policies, accepted architectura
decisions, implementation plans, identity and outcome primitives, typed command decisions, implementation plans, identity and outcome primitives, typed command
declarations, an instance-owned registry, a Cobra-backed routing and parsing declarations, an instance-owned registry, a Cobra-backed routing and parsing
adapter, public `App.Execute` coordination, and generated help/version commands. adapter, public `App.Execute` coordination, and generated help/version commands.
Configuration suite registration, scope discovery, ConfigKit integration, and Configuration suite registration, scope discovery, ConfigKit integration,
validation are implemented; other configuration and PromptKit operations remain validation, and native effective YAML display are implemented; other
pending. The configuration and PromptKit operations remain pending. The
[README](../README.md) provides project orientation; the [README](../README.md) provides project orientation; the
[documentation policy](policy/documentation.md#canonical-ownership) assigns [documentation policy](policy/documentation.md#canonical-ownership) assigns
canonical documentation owners. canonical documentation owners.

View File

@@ -1,6 +1,6 @@
# Initial CommandKit implementation plan # 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. Date: 2026-10-07.
@@ -606,7 +606,7 @@ development orientation, and CLI reference describe the implemented boundary.
## Stage 9: implement native effective YAML display ## 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). 1. Implement `config show`; its only initial format is yaml (default and explicit).
Single scope writes Effective.WriteEffective directly to Out with no envelope, 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 empty structures, a second acquisition failure with no prefix, and a writer failure
after output begins. These protect selected behavior, not encoder whitespace. 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 ## Stage 10: implement source inventory reports
**Status:** pending. **Prerequisite:** Stage 9. **Status:** pending. **Prerequisite:** Stage 9.

View File

@@ -80,6 +80,9 @@ func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], opera
if err := checkConfigFormat(input.Format); err != nil { if err := checkConfigFormat(input.Format); err != nil {
return nil, err 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 input.Scope == "all" {
if operation != ConfigValidate && operation != ConfigShow { if operation != ConfigValidate && operation != ConfigShow {
return nil, fmt.Errorf("scope all is supported only for validate and show") return nil, fmt.Errorf("scope all is supported only for validate and show")

177
show.go Normal file
View 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
}