Add completion hints to maintained commands
This commit is contained in:
@@ -45,8 +45,8 @@ func (a *App) builtinCommands() []Declaration {
|
||||
Summary: "Show application or CommandKit version.",
|
||||
Description: "Report identity and build metadata without loading application configuration.",
|
||||
Flags: []Flag[versionInput]{
|
||||
ValueFlag(FlagMeta{Name: "scope", Usage: "version subject"}, &scopeDefault, parseVersionScope, func(input *versionInput, value Supplied[string]) { input.Scope = value.Value }),
|
||||
ValueFlag(FlagMeta{Name: "format", Usage: "output format"}, &formatDefault, parseVersionFormat, func(input *versionInput, value Supplied[string]) { input.Format = value.Value }),
|
||||
ValueFlag(FlagMeta{Name: "scope", Usage: "version subject", Completion: CompleteChoices(CompletionChoice{Value: "app"}, CompletionChoice{Value: "commandkit"})}, &scopeDefault, parseVersionScope, func(input *versionInput, value Supplied[string]) { input.Scope = value.Value }),
|
||||
ValueFlag(FlagMeta{Name: "format", Usage: "output format", Completion: CompleteChoices(CompletionChoice{Value: "text"}, CompletionChoice{Value: "yaml"})}, &formatDefault, parseVersionFormat, func(input *versionInput, value Supplied[string]) { input.Format = value.Value }),
|
||||
},
|
||||
Run: func(invocation Invocation, input versionInput) Outcome {
|
||||
identity := a.identity
|
||||
|
||||
@@ -51,7 +51,9 @@ func validCompletionExecutable(name string) bool {
|
||||
|
||||
// CompletionChoice is a literal suggested value and its optional description.
|
||||
type CompletionChoice struct {
|
||||
Value string
|
||||
// Value is inserted as data; it is not interpreted as shell source.
|
||||
Value string
|
||||
// Description is optional context displayed by shells that support it.
|
||||
Description string
|
||||
}
|
||||
|
||||
@@ -71,7 +73,10 @@ const (
|
||||
completionDirectories
|
||||
)
|
||||
|
||||
// CompleteChoices suggests literal values. The supplied slice is copied.
|
||||
// CompleteChoices suggests literal values matching the current value prefix.
|
||||
// The supplied slice is copied. An empty list produces no suggestions. Empty
|
||||
// values and values containing a newline, tab, or NUL cannot be presented as
|
||||
// suggestions, but remain valid input if the command's converter accepts them.
|
||||
func CompleteChoices(choices ...CompletionChoice) CompletionHint {
|
||||
return CompletionHint{kind: completionChoices, choices: append([]CompletionChoice(nil), choices...)}
|
||||
}
|
||||
|
||||
25
docs/cli.md
25
docs/cli.md
@@ -66,6 +66,31 @@ version: dev
|
||||
revision: ""
|
||||
```
|
||||
|
||||
## Shell completion
|
||||
|
||||
Applications may install shell completion explicitly. When installed, generate
|
||||
and load the script for the executable name chosen by the application:
|
||||
|
||||
```sh
|
||||
# Bash
|
||||
source <(app completion bash)
|
||||
# Zsh, after initializing its completion system with compinit
|
||||
source <(app completion zsh)
|
||||
# Fish
|
||||
app completion fish | source
|
||||
```
|
||||
|
||||
The application selects the executable basename at installation; use that same
|
||||
name to invoke the generated script. Completion offers canonical command and
|
||||
flag names, static values, and file or directory paths where declarations
|
||||
provide hints. A fully entered alias is resolved to its canonical command;
|
||||
alias spellings and partial alias paths are not suggested. Completion queries
|
||||
do not invoke input factories, providers, or the selected command; application
|
||||
startup remains application-owned. See the
|
||||
[consumer guide](consumers/guide.md#shell-completion) for installation and
|
||||
hint declarations. Script generation follows the [reporting policy](#reporting),
|
||||
including quiet mode.
|
||||
|
||||
## Reporting
|
||||
|
||||
CommandKit applies one shared reporting policy to application handlers,
|
||||
|
||||
@@ -87,6 +87,34 @@ values convert before input construction. A command's `NewInput` creates input
|
||||
only when execution reaches it; help does not construct caller input or run
|
||||
provider callbacks.
|
||||
|
||||
## Shell completion
|
||||
|
||||
Install `commandkit.Completion("app")` alongside the application's other
|
||||
declarations, using the basename operators actually type. The basename accepts
|
||||
letters, digits, underscores, dots, hyphens, and colons, except for a leading
|
||||
hyphen. Add a hint to a flag or operand's metadata when a value has useful
|
||||
static choices or is a filesystem path:
|
||||
|
||||
```go
|
||||
commandkit.FlagMeta{
|
||||
Name: "format",
|
||||
Completion: commandkit.CompleteChoices(
|
||||
commandkit.CompletionChoice{Value: "text", Description: "Readable text"},
|
||||
commandkit.CompletionChoice{Value: "yaml", Description: "YAML output"},
|
||||
),
|
||||
}
|
||||
commandkit.FlagMeta{Name: "config", Completion: commandkit.CompleteFiles()}
|
||||
commandkit.ArgumentMeta{Name: "directory", Completion: commandkit.CompleteDirectories()}
|
||||
```
|
||||
|
||||
Hints are suggestions: converters and input validation still decide which
|
||||
values are accepted. An omitted hint or `NoCompletion()` gives no value
|
||||
suggestions. `CompleteChoices()` with no choices also gives no suggestions.
|
||||
Declarations retain copies of supplied choices. The [CLI reference](../cli.md#shell-completion)
|
||||
describes supported shells and loading scripts; the [core example](../../examples/core)
|
||||
and [composed example](../../integrations/ckprompt/examples/composed) show
|
||||
installation alongside aliases and integration commands.
|
||||
|
||||
## Invocation reporting
|
||||
|
||||
`Invocation.Reporter()` supplies the reporter for the current handler. Use
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Shell completion
|
||||
|
||||
Status: initial scope and all four product decisions selected; implementation
|
||||
has not started. Provide canonical suggestions with alias-aware interpretation
|
||||
for Bash, Zsh, and Fish through explicitly installed completion support.
|
||||
Status: the selected initial scope is implemented; final sprint validation
|
||||
remains. Canonical suggestions and alias-aware interpretation are available for
|
||||
Bash, Zsh, and Fish through explicitly installed completion support.
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -14,7 +14,7 @@ Completion helps an operator construct an invocation. It does not execute the
|
||||
selected application command. Preserve application ownership of runtime behavior
|
||||
and upstream ownership of configuration and prompt semantics.
|
||||
|
||||
## Current baseline
|
||||
## Planning baseline
|
||||
|
||||
Reviewed baseline: `2301872` (`Stop flag interpretation at unknown flags`).
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ includes suggesting command and flag alias spellings and valid continuations of
|
||||
alias-only prefixes. For example, completion could suggest `inspect` after
|
||||
`app legacy ` when `legacy inspect` is an installed alias.
|
||||
|
||||
Alias discovery may be omitted from the MVP. This deferral does not establish a
|
||||
Alias discovery is outside the initial scope. This deferral does not establish a
|
||||
permanent canonical-only completion contract or prohibit future alias discovery.
|
||||
A later design should determine how alias suggestions are presented alongside
|
||||
canonical suggestions while ensuring inserted continuations form valid next
|
||||
@@ -46,7 +46,8 @@ operator or consumer need and an appropriate ownership/behavior decision:
|
||||
|
||||
- JSON reports and output-file management or reloadable export.
|
||||
- Shell-completion extensions beyond the initial scope in the
|
||||
[completion roadmap](completion.md), including alias discovery described above.
|
||||
[completion roadmap](completion.md), including alias discovery described above
|
||||
and dynamic discovery of configured or remote values.
|
||||
- Configuration mutation, generic value assignment, and exhaustive field-origin
|
||||
inventories.
|
||||
- Credential diagnostics beyond PromptKit's native smoke checks and additional
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Shell completion implementation plan
|
||||
|
||||
Status: Stages 1–6 complete. Stage 7 is next.
|
||||
Status: Stages 1–7 complete. Stage 8 is next.
|
||||
|
||||
Implement the [shell completion roadmap](completion.md) from baseline
|
||||
`2301872`. Follow the stages in order in one sprint. Intermediate stages may
|
||||
@@ -274,6 +274,8 @@ interactive Bash, Zsh, and Fish transport matrix pass.
|
||||
|
||||
## Stage 7: Annotate maintained commands and examples
|
||||
|
||||
Status: complete.
|
||||
|
||||
1. Annotate static format/scope flags where their choices already come from
|
||||
fixed command metadata or literals. Use the same values as the existing
|
||||
version, config, and PromptKit command implementations; omit scope hints
|
||||
@@ -291,6 +293,16 @@ interactive Bash, Zsh, and Fish transport matrix pass.
|
||||
update the feature status and keep alias discovery and dynamic discovery
|
||||
in deferred work.
|
||||
|
||||
Completion: annotated built-in version, configuration, and PromptKit formats,
|
||||
supported configuration scopes, and filename selections. The core and composed
|
||||
examples install completion and demonstrate choices, paths, and entered
|
||||
aliases. Integration tests verify scope availability and completion without
|
||||
input, state, acquisition, comparison, engine, or client callbacks. GoDoc, CLI,
|
||||
consumer, example, and roadmap documentation now describe the delivered scope;
|
||||
alias and dynamic discovery remain deferred. Root and both integration package
|
||||
tests pass in the local workspace, and changed Markdown links and whitespace
|
||||
checks pass.
|
||||
|
||||
## Stage 8: Validate the assembled sprint
|
||||
|
||||
1. Exercise the generated scripts in Bash, Zsh, and Fish using a built fixture
|
||||
|
||||
@@ -7,6 +7,8 @@ go run ./examples/core hello --times=0 --tag=first
|
||||
go run ./examples/core ws greet --count=2
|
||||
go run ./examples/core ws --help
|
||||
go run ./examples/core hello --hlep
|
||||
go build -o core-example ./examples/core
|
||||
./core-example completion bash
|
||||
```
|
||||
|
||||
The example installs a canonical `workspace greet` command together with a
|
||||
@@ -16,6 +18,11 @@ alias. Its declaration slice mixes the canonical group and routing rules as
|
||||
`Invocation.CanonicalPath()` so the alternate route and its canonical target
|
||||
are visible in the output.
|
||||
|
||||
The example also installs `Completion("core-example")` and gives the `report`
|
||||
scenario argument static choices. Build it with the basename shown above, load
|
||||
the script for your shell using the [CLI reference](../../docs/cli.md#shell-completion),
|
||||
and try completing `core-example report d` or `core-example ws gr`.
|
||||
|
||||
Integration factories also return `commandkit.Command` and can be passed
|
||||
directly to `New` beside `commandkit.Alias` or flag-alias declarations. For a
|
||||
command-only `[]commandkit.Command`, convert its elements into a
|
||||
|
||||
@@ -64,7 +64,9 @@ func newApp() (*commandkit.App, error) {
|
||||
return reportInput{Scenario: "demo"}
|
||||
},
|
||||
Arguments: []commandkit.Argument[reportInput]{
|
||||
commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "scenario", Usage: "demo, recover, warn, warn-error, fail, or partial."}, false, func(value string) (string, error) { return value, nil },
|
||||
commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "scenario", Usage: "demo, recover, warn, warn-error, fail, or partial.", Completion: commandkit.CompleteChoices(
|
||||
commandkit.CompletionChoice{Value: "demo"}, commandkit.CompletionChoice{Value: "recover"}, commandkit.CompletionChoice{Value: "warn"},
|
||||
commandkit.CompletionChoice{Value: "warn-error"}, commandkit.CompletionChoice{Value: "fail"}, commandkit.CompletionChoice{Value: "partial"})}, false, func(value string) (string, error) { return value, nil },
|
||||
func(input *reportInput, value commandkit.Supplied[string]) { input.Scenario = value.Value }),
|
||||
},
|
||||
CheckInput: func(input reportInput) error {
|
||||
@@ -83,6 +85,7 @@ func newApp() (*commandkit.App, error) {
|
||||
declarations := []commandkit.Declaration{
|
||||
workspace,
|
||||
report,
|
||||
commandkit.Completion("core-example"),
|
||||
commandkit.Alias("hello", "workspace greet"),
|
||||
commandkit.Alias("ws", "workspace"),
|
||||
commandkit.FlagAliasFor("workspace greet", "--times", "--count"),
|
||||
|
||||
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