Add completion hints to maintained commands

This commit is contained in:
2026-11-03 04:09:27 +00:00
parent d1f31d2931
commit 96250d1887
20 changed files with 255 additions and 34 deletions

View 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...)
}

View 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)
}
}

View File

@@ -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) },

View File

@@ -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
}),

View File

@@ -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]) {

View 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...)
}

View File

@@ -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,

View File

@@ -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"),

View File

@@ -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) {

View File

@@ -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]) {

View File

@@ -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
}),