Add completion hints to maintained commands
This commit is contained in:
11
integrations/ckconfig/completion.go
Normal file
11
integrations/ckconfig/completion.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package ckconfig
|
||||
|
||||
import commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
|
||||
func completionChoices(values ...string) commandkit.CompletionHint {
|
||||
choices := make([]commandkit.CompletionChoice, len(values))
|
||||
for i, value := range values {
|
||||
choices[i].Value = value
|
||||
}
|
||||
return commandkit.CompleteChoices(choices...)
|
||||
}
|
||||
66
integrations/ckconfig/completion_test.go
Normal file
66
integrations/ckconfig/completion_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
func TestCompletionUsesInstalledScopeCapabilitiesWithoutAcquisition(t *testing.T) {
|
||||
var inputs, states, acquisitions, comparisons int
|
||||
suite := Suite[struct{}, struct{}]{
|
||||
NewInput: func() struct{} { inputs++; return struct{}{} },
|
||||
NewState: func() struct{} { states++; return struct{}{} },
|
||||
Scopes: []Scope[struct{}, struct{}]{
|
||||
{Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||
comparisons++
|
||||
return commandkit.Acquired[[]configkit.Difference]{}, nil
|
||||
}},
|
||||
{Name: "other", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||
acquisitions++
|
||||
return commandkit.Acquired[View]{}, nil
|
||||
}},
|
||||
},
|
||||
}
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, Command(suite), commandkit.Completion("example"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
contains, omit string
|
||||
}{
|
||||
{[]string{"__complete", "config", "diff", "--scope="}, "app\n", "other\n"},
|
||||
{[]string{"__complete", "config", "validate", "--scope="}, "other\n", "all\n"},
|
||||
{[]string{"__complete", "config", "diff", "--format="}, "yaml\n", "text\n"},
|
||||
{[]string{"__complete", "config", "validate", "--format="}, "text\n", "invalid\n"},
|
||||
{[]string{"__complete", "config", "diff", "--against-config=fixtures/"}, ":0\n", ":4\n"},
|
||||
} {
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), tc.args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Status != 0 || !strings.Contains(output.String(), tc.contains) || strings.Contains(output.String(), tc.omit) {
|
||||
t.Fatalf("query %q outcome=%#v output=%q", tc.args, outcome, output.String())
|
||||
}
|
||||
}
|
||||
allSuite := Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{
|
||||
{Name: "app", Operations: []Operation{Validate}, Acquire: suite.Scopes[1].Acquire},
|
||||
{Name: "other", Operations: []Operation{Validate}, Acquire: suite.Scopes[1].Acquire},
|
||||
}}
|
||||
allApp, err := commandkit.New(commandkit.Identity{Name: "example"}, Command(allSuite), commandkit.Completion("example"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var allOutput bytes.Buffer
|
||||
allOutcome := allApp.Execute(context.Background(), []string{"__complete", "config", "validate", "--scope="}, commandkit.Streams{Out: &allOutput})
|
||||
if allOutcome.Status != 0 || !strings.Contains(allOutput.String(), "all\n") {
|
||||
t.Fatalf("all-scope query outcome=%#v output=%q", allOutcome, allOutput.String())
|
||||
}
|
||||
if inputs != 0 || states != 0 || acquisitions != 0 || comparisons != 0 {
|
||||
t.Fatalf("completion ran suite work: %d %d %d %d", inputs, states, acquisitions, comparisons)
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit.
|
||||
Name: "scopes",
|
||||
Summary: "List configuration scopes and supported operations.",
|
||||
Flags: []commandkit.Flag[scopeInventoryInput]{commandkit.ValueFlag(
|
||||
commandkit.FlagMeta{Name: "format", Usage: "Output format: text or yaml."}, &formatDefault, parseConfigString,
|
||||
commandkit.FlagMeta{Name: "format", Usage: "Output format: text or yaml.", Completion: completionChoices("text", "yaml")}, &formatDefault, parseConfigString,
|
||||
func(input *scopeInventoryInput, value commandkit.Supplied[string]) { input.Format = value.Value },
|
||||
)},
|
||||
CheckInput: func(input scopeInventoryInput) error { return checkConfigFormat(input.Format) },
|
||||
|
||||
@@ -45,27 +45,43 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
||||
if operation == Show || operation == Diff {
|
||||
defaultFormat = "yaml"
|
||||
}
|
||||
formats := completionChoices("text", "yaml")
|
||||
if operation == Show || operation == Diff {
|
||||
formats = completionChoices("yaml")
|
||||
}
|
||||
var scopeChoices []commandkit.CompletionChoice
|
||||
allSupported := len(prepared.scopes) > 0
|
||||
for _, scope := range prepared.scopes {
|
||||
if containsOperation(scope.scope.Operations, operation) {
|
||||
scopeChoices = append(scopeChoices, commandkit.CompletionChoice{Value: scope.scope.Name})
|
||||
} else {
|
||||
allSupported = false
|
||||
}
|
||||
}
|
||||
if allSupported && (operation == Validate || operation == Show) {
|
||||
scopeChoices = append(scopeChoices, commandkit.CompletionChoice{Value: "all"})
|
||||
}
|
||||
flags := []commandkit.Flag[*configCommandInput[I, S]]{
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported."}, &defaultScope, parseConfigString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported.", Completion: commandkit.CompleteChoices(scopeChoices...)}, &defaultScope, parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).scope = value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Output format."}, &defaultFormat, parseConfigString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Output format.", Completion: formats}, &defaultFormat, parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).format = value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration source."}, (*string)(nil), parseConfigString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration source.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Config = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Overlay source."}, (*string)(nil), parseConfigString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Overlay source.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Overlay = value
|
||||
}),
|
||||
}
|
||||
if operation == Diff {
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-config", Usage: "Configuration source for the right side."}, (*string)(nil), parseConfigString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-config", Usage: "Configuration source for the right side.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||
(*input).against.Config = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-overlay", Usage: "Overlay source for the right side."}, (*string)(nil), parseConfigString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-overlay", Usage: "Overlay source for the right side.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseConfigString,
|
||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||
(*input).against.Overlay = value
|
||||
}),
|
||||
|
||||
@@ -134,11 +134,11 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string) commandkit.
|
||||
return &(*input).captured
|
||||
})
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).selection.Config = value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).selection.Overlay = value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Catalog format: yaml."}, stringDefault("yaml"), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Catalog format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
|
||||
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
|
||||
)
|
||||
if name == "prompts" || name == "profiles" || name == "backends" {
|
||||
@@ -256,13 +256,13 @@ func newDefinitionCommand[I, S any](suite Suite[I, S], name string, prompt bool)
|
||||
return &(*input).captured
|
||||
})
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **definitionInput[I], value commandkit.Supplied[string]) { (*input).selection.Config = value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **definitionInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Overlay = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Report format: text or yaml."}, stringDefault("text"), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Report format: text or yaml.", Completion: completionChoices("text", "yaml")}, stringDefault("text"), parseDefinitionString,
|
||||
func(input **definitionInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
|
||||
)
|
||||
spec := commandkit.CommandSpec[*definitionInput[I]]{
|
||||
@@ -302,11 +302,11 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
return &(*input).captured
|
||||
})
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Config = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Overlay = value
|
||||
}),
|
||||
@@ -318,7 +318,7 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).inspection.SessionID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Report format: yaml."}, stringDefault("yaml"), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Report format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
|
||||
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "var", Usage: "Caller variable assignment KEY=VALUE."}, []variableAssignment(nil), parseVariableAssignment,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[[]variableAssignment]) {
|
||||
|
||||
11
integrations/ckprompt/completion.go
Normal file
11
integrations/ckprompt/completion.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package ckprompt
|
||||
|
||||
import commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
|
||||
func completionChoices(values ...string) commandkit.CompletionHint {
|
||||
choices := make([]commandkit.CompletionChoice, len(values))
|
||||
for i, value := range values {
|
||||
choices[i].Value = value
|
||||
}
|
||||
return commandkit.CompleteChoices(choices...)
|
||||
}
|
||||
@@ -40,6 +40,13 @@ shortens the PromptKit group. `--fmt` is scoped to named prompt inspection and
|
||||
`--hlep` is available globally. The `smoke` shortcut retains the canonical
|
||||
`promptkit test KIND` operand, including the special `all` aggregate.
|
||||
|
||||
The example installs `Completion("composed-example")`. Build it as
|
||||
`go build -o composed-example .`, then load a shell script as described in the
|
||||
[CLI reference](../../../../docs/cli.md#shell-completion). Try completion after
|
||||
`composed-example pk show prompt briefing --fmt=` or after the filename prefix
|
||||
in `composed-example config show --config=fixtures/`. The already-entered `pk`
|
||||
alias selects the PromptKit command context without loading an engine.
|
||||
|
||||
The composition calls `ckconfig.Command(suite)` and `ckprompt.Command(definitions)`
|
||||
directly in a `[]commandkit.Declaration` with root-installed routing rules.
|
||||
Group children remain canonical `Command` values. For command-only slices,
|
||||
|
||||
@@ -290,6 +290,7 @@ func newAppWithEngineLoader(
|
||||
declarations := []commandkit.Declaration{
|
||||
configCommand,
|
||||
definitionCommand,
|
||||
commandkit.Completion("composed-example"),
|
||||
commandkit.Group("session", "Manage saved sessions.", restore),
|
||||
commandkit.Alias("list collections", "promptkit list collections"),
|
||||
commandkit.Alias("promptkit list bundles", "promptkit list collections"),
|
||||
|
||||
@@ -352,6 +352,34 @@ func TestComposedCatalogsAndSmokeUseInjectedClientOnlyForInference(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposedCompletionUsesHintsWithoutLoadingEngine(t *testing.T) {
|
||||
var loads, calls int
|
||||
loader := func(ctx context.Context, selection ckconfig.Selection, input *inspectionInput, cache *runCache) (*promptkit.Engine, error) {
|
||||
loads++
|
||||
return loadPromptEngine(ctx, selection, input, cache)
|
||||
}
|
||||
app, err := newAppWithEngineLoader(&exampleLLMClient{calls: &calls}, loader, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"__complete", "pk", "show", "prompt", "briefing", "--fmt=y"}, "yaml\n"},
|
||||
{[]string{"__complete", "pk", "show", "prompt", "briefing", "--config=fixtures/"}, ":0\n"},
|
||||
} {
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), tc.args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Status != 0 || !strings.Contains(output.String(), tc.want) {
|
||||
t.Fatalf("query %q outcome=%#v output=%q", tc.args, outcome, output.String())
|
||||
}
|
||||
}
|
||||
if loads != 0 || calls != 0 {
|
||||
t.Fatalf("completion loaded engine %d times and invoked client %d times", loads, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposedSmokeReportSurvivesInferenceFailure(t *testing.T) {
|
||||
wantCause := errors.New("local example inference failed")
|
||||
smokeTest := func(_ context.Context, request ckprompt.SmokeRequest, _ *inspectionInput, _ *runCache) (commandkit.Acquired[*promptkit.SmokeTestReport], error) {
|
||||
|
||||
@@ -28,15 +28,15 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
return &(*input).captured
|
||||
})
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Config = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Overlay = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Smoke report format: yaml."}, stringDefault("yaml"), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Smoke report format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "backend", Usage: "PromptKit backend ID."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
|
||||
@@ -29,11 +29,11 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
return &(*input).captured
|
||||
})
|
||||
flags = append(flags,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration input path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.Selection.Config = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path."}, (*string)(nil), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.Selection.Overlay = value
|
||||
}),
|
||||
@@ -45,7 +45,7 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.Request.SessionID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Confirmation format: yaml."}, stringDefault("yaml"), parseDefinitionString,
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Confirmation format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).format = value.Value
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user