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.",
|
Summary: "Show application or CommandKit version.",
|
||||||
Description: "Report identity and build metadata without loading application configuration.",
|
Description: "Report identity and build metadata without loading application configuration.",
|
||||||
Flags: []Flag[versionInput]{
|
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: "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"}, &formatDefault, parseVersionFormat, func(input *versionInput, value Supplied[string]) { input.Format = 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 {
|
Run: func(invocation Invocation, input versionInput) Outcome {
|
||||||
identity := a.identity
|
identity := a.identity
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ func validCompletionExecutable(name string) bool {
|
|||||||
|
|
||||||
// CompletionChoice is a literal suggested value and its optional description.
|
// CompletionChoice is a literal suggested value and its optional description.
|
||||||
type CompletionChoice struct {
|
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
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,10 @@ const (
|
|||||||
completionDirectories
|
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 {
|
func CompleteChoices(choices ...CompletionChoice) CompletionHint {
|
||||||
return CompletionHint{kind: completionChoices, choices: append([]CompletionChoice(nil), choices...)}
|
return CompletionHint{kind: completionChoices, choices: append([]CompletionChoice(nil), choices...)}
|
||||||
}
|
}
|
||||||
|
|||||||
25
docs/cli.md
25
docs/cli.md
@@ -66,6 +66,31 @@ version: dev
|
|||||||
revision: ""
|
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
|
## Reporting
|
||||||
|
|
||||||
CommandKit applies one shared reporting policy to application handlers,
|
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
|
only when execution reaches it; help does not construct caller input or run
|
||||||
provider callbacks.
|
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 reporting
|
||||||
|
|
||||||
`Invocation.Reporter()` supplies the reporter for the current handler. Use
|
`Invocation.Reporter()` supplies the reporter for the current handler. Use
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Shell completion
|
# Shell completion
|
||||||
|
|
||||||
Status: initial scope and all four product decisions selected; implementation
|
Status: the selected initial scope is implemented; final sprint validation
|
||||||
has not started. Provide canonical suggestions with alias-aware interpretation
|
remains. Canonical suggestions and alias-aware interpretation are available for
|
||||||
for Bash, Zsh, and Fish through explicitly installed completion support.
|
Bash, Zsh, and Fish through explicitly installed completion support.
|
||||||
|
|
||||||
## Purpose
|
## 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
|
selected application command. Preserve application ownership of runtime behavior
|
||||||
and upstream ownership of configuration and prompt semantics.
|
and upstream ownership of configuration and prompt semantics.
|
||||||
|
|
||||||
## Current baseline
|
## Planning baseline
|
||||||
|
|
||||||
Reviewed baseline: `2301872` (`Stop flag interpretation at unknown flags`).
|
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
|
alias-only prefixes. For example, completion could suggest `inspect` after
|
||||||
`app legacy ` when `legacy inspect` is an installed alias.
|
`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.
|
permanent canonical-only completion contract or prohibit future alias discovery.
|
||||||
A later design should determine how alias suggestions are presented alongside
|
A later design should determine how alias suggestions are presented alongside
|
||||||
canonical suggestions while ensuring inserted continuations form valid next
|
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.
|
- JSON reports and output-file management or reloadable export.
|
||||||
- Shell-completion extensions beyond the initial scope in the
|
- 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
|
- Configuration mutation, generic value assignment, and exhaustive field-origin
|
||||||
inventories.
|
inventories.
|
||||||
- Credential diagnostics beyond PromptKit's native smoke checks and additional
|
- Credential diagnostics beyond PromptKit's native smoke checks and additional
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shell completion implementation plan
|
# 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
|
Implement the [shell completion roadmap](completion.md) from baseline
|
||||||
`2301872`. Follow the stages in order in one sprint. Intermediate stages may
|
`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
|
## Stage 7: Annotate maintained commands and examples
|
||||||
|
|
||||||
|
Status: complete.
|
||||||
|
|
||||||
1. Annotate static format/scope flags where their choices already come from
|
1. Annotate static format/scope flags where their choices already come from
|
||||||
fixed command metadata or literals. Use the same values as the existing
|
fixed command metadata or literals. Use the same values as the existing
|
||||||
version, config, and PromptKit command implementations; omit scope hints
|
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
|
update the feature status and keep alias discovery and dynamic discovery
|
||||||
in deferred work.
|
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
|
## Stage 8: Validate the assembled sprint
|
||||||
|
|
||||||
1. Exercise the generated scripts in Bash, Zsh, and Fish using a built fixture
|
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 greet --count=2
|
||||||
go run ./examples/core ws --help
|
go run ./examples/core ws --help
|
||||||
go run ./examples/core hello --hlep
|
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
|
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
|
`Invocation.CanonicalPath()` so the alternate route and its canonical target
|
||||||
are visible in the output.
|
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
|
Integration factories also return `commandkit.Command` and can be passed
|
||||||
directly to `New` beside `commandkit.Alias` or flag-alias declarations. For a
|
directly to `New` beside `commandkit.Alias` or flag-alias declarations. For a
|
||||||
command-only `[]commandkit.Command`, convert its elements into a
|
command-only `[]commandkit.Command`, convert its elements into a
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ func newApp() (*commandkit.App, error) {
|
|||||||
return reportInput{Scenario: "demo"}
|
return reportInput{Scenario: "demo"}
|
||||||
},
|
},
|
||||||
Arguments: []commandkit.Argument[reportInput]{
|
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 }),
|
func(input *reportInput, value commandkit.Supplied[string]) { input.Scenario = value.Value }),
|
||||||
},
|
},
|
||||||
CheckInput: func(input reportInput) error {
|
CheckInput: func(input reportInput) error {
|
||||||
@@ -83,6 +85,7 @@ func newApp() (*commandkit.App, error) {
|
|||||||
declarations := []commandkit.Declaration{
|
declarations := []commandkit.Declaration{
|
||||||
workspace,
|
workspace,
|
||||||
report,
|
report,
|
||||||
|
commandkit.Completion("core-example"),
|
||||||
commandkit.Alias("hello", "workspace greet"),
|
commandkit.Alias("hello", "workspace greet"),
|
||||||
commandkit.Alias("ws", "workspace"),
|
commandkit.Alias("ws", "workspace"),
|
||||||
commandkit.FlagAliasFor("workspace greet", "--times", "--count"),
|
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",
|
Name: "scopes",
|
||||||
Summary: "List configuration scopes and supported operations.",
|
Summary: "List configuration scopes and supported operations.",
|
||||||
Flags: []commandkit.Flag[scopeInventoryInput]{commandkit.ValueFlag(
|
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 },
|
func(input *scopeInventoryInput, value commandkit.Supplied[string]) { input.Format = value.Value },
|
||||||
)},
|
)},
|
||||||
CheckInput: func(input scopeInventoryInput) error { return checkConfigFormat(input.Format) },
|
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 {
|
if operation == Show || operation == Diff {
|
||||||
defaultFormat = "yaml"
|
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]]{
|
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 }),
|
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 }),
|
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]) {
|
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Config = value
|
(*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]) {
|
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Overlay = value
|
(*input).selection.Overlay = value
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
if operation == Diff {
|
if operation == Diff {
|
||||||
flags = append(flags,
|
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]) {
|
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||||
(*input).against.Config = value
|
(*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]) {
|
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
|
||||||
(*input).against.Overlay = value
|
(*input).against.Overlay = value
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -134,11 +134,11 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string) commandkit.
|
|||||||
return &(*input).captured
|
return &(*input).captured
|
||||||
})
|
})
|
||||||
flags = append(flags,
|
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 }),
|
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 }),
|
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 }),
|
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
|
||||||
)
|
)
|
||||||
if name == "prompts" || name == "profiles" || name == "backends" {
|
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
|
return &(*input).captured
|
||||||
})
|
})
|
||||||
flags = append(flags,
|
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 }),
|
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]) {
|
func(input **definitionInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Overlay = value
|
(*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 }),
|
func(input **definitionInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
|
||||||
)
|
)
|
||||||
spec := commandkit.CommandSpec[*definitionInput[I]]{
|
spec := commandkit.CommandSpec[*definitionInput[I]]{
|
||||||
@@ -302,11 +302,11 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
|||||||
return &(*input).captured
|
return &(*input).captured
|
||||||
})
|
})
|
||||||
flags = append(flags,
|
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]) {
|
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Config = value
|
(*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]) {
|
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Overlay = value
|
(*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]) {
|
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).inspection.SessionID = value.Value
|
(*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 }),
|
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,
|
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "var", Usage: "Caller variable assignment KEY=VALUE."}, []variableAssignment(nil), parseVariableAssignment,
|
||||||
func(input **requirementsInput[I], value commandkit.Supplied[[]variableAssignment]) {
|
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
|
`--hlep` is available globally. The `smoke` shortcut retains the canonical
|
||||||
`promptkit test KIND` operand, including the special `all` aggregate.
|
`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)`
|
The composition calls `ckconfig.Command(suite)` and `ckprompt.Command(definitions)`
|
||||||
directly in a `[]commandkit.Declaration` with root-installed routing rules.
|
directly in a `[]commandkit.Declaration` with root-installed routing rules.
|
||||||
Group children remain canonical `Command` values. For command-only slices,
|
Group children remain canonical `Command` values. For command-only slices,
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ func newAppWithEngineLoader(
|
|||||||
declarations := []commandkit.Declaration{
|
declarations := []commandkit.Declaration{
|
||||||
configCommand,
|
configCommand,
|
||||||
definitionCommand,
|
definitionCommand,
|
||||||
|
commandkit.Completion("composed-example"),
|
||||||
commandkit.Group("session", "Manage saved sessions.", restore),
|
commandkit.Group("session", "Manage saved sessions.", restore),
|
||||||
commandkit.Alias("list collections", "promptkit list collections"),
|
commandkit.Alias("list collections", "promptkit list collections"),
|
||||||
commandkit.Alias("promptkit list bundles", "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) {
|
func TestComposedSmokeReportSurvivesInferenceFailure(t *testing.T) {
|
||||||
wantCause := errors.New("local example inference failed")
|
wantCause := errors.New("local example inference failed")
|
||||||
smokeTest := func(_ context.Context, request ckprompt.SmokeRequest, _ *inspectionInput, _ *runCache) (commandkit.Acquired[*promptkit.SmokeTestReport], error) {
|
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
|
return &(*input).captured
|
||||||
})
|
})
|
||||||
flags = append(flags,
|
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]) {
|
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Config = value
|
(*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]) {
|
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).selection.Overlay = value
|
(*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 }),
|
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,
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "backend", Usage: "PromptKit backend ID."}, (*string)(nil), parseDefinitionString,
|
||||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
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
|
return &(*input).captured
|
||||||
})
|
})
|
||||||
flags = append(flags,
|
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]) {
|
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).request.Selection.Config = value
|
(*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]) {
|
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).request.Selection.Overlay = value
|
(*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]) {
|
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).request.Request.SessionID = value.Value
|
(*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]) {
|
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||||
(*input).format = value.Value
|
(*input).format = value.Value
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user