Add configuration path discovery
This commit is contained in:
19
docs/cli.md
19
docs/cli.md
@@ -421,21 +421,32 @@ for native evidence definitions.
|
||||
### Explain a configuration path
|
||||
|
||||
`config explain --path POINTER` explains the effective and historical value at a
|
||||
JSON Pointer path. `--path` is required; an explicitly empty value selects the
|
||||
root. The default scope is `app`; `--format yaml` or `--format json` selects
|
||||
JSON Pointer path. An explicitly empty path selects the root. `config explain
|
||||
--list` discovers paths when the selected scope supports path discovery. An
|
||||
invocation with neither mode displays help; the two modes cannot be combined.
|
||||
The default scope is `app`; `--format yaml` or `--format json` selects
|
||||
structured output, and automatic output uses text. `--scope all` is unsupported.
|
||||
|
||||
```text
|
||||
example config explain --path /server/addr
|
||||
example config explain --path /profiles/team~1summary --format yaml
|
||||
example config explain --path= --format yaml
|
||||
example config explain --list --under /profiles --historical
|
||||
```
|
||||
|
||||
Listing normally includes current paths. `--historical` adds paths that only
|
||||
occurred in history; `--under POINTER` selects a subtree, and `--explicit=true`
|
||||
or `--explicit=false` filters current paths by native explicitness. Historical-only
|
||||
paths do not match an explicitness filter. These options require `--list`.
|
||||
The report contains the scope and each selected path's current presence and,
|
||||
when present, its explicitness. Path discovery does not include values or history.
|
||||
|
||||
Pointer tokens use JSON Pointer escaping: `~1` represents `/` and `~0`
|
||||
represents `~`. A malformed pointer is a usage error with status `2` and is
|
||||
rejected before acquisition. A well-formed path that is unknown in the selected
|
||||
configuration is an operational error with status `1`. Text output comes from
|
||||
the scope's native explanation writer.
|
||||
configuration is an operational error with status `1`. Text output includes
|
||||
the scope's native explanation and available field metadata. Structured output
|
||||
includes metadata when supplied by the scope.
|
||||
|
||||
Structured output uses these root fields:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||
|
||||
Status: implementation in progress; Stages 1–16 complete.
|
||||
Status: implementation in progress; Stages 1–17 complete.
|
||||
|
||||
## Scope, authority, and execution
|
||||
|
||||
@@ -610,6 +610,12 @@ algorithms remain upstream tests.
|
||||
|
||||
## Stage 17: Explanation discovery and metadata
|
||||
|
||||
**Complete.** Explain supports discovery filters and field metadata, with
|
||||
mode and capability checks before acquisition. `Invocation.Help` renders the
|
||||
selected command's existing help when no mode is selected, avoiding application
|
||||
factories. Root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0,
|
||||
including historical paths, escaped subtrees, and help delivery.
|
||||
|
||||
Extend `config explain` under [OD-4](promptkit-v0.20.0.md#od-4-configuration-path-and-metadata-presentation).
|
||||
Track supplied `--path` separately from its value; root `--path ''` is an
|
||||
operation. Bare invocation shows successful command help before input/state
|
||||
|
||||
@@ -107,6 +107,7 @@ func (current *execution) invocation() Invocation {
|
||||
reporter: current.reporter,
|
||||
enteredPath: current.route.enteredPath.String(),
|
||||
canonicalPath: current.route.canonicalPath.String(),
|
||||
renderHelp: func() Outcome { return current.renderHelp(nil) },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
identity.go
10
identity.go
@@ -52,12 +52,22 @@ type Invocation struct {
|
||||
reporter *Reporter
|
||||
enteredPath string
|
||||
canonicalPath string
|
||||
renderHelp func() Outcome
|
||||
}
|
||||
|
||||
// Reporter returns the reporting facility supplied for this invocation. A
|
||||
// manually constructed Invocation has no reporter.
|
||||
func (inv Invocation) Reporter() *Reporter { return inv.reporter }
|
||||
|
||||
// Help renders the selected command's help through this invocation's output.
|
||||
// It returns an operational failure when the invocation has no help renderer.
|
||||
func (inv Invocation) Help() Outcome {
|
||||
if inv.renderHelp == nil {
|
||||
return Failure("Command help is unavailable.")
|
||||
}
|
||||
return inv.renderHelp()
|
||||
}
|
||||
|
||||
// EnteredPath returns the command path used to invoke the selected command,
|
||||
// excluding the executable name, flags, and operands. It is empty for an
|
||||
// Invocation that was not created by command execution.
|
||||
|
||||
116
integrations/ckconfig/discovery_test.go
Normal file
116
integrations/ckconfig/discovery_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestExplainDiscoveryModesAndMetadata(t *testing.T) {
|
||||
loads, inputs, states := 0, 0, 0
|
||||
scope := FromConfigKit("app", "", func(_ context.Context, selection Selection, _, _ struct{}) (*configkit.Result[map[string]any], error) {
|
||||
loads++
|
||||
return configkit.Load(configkit.Mapping("fixture", map[string]any{"a/b": map[string]any{"x": 1}, "ab": 2, "abacus": 3, "gone": map[string]any{"x": 1}}), configkit.Options[map[string]any]{Overrides: selection.Overrides})
|
||||
})
|
||||
scope.Features.Overrides = true
|
||||
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{NewInput: func() struct{} { inputs++; return struct{}{} }, NewState: func() struct{} { states++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
run := func(args ...string) (commandkit.Outcome, string) {
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(context.Background(), append([]string{"config", "explain"}, args...), commandkit.Streams{Out: &out, Err: io.Discard})
|
||||
return result, out.String()
|
||||
}
|
||||
result, out := run()
|
||||
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, "--list") || !strings.Contains(out, "--path") || loads != 0 || inputs != 0 || states != 0 {
|
||||
t.Fatalf("bare help: %#v %q, %d/%d/%d", result, out, loads, inputs, states)
|
||||
}
|
||||
for _, args := range [][]string{{"--list", "--path=/ab"}, {"--historical"}, {"--under=/ab"}, {"--explicit=false"}, {"--path=/ab", "--under=/ab"}, {"--list", "--under=bad"}} {
|
||||
result, _ := run(args...)
|
||||
if result.Kind != commandkit.OutcomeUsage || loads != 0 || inputs != 0 || states != 0 {
|
||||
t.Fatalf("invalid %v: %#v, %d/%d/%d", args, result, loads, inputs, states)
|
||||
}
|
||||
}
|
||||
result, out = run("--list", "--under=/a~1b", "--format=json")
|
||||
var report pathDiscoveryReport
|
||||
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil || len(report.Paths) != 2 {
|
||||
t.Fatalf("escaped subtree: %#v %q", result, out)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, entry := range report.Paths {
|
||||
seen[entry.Path] = true
|
||||
}
|
||||
if !seen["/a~1b"] || !seen["/a~1b/x"] {
|
||||
t.Fatalf("escaped paths: %#v", report.Paths)
|
||||
}
|
||||
result, out = run("--list", "--under=/ab", "--format=json")
|
||||
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil || len(report.Paths) != 1 || report.Paths[0].Path != "/ab" {
|
||||
t.Fatalf("token boundary: %#v %q", result, out)
|
||||
}
|
||||
result, out = run("--list", "--historical", "--set=/gone=null", "--format=json")
|
||||
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil {
|
||||
t.Fatalf("historical: %#v %q", result, out)
|
||||
}
|
||||
removed := false
|
||||
for _, entry := range report.Paths {
|
||||
if entry.Path == "/gone/x" && !entry.Present && entry.Metadata == nil {
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if !removed {
|
||||
t.Fatalf("missing historical entry: %q", out)
|
||||
}
|
||||
result, out = run("--list", "--historical", "--explicit=false", "--set=/gone=null", "--format=json")
|
||||
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil {
|
||||
t.Fatalf("explicit filter: %#v %q", result, out)
|
||||
}
|
||||
for _, entry := range report.Paths {
|
||||
if !entry.Present || entry.Metadata == nil || entry.Metadata.Explicit {
|
||||
t.Fatalf("invalid filtered entry: %#v", entry)
|
||||
}
|
||||
}
|
||||
result, out = run("--path=", "--format=json")
|
||||
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, `"path": ""`) || !strings.Contains(out, `"metadata"`) {
|
||||
t.Fatalf("root explanation: %#v %q", result, out)
|
||||
}
|
||||
result, out = run("--path=/gone/x", "--set=/gone=null", "--format=json")
|
||||
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, `"metadata"`) || !strings.Contains(out, `"history"`) {
|
||||
t.Fatalf("historical explanation: %#v %q", result, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplainDiscoveryCapabilityBeforeAcquisition(t *testing.T) {
|
||||
calls := 0
|
||||
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[View]{}, nil
|
||||
}}
|
||||
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
result := app.Execute(context.Background(), []string{"config", "explain", "--list"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeUsage || calls != 0 {
|
||||
t.Fatalf("unsupported discovery: %#v calls=%d", result, calls)
|
||||
}
|
||||
scope.Features.PathDiscovery = true
|
||||
app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
result = app.Execute(context.Background(), []string{"config", "explain", "--list"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeOperational || calls != 1 {
|
||||
t.Fatalf("missing metadata: %#v calls=%d", result, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplainBareHelpRecordsDeliveryFailure(t *testing.T) {
|
||||
inputs, states, calls := 0, 0, 0
|
||||
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[View]{}, nil
|
||||
}}
|
||||
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{NewInput: func() struct{} { inputs++; return struct{}{} }, NewState: func() struct{} { states++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
result := app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: shortWriter{}, Err: io.Discard})
|
||||
if result.Presentation.Output.State != commandkit.DeliveryIncomplete || inputs != 0 || states != 0 || calls != 0 {
|
||||
t.Fatalf("help delivery = %#v, input/state/acquire=%d/%d/%d", result, inputs, states, calls)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package ckconfig
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
@@ -16,8 +17,37 @@ type explanationReport struct {
|
||||
Origin explanationOriginDTO `json:"origin" yaml:"origin"`
|
||||
History []explanationChangeDTO `json:"history" yaml:"history"`
|
||||
SecretReference *secretReferenceDTO `json:"secret_reference" yaml:"secret_reference"`
|
||||
Metadata *explanationMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type explanationMetadata struct {
|
||||
Explicit *bool `json:"explicit,omitempty"`
|
||||
Null *bool `json:"null,omitempty"`
|
||||
History []metadataHistoryRecord `json:"history,omitempty"`
|
||||
}
|
||||
|
||||
type metadataHistoryRecord struct {
|
||||
Path string `json:"path"`
|
||||
Previous []explanationOriginDTO `json:"previous,omitempty"`
|
||||
}
|
||||
|
||||
type pathDiscoveryReport struct {
|
||||
Scope string `json:"scope"`
|
||||
Paths []pathDiscoveryEntry `json:"paths"`
|
||||
}
|
||||
|
||||
type pathDiscoveryEntry struct {
|
||||
Path string `json:"path"`
|
||||
Present bool `json:"present"`
|
||||
Metadata *pathMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type pathMetadata struct {
|
||||
Explicit bool `json:"explicit"`
|
||||
}
|
||||
|
||||
var pathDiscoveryTemplate = template.Must(commandkit.ParseTextTemplate("paths", "{{range .Paths}}{{printf \"%q\" .Path}} {{if .Present}}{{if .Metadata.Explicit}}explicit{{else}}non-explicit{{end}}{{else}}historical{{end}}\n{{end}}", nil))
|
||||
|
||||
type snapshotDTO struct {
|
||||
Present bool `json:"present" yaml:"present"`
|
||||
Redacted bool `json:"redacted" yaml:"redacted"`
|
||||
@@ -72,6 +102,31 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
|
||||
}
|
||||
if plan.list {
|
||||
if acquired.Value.Metadata == nil {
|
||||
return configFailure("Configuration scope did not provide path metadata.", acquired.Explanation, fmt.Errorf("configuration scope %q declared path discovery without metadata", name))
|
||||
}
|
||||
report := pathDiscoveryReport{Scope: name, Paths: []pathDiscoveryEntry{}}
|
||||
for _, path := range acquired.Value.Metadata.HistoricalPaths() {
|
||||
if plan.under != nil && !pathWithin(path, *plan.under) {
|
||||
continue
|
||||
}
|
||||
info, present := acquired.Value.Metadata.Lookup(path)
|
||||
if !present && (!plan.historical || plan.explicit.Set) {
|
||||
continue
|
||||
}
|
||||
if present && plan.explicit.Set && info.Explicit != plan.explicit.Value {
|
||||
continue
|
||||
}
|
||||
entry := pathDiscoveryEntry{Path: path.String(), Present: present}
|
||||
if present {
|
||||
entry.Metadata = &pathMetadata{Explicit: info.Explicit}
|
||||
}
|
||||
report.Paths = append(report.Paths, entry)
|
||||
}
|
||||
_ = reporter.Report(commandkit.Report{Value: report, TextTemplate: pathDiscoveryTemplate})
|
||||
return commandkit.Success()
|
||||
}
|
||||
if acquired.Value.Explanation == nil {
|
||||
cause := fmt.Errorf("configuration scope %q did not provide an explanation reader", name)
|
||||
return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, cause)
|
||||
@@ -85,20 +140,59 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
|
||||
}
|
||||
report := makeExplanationReport(name, explanation)
|
||||
if acquired.Value.Metadata != nil {
|
||||
metadata := &explanationMetadata{}
|
||||
if info, ok := acquired.Value.Metadata.Lookup(plan.path); ok {
|
||||
metadata.Explicit, metadata.Null = &info.Explicit, &info.Null
|
||||
}
|
||||
for _, change := range acquired.Value.Metadata.History(plan.path) {
|
||||
entry := metadataHistoryRecord{Path: change.Path.String()}
|
||||
for _, previous := range change.Previous {
|
||||
entry.Previous = append(entry.Previous, originReport(previous))
|
||||
}
|
||||
metadata.History = append(metadata.History, entry)
|
||||
}
|
||||
if metadata.Explicit != nil || len(metadata.History) > 0 {
|
||||
report.Metadata = metadata
|
||||
}
|
||||
}
|
||||
text := template.Must(commandkit.ParseTextTemplate("explanation", "{{render .}}", template.FuncMap{
|
||||
"render": func(explanationReport) (string, error) {
|
||||
"render": func(report explanationReport) (string, error) {
|
||||
var output bytes.Buffer
|
||||
err := acquired.Value.Explanation.WriteExplanation(&output, plan.path)
|
||||
if err == nil && report.Metadata != nil && report.Metadata.Explicit != nil {
|
||||
_, err = fmt.Fprintf(&output, "Explicit: %t\nNull: %t\n", *report.Metadata.Explicit, *report.Metadata.Null)
|
||||
}
|
||||
return output.String(), err
|
||||
},
|
||||
}))
|
||||
renderErr := reporter.Report(commandkit.Report{Value: makeExplanationReport(name, explanation), TextTemplate: text})
|
||||
renderErr := reporter.Report(commandkit.Report{Value: report, TextTemplate: text})
|
||||
if renderErr != nil && invocation.Context.Err() != nil {
|
||||
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
|
||||
}
|
||||
return commandkit.Success()
|
||||
}
|
||||
|
||||
func pathWithin(path, parent configkit.Path) bool {
|
||||
tokens := func(value string) []string {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(strings.TrimPrefix(value, "/"), "/")
|
||||
}
|
||||
child, prefix := tokens(path.String()), tokens(parent.String())
|
||||
if len(prefix) > len(child) {
|
||||
return false
|
||||
}
|
||||
for index := range prefix {
|
||||
if prefix[index] != child[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {
|
||||
report := explanationReport{
|
||||
Scope: scope,
|
||||
|
||||
@@ -13,6 +13,10 @@ type configCommandInput[I, S any] struct {
|
||||
against Selection
|
||||
scope commandkit.Supplied[string]
|
||||
path commandkit.Supplied[string]
|
||||
list commandkit.Supplied[bool]
|
||||
historical commandkit.Supplied[bool]
|
||||
under commandkit.Supplied[string]
|
||||
explicit commandkit.Supplied[bool]
|
||||
values commandkit.FlagValues[I]
|
||||
againstValues commandkit.FlagValues[I]
|
||||
plan *configOperationPlan[I, S]
|
||||
@@ -34,6 +38,11 @@ type configOperationPlan[I, S any] struct {
|
||||
operation Operation
|
||||
scopeName string
|
||||
path configkit.Path
|
||||
list bool
|
||||
help bool
|
||||
historical bool
|
||||
under *configkit.Path
|
||||
explicit commandkit.Supplied[bool]
|
||||
scopes []selectedScopeInput[I, S]
|
||||
comparison *preparedComparison[I, S]
|
||||
}
|
||||
@@ -89,8 +98,13 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
||||
}
|
||||
}
|
||||
if operation == Explain {
|
||||
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }),
|
||||
commandkit.BoolFlag(commandkit.FlagMeta{Name: "list", Usage: "List configuration paths."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).list = value }),
|
||||
commandkit.BoolFlag(commandkit.FlagMeta{Name: "historical", Usage: "Include historical-only paths when listing."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).historical = value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "under", Usage: "List paths under a JSON Pointer."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).under = value }),
|
||||
commandkit.BoolFlag(commandkit.FlagMeta{Name: "explicit", Usage: "Filter listed current paths by explicitness."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).explicit = value }),
|
||||
)
|
||||
}
|
||||
flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
||||
if input == nil || *input == nil {
|
||||
@@ -110,8 +124,9 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
||||
name, summary := operationDeclaration(operation)
|
||||
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
|
||||
Name: name, Summary: summary,
|
||||
NewInput: func() *configCommandInput[I, S] { return &configCommandInput[I, S]{} },
|
||||
Flags: flags,
|
||||
Description: operationDescription(operation),
|
||||
NewInput: func() *configCommandInput[I, S] { return &configCommandInput[I, S]{} },
|
||||
Flags: flags,
|
||||
CheckInput: func(input *configCommandInput[I, S]) error {
|
||||
if input == nil {
|
||||
return fmt.Errorf("configuration input must not be nil")
|
||||
@@ -124,6 +139,13 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
||||
})
|
||||
}
|
||||
|
||||
func operationDescription(operation Operation) string {
|
||||
if operation == Explain {
|
||||
return "Use --list to discover paths or --path POINTER to explain one path."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func operationDeclaration(operation Operation) (string, string) {
|
||||
switch operation {
|
||||
case Validate:
|
||||
@@ -142,6 +164,18 @@ func operationDeclaration(operation Operation) (string, string) {
|
||||
}
|
||||
|
||||
func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation Operation, input *configCommandInput[I, S]) error {
|
||||
if operation == Explain {
|
||||
if input.list.Value && input.path.Set {
|
||||
return fmt.Errorf("--list and --path cannot be combined")
|
||||
}
|
||||
if !input.list.Value && (input.historical.Set || input.under.Set || input.explicit.Set) {
|
||||
return fmt.Errorf("--historical, --under, and --explicit require --list")
|
||||
}
|
||||
if !input.list.Value && !input.path.Set {
|
||||
input.plan = &configOperationPlan[I, S]{prepared: prepared, operation: operation, help: true}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
scopeName := "app"
|
||||
if input.scope.Set {
|
||||
@@ -170,26 +204,38 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
if (len(input.selection.Overrides) > 0 || len(input.against.Overrides) > 0) && !scope.scope.Features.Overrides {
|
||||
return fmt.Errorf("configuration scope %q does not support overrides", scope.scope.Name)
|
||||
}
|
||||
if operation == Explain && input.list.Value && !scope.scope.Features.PathDiscovery {
|
||||
return fmt.Errorf("configuration scope %q does not support path discovery", scope.scope.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var path configkit.Path
|
||||
if operation == Explain {
|
||||
if !input.path.Set {
|
||||
return fmt.Errorf("configuration explanation requires --path")
|
||||
}
|
||||
if operation == Explain && input.path.Set {
|
||||
parsed, err := configkit.ParsePath(input.path.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration path: %w", err)
|
||||
}
|
||||
path = parsed
|
||||
}
|
||||
var under *configkit.Path
|
||||
if input.under.Set {
|
||||
parsed, err := configkit.ParsePath(input.under.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid subtree path: %w", err)
|
||||
}
|
||||
under = &parsed
|
||||
}
|
||||
|
||||
plan := &configOperationPlan[I, S]{
|
||||
prepared: prepared,
|
||||
operation: operation,
|
||||
scopeName: scopeName,
|
||||
path: path,
|
||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||
prepared: prepared,
|
||||
operation: operation,
|
||||
scopeName: scopeName,
|
||||
path: path,
|
||||
list: input.list.Value,
|
||||
historical: input.historical.Value,
|
||||
under: under,
|
||||
explicit: input.explicit,
|
||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||
}
|
||||
if operation == Diff {
|
||||
scope := selected[0]
|
||||
|
||||
@@ -11,6 +11,9 @@ func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Op
|
||||
if input == nil || input.plan == nil {
|
||||
return commandkit.Failure("Configuration operation was not prepared.")
|
||||
}
|
||||
if input.plan.help {
|
||||
return invocation.Help()
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return configFailure("Configuration operation was canceled.", commandkit.Explanation{}, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user