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

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

View File

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

View File

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

View File

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

View File

@@ -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`).

View File

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

View File

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

View File

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

View File

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

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