Add filtered PromptKit catalog commands
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Dependency upgrades, discovery, and smoke testing implementation plan
|
# Dependency upgrades, discovery, and smoke testing implementation plan
|
||||||
|
|
||||||
Status: Stages 1–6 complete; Stage 7 is next.
|
Status: Stages 1–7 complete; Stage 8 is next.
|
||||||
|
|
||||||
## Authority and baseline
|
## Authority and baseline
|
||||||
|
|
||||||
@@ -423,6 +423,14 @@ filter conversion, and collection optional versions; run focused catalog tests.
|
|||||||
**Complete when:** the accepted catalog CLI and reports invoke the intended
|
**Complete when:** the accepted catalog CLI and reports invoke the intended
|
||||||
callbacks through the standard binding system.
|
callbacks through the standard binding system.
|
||||||
|
|
||||||
|
**Completed:** 2026-10-17. `promptkit list` now installs each supplied catalog
|
||||||
|
independently. Prompts, profiles, and backends bind namespace and all-namespace
|
||||||
|
filters; namespaces bind repeatable native resource kinds; collections use
|
||||||
|
their unfiltered operation and project optional release versions. Added
|
||||||
|
capturing-callback coverage for filter mapping, false and repeated values,
|
||||||
|
collection YAML presence, and conflicting filter preflight. The full ckprompt
|
||||||
|
suite and `GOTOOLCHAIN=local go build ./...` passed.
|
||||||
|
|
||||||
## Stage 8: Verify discovery availability, lifetimes, and outcomes
|
## Stage 8: Verify discovery availability, lifetimes, and outcomes
|
||||||
|
|
||||||
**Primary files:** focused ckprompt catalog command/adapter tests.
|
**Primary files:** focused ckprompt catalog command/adapter tests.
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ type definitionInput[I any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type catalogInput[I any] struct {
|
type catalogInput[I any] struct {
|
||||||
selection ckconfig.Selection
|
selection ckconfig.Selection
|
||||||
format string
|
namespace commandkit.Supplied[string]
|
||||||
captured commandkit.FlagValues[I]
|
allNamespaces commandkit.Supplied[bool]
|
||||||
selected I
|
kinds []promptkit.ResourceKind
|
||||||
|
filter promptkit.NamespaceFilter
|
||||||
|
format string
|
||||||
|
captured commandkit.FlagValues[I]
|
||||||
|
selected I
|
||||||
}
|
}
|
||||||
|
|
||||||
type requirementsInput[I any] struct {
|
type requirementsInput[I any] struct {
|
||||||
@@ -48,7 +52,7 @@ type inputAssignment struct {
|
|||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command captures the optional PromptKit catalog, named inspection,
|
// Command captures the optional PromptKit catalogs, named inspection,
|
||||||
// requirements discovery, and concrete request validation callbacks and adds
|
// requirements discovery, and concrete request validation callbacks and adds
|
||||||
// only supplied operations. Discovery and validation share the
|
// only supplied operations. Discovery and validation share the
|
||||||
// `requirements` group and remain independently optional. An absent suite
|
// `requirements` group and remain independently optional. An absent suite
|
||||||
@@ -63,7 +67,7 @@ type inputAssignment struct {
|
|||||||
// render YAML with `yaml` as the only format and default. Named-show formats are
|
// render YAML with `yaml` as the only format and default. Named-show formats are
|
||||||
// unchanged.
|
// unchanged.
|
||||||
func Command[I, S any](suite Suite[I, S]) commandkit.Command {
|
func Command[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||||
if suite.InspectPrompt == nil && suite.InspectProfile == nil && suite.InspectRequirements == nil && suite.ValidateRequest == nil && suite.ListPrompts == nil && suite.ListProfiles == nil {
|
if suite.InspectPrompt == nil && suite.InspectProfile == nil && suite.InspectRequirements == nil && suite.ValidateRequest == nil && suite.ListPrompts == nil && suite.ListProfiles == nil && suite.ListBackends == nil && suite.ListNamespaces == nil && suite.ListCollections == nil {
|
||||||
command := commandkit.NoCommand()
|
command := commandkit.NoCommand()
|
||||||
for _, issue := range suite.issues {
|
for _, issue := range suite.issues {
|
||||||
command = commandkit.WithError(command, issue)
|
command = commandkit.WithError(command, issue)
|
||||||
@@ -83,12 +87,21 @@ func Command[I, S any](suite Suite[I, S]) commandkit.Command {
|
|||||||
if len(showCommands) != 0 {
|
if len(showCommands) != 0 {
|
||||||
rootCommands = append(rootCommands, commandkit.Group("show", "Show a named PromptKit definition.", showCommands...))
|
rootCommands = append(rootCommands, commandkit.Group("show", "Show a named PromptKit definition.", showCommands...))
|
||||||
}
|
}
|
||||||
listCommands := make([]commandkit.Command, 0, 2)
|
listCommands := make([]commandkit.Command, 0, 5)
|
||||||
if suite.ListPrompts != nil {
|
if suite.ListPrompts != nil {
|
||||||
listCommands = append(listCommands, newCatalogListCommand(suite, "prompts", true))
|
listCommands = append(listCommands, newCatalogListCommand(suite, "prompts"))
|
||||||
}
|
}
|
||||||
if suite.ListProfiles != nil {
|
if suite.ListProfiles != nil {
|
||||||
listCommands = append(listCommands, newCatalogListCommand(suite, "profiles", false))
|
listCommands = append(listCommands, newCatalogListCommand(suite, "profiles"))
|
||||||
|
}
|
||||||
|
if suite.ListBackends != nil {
|
||||||
|
listCommands = append(listCommands, newCatalogListCommand(suite, "backends"))
|
||||||
|
}
|
||||||
|
if suite.ListNamespaces != nil {
|
||||||
|
listCommands = append(listCommands, newCatalogListCommand(suite, "namespaces"))
|
||||||
|
}
|
||||||
|
if suite.ListCollections != nil {
|
||||||
|
listCommands = append(listCommands, newCatalogListCommand(suite, "collections"))
|
||||||
}
|
}
|
||||||
if len(listCommands) != 0 {
|
if len(listCommands) != 0 {
|
||||||
rootCommands = append(rootCommands, commandkit.Group("list", "List PromptKit catalogs.", listCommands...))
|
rootCommands = append(rootCommands, commandkit.Group("list", "List PromptKit catalogs.", listCommands...))
|
||||||
@@ -110,7 +123,7 @@ func Command[I, S any](suite Suite[I, S]) commandkit.Command {
|
|||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCatalogListCommand[I, S any](suite Suite[I, S], name string, prompts bool) commandkit.Command {
|
func newCatalogListCommand[I, S any](suite Suite[I, S], name string) commandkit.Command {
|
||||||
flags := commandkit.CaptureFlags(suite.Selectors, func(input **catalogInput[I]) *commandkit.FlagValues[I] {
|
flags := commandkit.CaptureFlags(suite.Selectors, func(input **catalogInput[I]) *commandkit.FlagValues[I] {
|
||||||
if input == nil || *input == nil {
|
if input == nil || *input == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -125,6 +138,23 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string, prompts boo
|
|||||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Catalog format: yaml."}, stringDefault("yaml"), parseDefinitionString,
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Catalog format: 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" {
|
||||||
|
flags = append(flags,
|
||||||
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "namespace", Usage: "Select one native namespace."}, (*string)(nil), parseDefinitionString,
|
||||||
|
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).namespace = value }),
|
||||||
|
commandkit.BoolFlag(commandkit.FlagMeta{Name: "all-namespaces", Usage: "Select identities from every namespace."}, false,
|
||||||
|
func(input **catalogInput[I], value commandkit.Supplied[bool]) { (*input).allNamespaces = value }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if name == "namespaces" {
|
||||||
|
flags = append(flags,
|
||||||
|
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "kind", Usage: "Resource kind to include."}, []promptkit.ResourceKind(nil),
|
||||||
|
func(value string) (promptkit.ResourceKind, error) { return promptkit.ResourceKind(value), nil },
|
||||||
|
func(input **catalogInput[I], value commandkit.Supplied[[]promptkit.ResourceKind]) {
|
||||||
|
(*input).kinds = value.Value
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
return commandkit.NewCommand(commandkit.CommandSpec[*catalogInput[I]]{
|
return commandkit.NewCommand(commandkit.CommandSpec[*catalogInput[I]]{
|
||||||
Name: name,
|
Name: name,
|
||||||
Summary: "List PromptKit " + name + ".",
|
Summary: "List PromptKit " + name + ".",
|
||||||
@@ -139,6 +169,9 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string, prompts boo
|
|||||||
if err := checkCatalogFormat(input.format); err != nil {
|
if err := checkCatalogFormat(input.format); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := prepareCatalogFilter(input, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
selected, err := prepareCatalogInput(suite, input)
|
selected, err := prepareCatalogInput(suite, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -147,14 +180,53 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string, prompts boo
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: func(invocation commandkit.Invocation, input *catalogInput[I]) commandkit.Outcome {
|
Run: func(invocation commandkit.Invocation, input *catalogInput[I]) commandkit.Outcome {
|
||||||
if prompts {
|
switch name {
|
||||||
|
case "prompts":
|
||||||
return runCatalogList(invocation, suite, input, suite.ListPrompts, encodePromptCatalog)
|
return runCatalogList(invocation, suite, input, suite.ListPrompts, encodePromptCatalog)
|
||||||
|
case "profiles":
|
||||||
|
return runCatalogList(invocation, suite, input, suite.ListProfiles, encodeProfileCatalog)
|
||||||
|
case "backends":
|
||||||
|
return runCatalogList(invocation, suite, input, suite.ListBackends, encodeProfileCatalog)
|
||||||
|
case "namespaces":
|
||||||
|
list := func(ctx context.Context, request CatalogRequest, value I, state S) (commandkit.Acquired[[]string], error) {
|
||||||
|
return suite.ListNamespaces(ctx, NamespaceRequest{Selection: request.Selection, Kinds: input.kinds}, value, state)
|
||||||
|
}
|
||||||
|
return runCatalogList(invocation, suite, input, list, encodeProfileCatalog)
|
||||||
|
case "collections":
|
||||||
|
list := func(ctx context.Context, request CatalogRequest, value I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
|
||||||
|
return suite.ListCollections(ctx, request.Selection, value, state)
|
||||||
|
}
|
||||||
|
return runCatalogList(invocation, suite, input, list, encodeCollectionCatalog)
|
||||||
|
default:
|
||||||
|
return definitionFailure(fmt.Errorf("unknown PromptKit catalog %q", name), nil)
|
||||||
}
|
}
|
||||||
return runCatalogList(invocation, suite, input, suite.ListProfiles, encodeProfileCatalog)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareCatalogFilter[I any](input *catalogInput[I], name string) error {
|
||||||
|
input.filter = promptkit.NamespaceFilter{Mode: promptkit.NamespaceDefault}
|
||||||
|
if name != "prompts" && name != "profiles" && name != "backends" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if input.allNamespaces.Set && input.allNamespaces.Value && input.namespace.Set {
|
||||||
|
return errors.New("--namespace cannot be combined with --all-namespaces")
|
||||||
|
}
|
||||||
|
if input.allNamespaces.Set && input.allNamespaces.Value {
|
||||||
|
input.filter.Mode = promptkit.NamespaceAll
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if input.namespace.Set {
|
||||||
|
if input.namespace.Value == "" {
|
||||||
|
input.filter.Mode = promptkit.NamespaceUnqualified
|
||||||
|
} else {
|
||||||
|
input.filter.Mode = promptkit.NamespaceNamed
|
||||||
|
input.filter.Name = input.namespace.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkCatalogFormat(format string) error {
|
func checkCatalogFormat(format string) error {
|
||||||
if format != "yaml" {
|
if format != "yaml" {
|
||||||
return fmt.Errorf("unsupported catalog format %q", format)
|
return fmt.Errorf("unsupported catalog format %q", format)
|
||||||
@@ -328,7 +400,7 @@ func runCatalogList[T any, I, S any](
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acquired, err := list(invocation.Context, CatalogRequest{Selection: input.selection}, input.selected, state)
|
acquired, err := list(invocation.Context, CatalogRequest{Selection: input.selection, Filter: input.filter}, input.selected, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return definitionFailure(err, acquired.Diagnostics)
|
return definitionFailure(err, acquired.Diagnostics)
|
||||||
}
|
}
|
||||||
@@ -675,6 +747,19 @@ func encodeProfileCatalog(profiles []string) ([]byte, error) {
|
|||||||
return encodePromptYAMLBytes(profiles)
|
return encodePromptYAMLBytes(profiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type collectionCatalogEntry struct {
|
||||||
|
Namespace string `yaml:"namespace"`
|
||||||
|
Version *string `yaml:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeCollectionCatalog(collections []promptkit.CollectionInfo) ([]byte, error) {
|
||||||
|
entries := make([]collectionCatalogEntry, len(collections))
|
||||||
|
for index, collection := range collections {
|
||||||
|
entries[index] = collectionCatalogEntry{Namespace: collection.Namespace, Version: collection.ReleaseVersion}
|
||||||
|
}
|
||||||
|
return encodePromptYAMLBytes(entries)
|
||||||
|
}
|
||||||
|
|
||||||
func encodePromptYAMLBytes(value any) ([]byte, error) {
|
func encodePromptYAMLBytes(value any) ([]byte, error) {
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
if err := encodePromptYAML(&output, value); err != nil {
|
if err := encodePromptYAML(&output, value); err != nil {
|
||||||
|
|||||||
@@ -275,6 +275,94 @@ func TestCatalogCommandTreesAreIndependentAndOmitShow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDiscoveryCommandsForwardFiltersAndRenderCollections(t *testing.T) {
|
||||||
|
var promptRequest, profileRequest, backendRequest CatalogRequest
|
||||||
|
var namespaceRequest NamespaceRequest
|
||||||
|
var collectionSelection ckconfig.Selection
|
||||||
|
providers := 0
|
||||||
|
suite := Suite[struct{}, struct{}]{
|
||||||
|
ListPrompts: func(_ context.Context, request CatalogRequest, _, _ struct{}) (commandkit.Acquired[[]promptkit.PromptIdentity], error) {
|
||||||
|
providers++
|
||||||
|
promptRequest = request
|
||||||
|
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: []promptkit.PromptIdentity{{ID: "team/briefing", Version: testStringPointer("")}}}, nil
|
||||||
|
},
|
||||||
|
ListProfiles: func(_ context.Context, request CatalogRequest, _, _ struct{}) (commandkit.Acquired[[]string], error) {
|
||||||
|
providers++
|
||||||
|
profileRequest = request
|
||||||
|
return commandkit.Acquired[[]string]{Value: []string{"default"}}, nil
|
||||||
|
},
|
||||||
|
ListBackends: func(_ context.Context, request CatalogRequest, _, _ struct{}) (commandkit.Acquired[[]string], error) {
|
||||||
|
providers++
|
||||||
|
backendRequest = request
|
||||||
|
return commandkit.Acquired[[]string]{Value: []string{"local"}}, nil
|
||||||
|
},
|
||||||
|
ListNamespaces: func(_ context.Context, request NamespaceRequest, _, _ struct{}) (commandkit.Acquired[[]string], error) {
|
||||||
|
providers++
|
||||||
|
namespaceRequest = request
|
||||||
|
return commandkit.Acquired[[]string]{Value: []string{"team", "root"}}, nil
|
||||||
|
},
|
||||||
|
ListCollections: func(_ context.Context, selection ckconfig.Selection, _, _ struct{}) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
|
||||||
|
providers++
|
||||||
|
collectionSelection = selection
|
||||||
|
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: []promptkit.CollectionInfo{
|
||||||
|
{Namespace: "team", ReleaseVersion: testStringPointer("v1")},
|
||||||
|
{Namespace: "empty-version", ReleaseVersion: testStringPointer("")},
|
||||||
|
{Namespace: "unversioned"},
|
||||||
|
}}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app := newPromptTestApp(t, Command(suite))
|
||||||
|
run := func(args ...string) (commandkit.Outcome, string) {
|
||||||
|
t.Helper()
|
||||||
|
var output bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
return outcome, output.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome, output := run("promptkit", "list", "prompts", "--namespace", "first", "--namespace", "team", "--all-namespaces=false", "--config", "selected.yml")
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || promptRequest.Filter != (promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"}) || !promptRequest.Selection.Config.Set || promptRequest.Selection.Config.Value != "selected.yml" {
|
||||||
|
t.Fatalf("prompt list outcome/request = (%#v, %#v), output=%q", outcome, promptRequest, output)
|
||||||
|
}
|
||||||
|
var promptRows []map[string]any
|
||||||
|
if err := yaml.Unmarshal([]byte(output), &promptRows); err != nil || len(promptRows) != 1 || promptRows[0]["id"] != "team/briefing" || promptRows[0]["version"] != "" {
|
||||||
|
t.Fatalf("prompt catalog YAML = %#v, error=%v, output=%q", promptRows, err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome, _ = run("promptkit", "list", "profiles", "--namespace", "")
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || profileRequest.Filter.Mode != promptkit.NamespaceUnqualified || profileRequest.Filter.Name != "" {
|
||||||
|
t.Fatalf("profile list outcome/filter = (%#v, %#v)", outcome, profileRequest.Filter)
|
||||||
|
}
|
||||||
|
outcome, _ = run("promptkit", "list", "backends", "--all-namespaces")
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || backendRequest.Filter.Mode != promptkit.NamespaceAll {
|
||||||
|
t.Fatalf("backend list outcome/filter = (%#v, %#v)", outcome, backendRequest.Filter)
|
||||||
|
}
|
||||||
|
outcome, output = run("promptkit", "list", "namespaces", "--kind", "prompt", "--kind", "backend")
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(namespaceRequest.Kinds, []promptkit.ResourceKind{promptkit.ResourcePrompt, promptkit.ResourceBackend}) {
|
||||||
|
t.Fatalf("namespace list outcome/kinds = (%#v, %#v)", outcome, namespaceRequest.Kinds)
|
||||||
|
}
|
||||||
|
var namespaces []string
|
||||||
|
if err := yaml.Unmarshal([]byte(output), &namespaces); err != nil || !reflect.DeepEqual(namespaces, []string{"team", "root"}) {
|
||||||
|
t.Fatalf("namespace YAML = %#v, error=%v, output=%q", namespaces, err, output)
|
||||||
|
}
|
||||||
|
outcome, output = run("promptkit", "list", "collections", "--config", "collections.yml")
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || !collectionSelection.Config.Set || collectionSelection.Config.Value != "collections.yml" {
|
||||||
|
t.Fatalf("collection list outcome/selection = (%#v, %#v)", outcome, collectionSelection)
|
||||||
|
}
|
||||||
|
var collections []map[string]any
|
||||||
|
if err := yaml.Unmarshal([]byte(output), &collections); err != nil || len(collections) != 3 || collections[0]["namespace"] != "team" || collections[0]["version"] != "v1" || collections[1]["version"] != "" || collections[2]["version"] != nil {
|
||||||
|
t.Fatalf("collection YAML = %#v, error=%v, output=%q", collections, err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeConflict := providers
|
||||||
|
outcome, _ = run("promptkit", "list", "backends", "--namespace", "team", "--all-namespaces")
|
||||||
|
if outcome.Kind != commandkit.OutcomeUsage || providers != beforeConflict {
|
||||||
|
t.Fatalf("conflicting namespace filters = %#v, provider calls %d -> %d", outcome, beforeConflict, providers)
|
||||||
|
}
|
||||||
|
if providers != 5 {
|
||||||
|
t.Fatalf("catalog providers called = %d, want five", providers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCatalogListUsesPreparedInputAndPreflightsFormat(t *testing.T) {
|
func TestCatalogListUsesPreparedInputAndPreflightsFormat(t *testing.T) {
|
||||||
type input struct{ campaign commandkit.Supplied[string] }
|
type input struct{ campaign commandkit.Supplied[string] }
|
||||||
type state struct{ token int }
|
type state struct{ token int }
|
||||||
|
|||||||
Reference in New Issue
Block a user