Complete CommandKit consumer documentation
This commit is contained in:
@@ -11,10 +11,16 @@ and path explanation reports. Applications can register a PromptKit engine
|
|||||||
configuration scope independently of PromptKit definition commands, and can
|
configuration scope independently of PromptKit definition commands, and can
|
||||||
install optional prompt and profile inspection commands.
|
install optional prompt and profile inspection commands.
|
||||||
|
|
||||||
|
The [consumer guide](docs/consumers/guide.md) links to two maintained examples:
|
||||||
|
[simple](examples/simple/README.md) and
|
||||||
|
[composed](examples/composed/README.md).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Contributor guide](docs/development.md)
|
- [Contributor guide](docs/development.md)
|
||||||
- [Command line reference](docs/cli.md)
|
- [Command line reference](docs/cli.md)
|
||||||
|
- [Consumer guide](docs/consumers/guide.md)
|
||||||
|
- [Internal package overview](docs/internal/overview.md)
|
||||||
- [Architecture policy](docs/policy/architecture.md)
|
- [Architecture policy](docs/policy/architecture.md)
|
||||||
- [CLI standard proposal](docs/roadmap/01-cli-plan.md)
|
- [CLI standard proposal](docs/roadmap/01-cli-plan.md)
|
||||||
- [Dispatcher implementation roadmap](docs/roadmap/02-dispatch.md)
|
- [Dispatcher implementation roadmap](docs/roadmap/02-dispatch.md)
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ type configInput[C any] struct {
|
|||||||
selectors []selectorState[C]
|
selectors []selectorState[C]
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterConfig installs the config family and its scope discovery command.
|
// RegisterConfig installs the available config handlers and the scope discovery
|
||||||
|
// command. Its registration and discovery paths do not invoke scope loaders.
|
||||||
func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
||||||
if app == nil {
|
if app == nil {
|
||||||
return fmt.Errorf("cannot register configuration on a nil app")
|
return fmt.Errorf("cannot register configuration on a nil app")
|
||||||
@@ -182,7 +183,11 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigKitScope adapts a ConfigKit loader into a configuration scope.
|
// NewConfigKitScope adapts a ConfigKit loader into a scope with validation,
|
||||||
|
// effective display, source listing, and path explanation capabilities. The
|
||||||
|
// loader runs once per selected operation. Successful loads expose the result's
|
||||||
|
// public facets directly; ConfigKit LoadError records become CommandKit
|
||||||
|
// diagnostics while the original error is returned.
|
||||||
func NewConfigKitScope[C, T any](name, summary string, load func(context.Context, Selection, C) (*configkit.Result[T], error)) (ConfigScope[C], error) {
|
func NewConfigKitScope[C, T any](name, summary string, load func(context.Context, Selection, C) (*configkit.Result[T], error)) (ConfigScope[C], error) {
|
||||||
if load == nil {
|
if load == nil {
|
||||||
return ConfigScope[C]{}, fmt.Errorf("ConfigKit scope loader must not be nil")
|
return ConfigScope[C]{}, fmt.Errorf("ConfigKit scope loader must not be nil")
|
||||||
|
|||||||
4
doc.go
4
doc.go
@@ -1,3 +1,3 @@
|
|||||||
// Package commandkit defines public identity, stream, invocation, and outcome
|
// Package commandkit provides an instance-owned CLI dispatcher with typed
|
||||||
// types for the CommandKit Go library.
|
// command declarations, configuration scopes, and PromptKit inspection.
|
||||||
package commandkit
|
package commandkit
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ and asks PromptKit to inspect the named definition. Inspection reads the
|
|||||||
engine's captured configuration; it does not generate a prompt, contact a
|
engine's captured configuration; it does not generate a prompt, contact a
|
||||||
provider, or look up credentials.
|
provider, or look up credentials.
|
||||||
Prompt definitions and profile resolution remain owned by
|
Prompt definitions and profile resolution remain owned by
|
||||||
[PromptKit](https://pkg.go.dev/gitea.maximumdirect.net/eric/promptkit).
|
[PromptKit's inspection API](https://gitea.maximumdirect.net/eric/promptkit/src/commit/4b8169da286917fde7875425bb32b799b12c6af3/engine.go).
|
||||||
|
|
||||||
```text
|
```text
|
||||||
example promptkit show prompt customer/summary --version 2 --format yaml
|
example promptkit show prompt customer/summary --version 2 --format yaml
|
||||||
|
|||||||
74
docs/consumers/guide.md
Normal file
74
docs/consumers/guide.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Consumer guide
|
||||||
|
|
||||||
|
CommandKit's supported Go API is the root `commandkit` package. Start with an
|
||||||
|
`App`, register CommandKit's standard inspection families, register application
|
||||||
|
commands, and call `App.Execute` with the caller's context and streams. The
|
||||||
|
caller retains process startup, signal handling, and exit mapping.
|
||||||
|
|
||||||
|
## A configuration scope
|
||||||
|
|
||||||
|
Use [`NewConfigKitScope`](../../config.go) to adapt one ConfigKit loader. The
|
||||||
|
loader receives the invocation's explicit `Selection`; the application decides
|
||||||
|
how those supplied config and overlay values map to its inputs.
|
||||||
|
|
||||||
|
```go
|
||||||
|
scope, err := commandkit.NewConfigKitScope[appContext, appConfig](
|
||||||
|
"app",
|
||||||
|
"Application settings.",
|
||||||
|
func(ctx context.Context, selection commandkit.Selection, app appContext) (*configkit.Result[appConfig], error) {
|
||||||
|
return loadAppConfig(ctx, selection, app)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = commandkit.RegisterConfig(app, commandkit.ConfigSuite[appContext]{
|
||||||
|
Scopes: []commandkit.ConfigScope[appContext]{scope},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`config show`, `config sources`, and `config explain` use the configuration
|
||||||
|
captured by that load. A scope can support only the operations it provides.
|
||||||
|
When several scopes share invocation-local application state, create it in
|
||||||
|
`ConfigSuite.NewContext`; use typed `Selector` bindings to copy only the values
|
||||||
|
that apply to each scope. See the [composed example](../../examples/composed/README.md)
|
||||||
|
for a cache shared by the `app` and `promptkit` scopes.
|
||||||
|
|
||||||
|
## PromptKit configuration and definitions
|
||||||
|
|
||||||
|
Register `NewPromptKitScope` with the config suite when you want the effective
|
||||||
|
PromptKit configuration and its ConfigKit provenance available under `config`.
|
||||||
|
Register `NewPromptKitSuite` and `RegisterPromptKit` separately to enable
|
||||||
|
`promptkit show prompt` and/or `promptkit show profile`. Each adapter calls the
|
||||||
|
owning public inspection method and preserves acquisition diagnostics with a
|
||||||
|
failed named lookup. Definition inspection does not invoke execution APIs or
|
||||||
|
resolve provider credentials.
|
||||||
|
|
||||||
|
The [composed example](../../examples/composed/README.md) shares its ordinary app and
|
||||||
|
engine-loading functions across the configuration and definition suites. It
|
||||||
|
also keeps `session restore` as an application-owned typed command, including
|
||||||
|
its session data and domain outcome mapping.
|
||||||
|
|
||||||
|
## Typed application commands
|
||||||
|
|
||||||
|
Declare flags, operands, and a typed input struct with `NewCommand`. Bindings
|
||||||
|
preserve whether a value was explicitly supplied, so an explicit zero can differ
|
||||||
|
from an omitted default. `RepeatFlag` retains occurrence order. The
|
||||||
|
[simple example](../../examples/simple/README.md) combines both features with a typed
|
||||||
|
ConfigKit scope.
|
||||||
|
|
||||||
|
## Outcomes and process exit
|
||||||
|
|
||||||
|
Handlers return `Success`, `Failure`, or an application-owned `Outcome`. The
|
||||||
|
caller chooses how a nonzero status maps to process exit; `Execute` never exits
|
||||||
|
the process. It writes CommandKit diagnostics to the supplied error stream and
|
||||||
|
returns the resulting classification and delivery information. Applications
|
||||||
|
keep their domain meanings and output in their own handlers.
|
||||||
|
|
||||||
|
Exact CommandKit contracts are documented on the exported declarations and
|
||||||
|
their GoDoc comments. ConfigKit owns configuration decoding and provenance;
|
||||||
|
consult its [inspection API](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/dc09959cca4e105f49ec316c6d4ba585bd2340bd/inspection.go).
|
||||||
|
PromptKit owns prompt formats and profile resolution; consult its [public
|
||||||
|
engine methods](https://gitea.maximumdirect.net/eric/promptkit/src/commit/4b8169da286917fde7875425bb32b799b12c6af3/engine.go),
|
||||||
|
[configuration inspection](https://gitea.maximumdirect.net/eric/promptkit/src/commit/4b8169da286917fde7875425bb32b799b12c6af3/configuration_inspection.go),
|
||||||
|
and [inspection types](https://gitea.maximumdirect.net/eric/promptkit/src/commit/4b8169da286917fde7875425bb32b799b12c6af3/types.go).
|
||||||
@@ -4,10 +4,10 @@ CommandKit currently contains adopted repository policies, accepted architectura
|
|||||||
decisions, implementation plans, identity and outcome primitives, typed command
|
decisions, implementation plans, identity and outcome primitives, typed command
|
||||||
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
||||||
adapter, public `App.Execute` coordination, and generated help/version commands.
|
adapter, public `App.Execute` coordination, and generated help/version commands.
|
||||||
Configuration suite registration, scope discovery, ConfigKit integration,
|
Configuration suite registration, scope discovery, ConfigKit validation and
|
||||||
validation, native effective YAML display, source inventory, and path
|
reports, PromptKit engine configuration inspection, and optional PromptKit
|
||||||
explanation reports, PromptKit engine configuration inspection, and optional
|
prompt/profile inspection commands are implemented. The maintained
|
||||||
PromptKit prompt/profile inspection commands are implemented. The
|
[consumer guide](consumers/guide.md) links to maintained examples. The
|
||||||
[README](../README.md) provides project orientation; the
|
[README](../README.md) provides project orientation; the
|
||||||
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
||||||
canonical documentation owners.
|
canonical documentation owners.
|
||||||
@@ -87,3 +87,19 @@ Follow the [local commit policy](policy/agents.md#git-commit-policy). Completed
|
|||||||
units receive local commits by default, subject to explicit user instructions.
|
units receive local commits by default, subject to explicit user instructions.
|
||||||
Preserve unrelated work; default commit authorization does not authorize pushing
|
Preserve unrelated work; default commit authorization does not authorize pushing
|
||||||
or publishing.
|
or publishing.
|
||||||
|
|
||||||
|
## Current public examples
|
||||||
|
|
||||||
|
The maintained consumers under `examples/` compile with the module suite and
|
||||||
|
run without external services:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run ./examples/simple config show
|
||||||
|
go run ./examples/simple greet --count=0 --tag=first --tag second
|
||||||
|
go run ./examples/composed config validate --scope all --campaign spring
|
||||||
|
go run ./examples/composed promptkit show prompt briefing --campaign spring --format yaml
|
||||||
|
go run ./examples/composed session restore session-42 --campaign spring
|
||||||
|
```
|
||||||
|
|
||||||
|
The [consumer guide](consumers/guide.md) explains how their public API usage
|
||||||
|
fits together.
|
||||||
|
|||||||
36
docs/internal/overview.md
Normal file
36
docs/internal/overview.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Internal package overview
|
||||||
|
|
||||||
|
CommandKit ships one public package, `commandkit`, and one internal package,
|
||||||
|
`internal/cobra`. This inventory describes the current implementation; the
|
||||||
|
[architecture policy](../policy/architecture.md) owns normative boundaries and
|
||||||
|
[ADR 0003](../adr/0003-public-facade.md) records the public package boundary.
|
||||||
|
|
||||||
|
## `commandkit`
|
||||||
|
|
||||||
|
The root package declares the public API and owns the instance registry, typed
|
||||||
|
command input preparation, invocation coordination, standard handlers, and
|
||||||
|
provider adapters. Configuration reports adapt ConfigKit's public records and
|
||||||
|
writers. PromptKit scope and definition handlers use PromptKit's public
|
||||||
|
inspection methods and map their results to CommandKit-owned reports.
|
||||||
|
|
||||||
|
The root package imports `internal/cobra` to bridge its command declarations and
|
||||||
|
invocations to Cobra parsing and routing. Application callbacks, output streams,
|
||||||
|
context, and supported error identity cross that bridge through the root's typed
|
||||||
|
contracts.
|
||||||
|
|
||||||
|
## `internal/cobra`
|
||||||
|
|
||||||
|
This package owns the Cobra-specific command tree and argument/flag parsing
|
||||||
|
adapter. It translates neutral command metadata and parsed values into a bridge
|
||||||
|
result consumed by the root package. It does not import `commandkit` and does not
|
||||||
|
own registry policy, public command declarations, provider loading, or report
|
||||||
|
rendering.
|
||||||
|
|
||||||
|
## External implementation boundaries
|
||||||
|
|
||||||
|
ConfigKit owns typed configuration loading, source history, validation,
|
||||||
|
resolution, and path explanations. PromptKit owns captured engine
|
||||||
|
configuration, prompt definitions, profile inheritance, and named inspections.
|
||||||
|
CommandKit depends on their public module APIs and does not maintain parallel
|
||||||
|
schemas. Cobra and pflag are confined behind the internal adapter; YAML report
|
||||||
|
encoding is performed by the root package.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Initial CommandKit implementation plan
|
# Initial CommandKit implementation plan
|
||||||
|
|
||||||
Status: implementation underway; Stages 1-13 complete and Stage 14 pending.
|
Status: implementation complete; Stages 1-14 complete.
|
||||||
|
|
||||||
Date: 2026-10-07.
|
Date: 2026-10-07.
|
||||||
|
|
||||||
@@ -818,7 +818,7 @@ selection errors, optional capabilities, and help without engine construction.
|
|||||||
|
|
||||||
## Stage 14: substantiate consumer fit and complete documentation
|
## Stage 14: substantiate consumer fit and complete documentation
|
||||||
|
|
||||||
**Status:** pending. **Prerequisite:** Stage 13.
|
**Status:** complete. **Prerequisite:** Stage 13.
|
||||||
|
|
||||||
1. Add complete maintained examples using only the public root API and published
|
1. Add complete maintained examples using only the public root API and published
|
||||||
upstream packages. Use two ordinary example directories (separate Go main
|
upstream packages. Use two ordinary example directories (separate Go main
|
||||||
@@ -860,8 +860,12 @@ selection errors, optional capabilities, and help without engine construction.
|
|||||||
into the implementation. Record completed stage status and checks, then commit
|
into the implementation. Record completed stage status and checks, then commit
|
||||||
the completed unit under the local policy. Publishing remains separate work.
|
the completed unit under the local policy. Publishing remains separate work.
|
||||||
|
|
||||||
**Completion evidence:** each included roadmap feature has observable behavior
|
**Completion evidence:** added maintained `examples/simple` and
|
||||||
and its appropriate checks; examples compile and use the real public dispatcher;
|
`examples/composed` consumers, public consumer guidance, internal architecture
|
||||||
default tests do not require real services/credentials; documents describe only
|
overview, and updated CLI/development/README/feature-roadmap documentation. The
|
||||||
implemented contracts. The final report identifies the commits, checks, and
|
examples compile and use the public dispatcher with synthetic data. Shared
|
||||||
deferred capabilities without claiming downstream migrations or release completion.
|
selector helpers serve both configuration suites. Reviewed exported declarations,
|
||||||
|
root and adapter imports, dependency replacements, attribution, and retained
|
||||||
|
deferred feature IDs. Verified with `gofmt`, `go test ./...`, `go mod tidy -diff`,
|
||||||
|
document link checks, example smoke commands, and `git diff --check`. Publishing,
|
||||||
|
downstream migrations, and deferred capabilities remain outside this stage.
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
# Initial CommandKit feature roadmap
|
# Initial CommandKit feature roadmap
|
||||||
|
|
||||||
Status: selected initial implementation target; unimplemented.
|
Status: selected initial implementation target; implemented.
|
||||||
|
|
||||||
Date: 2026-10-07.
|
Date: 2026-10-07.
|
||||||
|
|
||||||
## Purpose and authority
|
## Purpose and authority
|
||||||
|
|
||||||
This document defines the selected end state of the initial library: packages,
|
This document defines the selected initial target for the library: packages,
|
||||||
features, integration contracts, dependencies, and completion evidence. It is
|
features, integration contracts, dependencies, and completion evidence. It was
|
||||||
prepared using [the agreed checklist](initial-plan.md). Implementation steps belong
|
prepared using [the agreed checklist](initial-plan.md). Implementation steps and
|
||||||
in [the implementation plan](implementation.md). This document describes the target
|
stage evidence belong in [the implementation plan](implementation.md). The
|
||||||
state; CommandKit currently has no Go module or dispatcher implementation.
|
included target is implemented; the table below records feature status.
|
||||||
|
|
||||||
|
| Included feature IDs | Status | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| API-01, REG-01, INPUT-01, BACK-01, INV-01, OUT-01 | complete | Root API and dispatcher tests; `go test ./...`. |
|
||||||
|
| HELP-01, VER-01 | complete | Help/version behavior tests; `go test ./...`. |
|
||||||
|
| INT-01, CFG-01–05 | complete | Configuration scope, validation, show, sources, and explain tests; `go test ./...`. |
|
||||||
|
| PK-01 | complete | PromptKit scope adapter tests; `go test ./...`. |
|
||||||
|
| PK-02, PK-03 | complete | Prompt/profile inspection tests; `go test ./...`. |
|
||||||
|
| DIFF-01, CAT-01 | deferred | Require upstream safe snapshot and catalog APIs; see [deferred capability targets](#deferred-capability-targets). |
|
||||||
|
|
||||||
The [architecture policy](../policy/architecture.md) and nine accepted ADRs control
|
The [architecture policy](../policy/architecture.md) and nine accepted ADRs control
|
||||||
this roadmap. The delivery scope, typed API model, selector/report behavior, and
|
this roadmap. The delivery scope, typed API model, selector/report behavior, and
|
||||||
@@ -63,11 +72,10 @@ module still declares the dependencies used by its bundled helpers.
|
|||||||
| PromptKit definitions | `promptkit show prompt` and `promptkit show profile`, including effective `ExtraParams` |
|
| PromptKit definitions | `promptkit show prompt` and `promptkit show profile`, including effective `ExtraParams` |
|
||||||
| Application commands | Typed custom leaf commands, nested groups, and groups with their own runnable handler |
|
| Application commands | Typed custom leaf commands, nested groups, and groups with their own runnable handler |
|
||||||
|
|
||||||
Defer catalog listing and semantic comparison until the owning libraries expose
|
Catalog listing and semantic comparison remain deferred until the owning
|
||||||
the required public contracts. These accepted standard operations remain future
|
libraries expose the required public contracts. Their absence appears accurately
|
||||||
implementation targets. Their absence must appear accurately in help and
|
in help and capabilities. The implemented target uses the released inspection
|
||||||
capabilities. Initial delivery uses the current released inspection APIs; upstream
|
APIs; upstream snapshot/catalog enhancements are separate follow-up work.
|
||||||
snapshot/catalog enhancements are separate follow-up work.
|
|
||||||
|
|
||||||
This scope provides meaningful configuration inspection immediately and avoids
|
This scope provides meaningful configuration inspection immediately and avoids
|
||||||
requiring an initial consumer migration to wait for every future capability.
|
requiring an initial consumer migration to wait for every future capability.
|
||||||
|
|||||||
18
examples/composed/README.md
Normal file
18
examples/composed/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Composed consumer
|
||||||
|
|
||||||
|
This example composes application configuration, PromptKit configuration,
|
||||||
|
definition inspection, and one application-owned command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run . config scopes
|
||||||
|
go run . config validate --scope all --campaign spring
|
||||||
|
go run . promptkit show prompt briefing --campaign spring --format yaml
|
||||||
|
go run . promptkit show profile spring --campaign spring
|
||||||
|
go run . session restore session-42 --campaign spring
|
||||||
|
```
|
||||||
|
|
||||||
|
The sources and session store are small in-memory fixtures. `--campaign` is a
|
||||||
|
typed selector shared by the app and PromptKit configuration scopes. Their
|
||||||
|
scope contexts share a per-invocation cache created by `ConfigSuite.NewContext`.
|
||||||
|
Prompt/profile inspection has its own invocation context and composes the same
|
||||||
|
application and engine loader functions.
|
||||||
223
examples/composed/main.go
Normal file
223
examples/composed/main.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type campaign struct {
|
||||||
|
PromptID string `yaml:"prompt_id"`
|
||||||
|
PromptVersion string `yaml:"prompt_version"`
|
||||||
|
ProfileID string `yaml:"profile_id"`
|
||||||
|
Model string `yaml:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type appConfiguration struct {
|
||||||
|
DefaultCampaign string `yaml:"default_campaign"`
|
||||||
|
Campaigns map[string]campaign `yaml:"campaigns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type invocationCache struct {
|
||||||
|
application *configkit.Result[appConfiguration]
|
||||||
|
}
|
||||||
|
|
||||||
|
type configContext struct {
|
||||||
|
Cache *invocationCache
|
||||||
|
Campaign commandkit.Supplied[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
type definitionContext struct {
|
||||||
|
Cache *invocationCache
|
||||||
|
Campaign commandkit.Supplied[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
type restoreInput struct {
|
||||||
|
Campaign commandkit.Supplied[string]
|
||||||
|
Session string
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
ID string
|
||||||
|
Campaign string
|
||||||
|
Summary string
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionStore struct {
|
||||||
|
active string
|
||||||
|
records map[string]session
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadApplicationConfig(_ context.Context, _ commandkit.Selection, cache *invocationCache) (*configkit.Result[appConfiguration], error) {
|
||||||
|
if cache != nil && cache.application != nil {
|
||||||
|
return cache.application, nil
|
||||||
|
}
|
||||||
|
result, err := configkit.Load[appConfiguration](configkit.Mapping("composed app config", map[string]any{
|
||||||
|
"default_campaign": "spring",
|
||||||
|
"campaigns": map[string]any{
|
||||||
|
"spring": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "spring", "model": "spring-model"},
|
||||||
|
"autumn": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "autumn", "model": "autumn-model"},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
if err == nil && cache != nil {
|
||||||
|
cache.application = result
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedCampaign(configuration appConfiguration, supplied commandkit.Supplied[string]) (string, campaign, error) {
|
||||||
|
name := configuration.DefaultCampaign
|
||||||
|
if supplied.Set {
|
||||||
|
name = supplied.Value
|
||||||
|
}
|
||||||
|
value, ok := configuration.Campaigns[name]
|
||||||
|
if !ok {
|
||||||
|
return "", campaign{}, fmt.Errorf("unknown campaign %q", name)
|
||||||
|
}
|
||||||
|
return name, value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPromptEngine(ctx context.Context, selection commandkit.Selection, cache *invocationCache, selected commandkit.Supplied[string]) (*promptkit.Engine, error) {
|
||||||
|
application, err := loadApplicationConfig(ctx, selection, cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name, chosen, err := selectedCampaign(application.Config, selected)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
engineConfig := configkit.Mapping("campaign "+name, map[string]any{
|
||||||
|
"profiles": map[string]any{chosen.ProfileID: map[string]any{
|
||||||
|
"endpoint": "https://example.invalid/v1",
|
||||||
|
"model": chosen.Model,
|
||||||
|
"extra_params": map[string]any{
|
||||||
|
"campaign": name,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
"prompts": map[string]any{chosen.PromptID: map[string]any{
|
||||||
|
chosen.PromptVersion: map[string]any{
|
||||||
|
"default_profile": chosen.ProfileID,
|
||||||
|
"inputs": []any{map[string]any{"name": "topic", "required": true, "content_type": "text/plain", "description": "Topic to brief"}},
|
||||||
|
"messages": []any{map[string]any{"role": "user", "content": "Write a concise briefing about {{.topic}}."}},
|
||||||
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
return promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: engineConfig}}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func campaignSelector[C any](set func(*C, commandkit.Supplied[string])) commandkit.Selector[C] {
|
||||||
|
return commandkit.Selector[C]{
|
||||||
|
Binding: commandkit.ValueFlag[C](commandkit.FlagMeta{Name: "campaign", Usage: "Select a configured campaign."}, nil, func(value string) (string, error) { return value, nil }, set),
|
||||||
|
Scopes: []string{"app", "promptkit"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp() (*commandkit.App, error) {
|
||||||
|
app, err := commandkit.New(commandkit.Identity{Name: "composed-example", Version: "1.0"})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
appScope, err := commandkit.NewConfigKitScope[configContext, appConfiguration]("app", "Application campaigns.", func(ctx context.Context, selection commandkit.Selection, input configContext) (*configkit.Result[appConfiguration], error) {
|
||||||
|
return loadApplicationConfig(ctx, selection, input.Cache)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
promptScope, err := commandkit.NewPromptKitScope(func(ctx context.Context, selection commandkit.Selection, input configContext) (*promptkit.Engine, error) {
|
||||||
|
return loadPromptEngine(ctx, selection, input.Cache, input.Campaign)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configSuite := commandkit.ConfigSuite[configContext]{
|
||||||
|
NewContext: func() configContext { return configContext{Cache: &invocationCache{}} },
|
||||||
|
Selectors: []commandkit.Selector[configContext]{campaignSelector(func(input *configContext, value commandkit.Supplied[string]) {
|
||||||
|
input.Campaign = value
|
||||||
|
})},
|
||||||
|
Scopes: []commandkit.ConfigScope[configContext]{appScope, promptScope},
|
||||||
|
}
|
||||||
|
if err := commandkit.RegisterConfig(app, configSuite); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
definitionSuite, err := commandkit.NewPromptKitSuite(func(ctx context.Context, selection commandkit.Selection, input definitionContext) (*promptkit.Engine, error) {
|
||||||
|
return loadPromptEngine(ctx, selection, input.Cache, input.Campaign)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
definitionSuite.NewContext = func() definitionContext { return definitionContext{Cache: &invocationCache{}} }
|
||||||
|
definitionSuite.Selectors = []commandkit.Selector[definitionContext]{campaignSelector(func(input *definitionContext, value commandkit.Supplied[string]) {
|
||||||
|
input.Campaign = value
|
||||||
|
})}
|
||||||
|
if err := commandkit.RegisterPromptKit(app, definitionSuite); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
store := &sessionStore{records: map[string]session{"session-42": {ID: "session-42", Campaign: "spring", Summary: "A synthetic planning session."}}}
|
||||||
|
restore, err := commandkit.NewCommand(commandkit.CommandSpec[restoreInput]{
|
||||||
|
Name: "restore",
|
||||||
|
Summary: "Restore one saved session.",
|
||||||
|
Flags: []commandkit.Flag[restoreInput]{commandkit.ValueFlag[restoreInput](commandkit.FlagMeta{Name: "campaign", Usage: "Campaign owning the session."}, nil, func(value string) (string, error) { return value, nil }, func(input *restoreInput, value commandkit.Supplied[string]) {
|
||||||
|
input.Campaign = value
|
||||||
|
})},
|
||||||
|
Arguments: []commandkit.Argument[restoreInput]{commandkit.ValueArgument[restoreInput](commandkit.ArgumentMeta{Name: "ID"}, true, func(value string) (string, error) { return value, nil }, func(input *restoreInput, value commandkit.Supplied[string]) {
|
||||||
|
input.Session = value.Value
|
||||||
|
})},
|
||||||
|
Run: func(invocation commandkit.Invocation, input restoreInput) commandkit.Outcome {
|
||||||
|
return restoreSession(invocation, store, input)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sessionGroup, err := commandkit.Group("session", "Manage saved sessions.", restore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := app.Register(sessionGroup); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreSession(invocation commandkit.Invocation, store *sessionStore, input restoreInput) commandkit.Outcome {
|
||||||
|
application, err := loadApplicationConfig(invocation.Context, commandkit.Selection{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Failure(err)
|
||||||
|
}
|
||||||
|
name, _, err := selectedCampaign(application.Config, input.Campaign)
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Failure(err)
|
||||||
|
}
|
||||||
|
record, ok := store.records[input.Session]
|
||||||
|
if !ok {
|
||||||
|
return commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 4, Domain: "session_not_found", Cause: errors.New("saved session was not found")}
|
||||||
|
}
|
||||||
|
if record.Campaign != name {
|
||||||
|
return commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 3, Domain: "campaign_mismatch", Cause: fmt.Errorf("session belongs to campaign %q", record.Campaign)}
|
||||||
|
}
|
||||||
|
store.active = record.ID
|
||||||
|
if _, err := fmt.Fprintf(invocation.Streams.Out, "restored %s: %s\n", record.ID, record.Summary); err != nil {
|
||||||
|
return commandkit.Failure(err)
|
||||||
|
}
|
||||||
|
return commandkit.Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app, err := newApp()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
outcome := app.Execute(context.Background(), os.Args[1:], commandkit.Streams{Out: os.Stdout, Err: os.Stderr})
|
||||||
|
if outcome.Status != 0 {
|
||||||
|
os.Exit(outcome.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
examples/simple/README.md
Normal file
14
examples/simple/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Simple consumer
|
||||||
|
|
||||||
|
Run the maintained example with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run . config show
|
||||||
|
go run . greet
|
||||||
|
go run . greet --count=0 --tag=first --tag second
|
||||||
|
```
|
||||||
|
|
||||||
|
The `config` family comes from one typed ConfigKit scope. The custom `greet`
|
||||||
|
command uses CommandKit's typed bindings and shows the difference between an
|
||||||
|
omitted `--count` and an explicitly supplied zero, plus ordered repeated tags.
|
||||||
|
The configuration is synthetic and stays in memory.
|
||||||
90
examples/simple/main.go
Normal file
90
examples/simple/main.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appConfiguration struct {
|
||||||
|
Greeting string `yaml:"greeting"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type greetInput struct {
|
||||||
|
Count commandkit.Supplied[int]
|
||||||
|
Tags commandkit.Supplied[[]string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAppConfiguration(context.Context, commandkit.Selection, struct{}) (*configkit.Result[appConfiguration], error) {
|
||||||
|
return configkit.Load[appConfiguration](configkit.Bytes("simple example", []byte("greeting: hello\n")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp() (*commandkit.App, error) {
|
||||||
|
app, err := commandkit.New(commandkit.Identity{Name: "simple-example", Version: "1.0"})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scope, err := commandkit.NewConfigKitScope[struct{}, appConfiguration]("app", "Application settings.", loadAppConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
countDefault := 1
|
||||||
|
tagDefaults := []string{"example"}
|
||||||
|
greet, err := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{
|
||||||
|
Name: "greet",
|
||||||
|
Summary: "Show flag presence and repeated values.",
|
||||||
|
Flags: []commandkit.Flag[greetInput]{
|
||||||
|
commandkit.ValueFlag[greetInput](commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) {
|
||||||
|
input.Count = value
|
||||||
|
}),
|
||||||
|
commandkit.RepeatFlag[greetInput](commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) {
|
||||||
|
input.Tags = value
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome {
|
||||||
|
configuration, err := loadAppConfiguration(invocation.Context, commandkit.Selection{}, struct{}{})
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Failure(err)
|
||||||
|
}
|
||||||
|
count := fmt.Sprintf("default %d (not supplied)", input.Count.Value)
|
||||||
|
if input.Count.Set {
|
||||||
|
count = fmt.Sprintf("explicit %d", input.Count.Value)
|
||||||
|
}
|
||||||
|
tags := fmt.Sprintf("default %v (not supplied)", input.Tags.Value)
|
||||||
|
if input.Tags.Set {
|
||||||
|
tags = fmt.Sprintf("explicit %v", input.Tags.Value)
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(invocation.Streams.Out, "%s; count=%s; tags=%s\n", configuration.Config.Greeting, count, tags); err != nil {
|
||||||
|
return commandkit.Failure(err)
|
||||||
|
}
|
||||||
|
return commandkit.Success()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := app.Register(greet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app, err := newApp()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
outcome := app.Execute(context.Background(), os.Args[1:], commandkit.Streams{Out: os.Stdout, Err: os.Stderr})
|
||||||
|
if outcome.Status != 0 {
|
||||||
|
os.Exit(outcome.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
promptkit.go
48
promptkit.go
@@ -169,19 +169,13 @@ func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C],
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type promptKitSelectorState[C any] struct {
|
|
||||||
scopes []string
|
|
||||||
apply func(*C)
|
|
||||||
clear func(*C)
|
|
||||||
}
|
|
||||||
|
|
||||||
type promptKitInput[C any] struct {
|
type promptKitInput[C any] struct {
|
||||||
Selection Selection
|
Selection Selection
|
||||||
Format string
|
Format string
|
||||||
ID string
|
ID string
|
||||||
Version string
|
Version string
|
||||||
SelectorContext C
|
SelectorContext C
|
||||||
selectors []promptKitSelectorState[C]
|
selectors []selectorState[C]
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[promptKitInput[C]] {
|
func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[promptKitInput[C]] {
|
||||||
@@ -190,13 +184,11 @@ func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[pro
|
|||||||
ValueFlag[promptKitInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
ValueFlag[promptKitInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
||||||
input.Format = value.Value
|
input.Format = value.Value
|
||||||
}),
|
}),
|
||||||
ValueFlag[promptKitInput[C]](FlagMeta{Name: "config", Usage: "Configuration source."}, nil, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
|
||||||
input.Selection.Config = value
|
|
||||||
}),
|
|
||||||
ValueFlag[promptKitInput[C]](FlagMeta{Name: "overlay", Usage: "Overlay source."}, nil, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
|
||||||
input.Selection.Overlay = value
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
flags = append(flags, configurationSelectionFlags(
|
||||||
|
func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Config = value },
|
||||||
|
func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Overlay = value },
|
||||||
|
)...)
|
||||||
if prompt {
|
if prompt {
|
||||||
versionDefault := ""
|
versionDefault := ""
|
||||||
flags = append(flags, ValueFlag[promptKitInput[C]](FlagMeta{Name: "version", Usage: "Exact prompt version; empty selects the owning default."}, &versionDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
flags = append(flags, ValueFlag[promptKitInput[C]](FlagMeta{Name: "version", Usage: "Exact prompt version; empty selects the owning default."}, &versionDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
||||||
@@ -204,34 +196,16 @@ func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[pro
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
for _, selector := range suite.Selectors {
|
for _, selector := range suite.Selectors {
|
||||||
flags = append(flags, adaptPromptKitSelector(selector))
|
flags = append(flags, adaptContextSelector(selector,
|
||||||
|
func(input *promptKitInput[C]) *C { return &input.SelectorContext },
|
||||||
|
func(input *promptKitInput[C], state selectorState[C]) {
|
||||||
|
input.selectors = append(input.selectors, state)
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func adaptPromptKitSelector[C any](selector Selector[C]) Flag[promptKitInput[C]] {
|
|
||||||
binding := selector.Binding.binding
|
|
||||||
meta := binding.meta
|
|
||||||
return Flag[promptKitInput[C]]{binding: flagBinding[promptKitInput[C]]{
|
|
||||||
meta: meta,
|
|
||||||
defaultText: binding.defaultText,
|
|
||||||
repeated: binding.repeated,
|
|
||||||
boolean: binding.boolean,
|
|
||||||
valid: binding.valid,
|
|
||||||
prepare: func(values []string) (func(*promptKitInput[C]), error) {
|
|
||||||
apply, err := binding.prepare(values)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
state := promptKitSelectorState[C]{scopes: append([]string(nil), selector.Scopes...), apply: apply, clear: binding.clear}
|
|
||||||
return func(input *promptKitInput[C]) {
|
|
||||||
apply(&input.SelectorContext)
|
|
||||||
input.selectors = append(input.selectors, state)
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPromptKitInput[C any](suite PromptKitSuite[C], input promptKitInput[C]) error {
|
func checkPromptKitInput[C any](suite PromptKitSuite[C], input promptKitInput[C]) error {
|
||||||
if _, err := parseConfigFormat(input.Format); err != nil {
|
if _, err := parseConfigFormat(input.Format); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewPromptKitScope adapts PromptKit's captured configuration inspection to
|
// NewPromptKitScope adapts PromptKit's captured configuration inspection to
|
||||||
// CommandKit's configuration commands.
|
// CommandKit's configuration commands. The loader runs only when a selected
|
||||||
|
// configuration operation acquires this scope. Successful construction and
|
||||||
|
// inspection supply validation; the adapter does not execute prompts, resolve
|
||||||
|
// credentials, or probe model availability.
|
||||||
func NewPromptKitScope[C any](
|
func NewPromptKitScope[C any](
|
||||||
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
||||||
) (ConfigScope[C], error) {
|
) (ConfigScope[C], error) {
|
||||||
|
|||||||
@@ -250,6 +250,11 @@ func TestPromptKitInspectionSelectionErrorsHelpAndOptionalCapabilities(t *testin
|
|||||||
if outcome.Kind != commandkit.OutcomeUsage || loaderCalls != beforeMissing {
|
if outcome.Kind != commandkit.OutcomeUsage || loaderCalls != beforeMissing {
|
||||||
t.Fatalf("missing ID = (%#v, calls=%d), want usage before acquisition", outcome, loaderCalls)
|
t.Fatalf("missing ID = (%#v, calls=%d), want usage before acquisition", outcome, loaderCalls)
|
||||||
}
|
}
|
||||||
|
beforeScopeFlag := loaderCalls
|
||||||
|
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "profile", "child", "--scope", "promptkit"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeUsage || loaderCalls != beforeScopeFlag {
|
||||||
|
t.Fatalf("definition command accepted --scope: outcome=%#v calls=%d", outcome, loaderCalls)
|
||||||
|
}
|
||||||
|
|
||||||
profileOnlyApp, _ := commandkit.New(commandkit.Identity{Name: "profile-only"})
|
profileOnlyApp, _ := commandkit.New(commandkit.Identity{Name: "profile-only"})
|
||||||
if err := commandkit.RegisterPromptKit(profileOnlyApp, commandkit.PromptKitSuite[struct{}]{InspectProfile: func(ctx context.Context, _ commandkit.Selection, _ struct{}, id string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
if err := commandkit.RegisterPromptKit(profileOnlyApp, commandkit.PromptKitSuite[struct{}]{InspectProfile: func(ctx context.Context, _ commandkit.Selection, _ struct{}, id string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||||
@@ -362,6 +367,29 @@ func TestPromptKitDirectSuiteRetainsDiagnosticsOnNamedLookupFailure(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPromptKitInspectionReportsOutputDeliveryFailure(t *testing.T) {
|
||||||
|
engine := promptKitInspectionEngine(t)
|
||||||
|
suite, err := commandkit.NewPromptKitSuite(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||||||
|
return engine, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||||
|
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeErr := errors.New("report output failed")
|
||||||
|
outcome := app.Execute(context.Background(), []string{"promptkit", "show", "profile", "child", "--format", "yaml"}, commandkit.Streams{Out: promptKitFailingWriter{err: writeErr}, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, writeErr) {
|
||||||
|
t.Fatalf("promptkit output failure = %#v", outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptKitFailingWriter struct{ err error }
|
||||||
|
|
||||||
|
func (writer promptKitFailingWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||||
|
|
||||||
func sortedKeys(values map[string]any) []string {
|
func sortedKeys(values map[string]any) []string {
|
||||||
keys := make([]string, 0, len(values))
|
keys := make([]string, 0, len(values))
|
||||||
for key := range values {
|
for key := range values {
|
||||||
|
|||||||
34
selectors.go
34
selectors.go
@@ -15,13 +15,11 @@ func configInputFlags[C any](suite ConfigSuite[C], formatDefault string) []Flag[
|
|||||||
ValueFlag[configInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *configInput[C], value Supplied[string]) {
|
ValueFlag[configInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *configInput[C], value Supplied[string]) {
|
||||||
input.Format = value.Value
|
input.Format = value.Value
|
||||||
}),
|
}),
|
||||||
ValueFlag[configInput[C]](FlagMeta{Name: "config", Usage: "Configuration source."}, nil, parseString, func(input *configInput[C], value Supplied[string]) {
|
|
||||||
input.Selection.Config = value
|
|
||||||
}),
|
|
||||||
ValueFlag[configInput[C]](FlagMeta{Name: "overlay", Usage: "Overlay source."}, nil, parseString, func(input *configInput[C], value Supplied[string]) {
|
|
||||||
input.Selection.Overlay = value
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
flags = append(flags, configurationSelectionFlags(
|
||||||
|
func(input *configInput[C], value Supplied[string]) { input.Selection.Config = value },
|
||||||
|
func(input *configInput[C], value Supplied[string]) { input.Selection.Overlay = value },
|
||||||
|
)...)
|
||||||
for _, selector := range suite.Selectors {
|
for _, selector := range suite.Selectors {
|
||||||
flags = append(flags, adaptSelector(selector))
|
flags = append(flags, adaptSelector(selector))
|
||||||
}
|
}
|
||||||
@@ -29,6 +27,13 @@ func configInputFlags[C any](suite ConfigSuite[C], formatDefault string) []Flag[
|
|||||||
}
|
}
|
||||||
|
|
||||||
func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
|
func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
|
||||||
|
return adaptContextSelector(selector,
|
||||||
|
func(input *configInput[C]) *C { return &input.SelectorContext },
|
||||||
|
func(input *configInput[C], state selectorState[C]) { input.selectors = append(input.selectors, state) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adaptContextSelector[C any, I any](selector Selector[C], selectContext func(*I) *C, record func(*I, selectorState[C])) Flag[I] {
|
||||||
binding := selector.Binding.binding
|
binding := selector.Binding.binding
|
||||||
meta := binding.meta
|
meta := binding.meta
|
||||||
if len(selector.Scopes) == 0 {
|
if len(selector.Scopes) == 0 {
|
||||||
@@ -36,13 +41,13 @@ func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
|
|||||||
} else {
|
} else {
|
||||||
meta.Usage += " (applies to: " + joinNames(selector.Scopes) + ")"
|
meta.Usage += " (applies to: " + joinNames(selector.Scopes) + ")"
|
||||||
}
|
}
|
||||||
return Flag[configInput[C]]{binding: flagBinding[configInput[C]]{
|
return Flag[I]{binding: flagBinding[I]{
|
||||||
meta: meta,
|
meta: meta,
|
||||||
defaultText: binding.defaultText,
|
defaultText: binding.defaultText,
|
||||||
repeated: binding.repeated,
|
repeated: binding.repeated,
|
||||||
boolean: binding.boolean,
|
boolean: binding.boolean,
|
||||||
valid: binding.valid,
|
valid: binding.valid,
|
||||||
prepare: func(values []string) (func(*configInput[C]), error) {
|
prepare: func(values []string) (func(*I), error) {
|
||||||
apply, err := binding.prepare(values)
|
apply, err := binding.prepare(values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -53,14 +58,21 @@ func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
|
|||||||
apply: apply,
|
apply: apply,
|
||||||
clear: binding.clear,
|
clear: binding.clear,
|
||||||
}
|
}
|
||||||
return func(input *configInput[C]) {
|
return func(input *I) {
|
||||||
apply(&input.SelectorContext)
|
apply(selectContext(input))
|
||||||
input.selectors = append(input.selectors, state)
|
record(input, state)
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configurationSelectionFlags[I any](setConfig, setOverlay func(*I, Supplied[string])) []Flag[I] {
|
||||||
|
return []Flag[I]{
|
||||||
|
ValueFlag[I](FlagMeta{Name: "config", Usage: "Configuration source."}, nil, parseString, setConfig),
|
||||||
|
ValueFlag[I](FlagMeta{Name: "overlay", Usage: "Overlay source."}, nil, parseString, setOverlay),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseString(value string) (string, error) { return value, nil }
|
func parseString(value string) (string, error) { return value, nil }
|
||||||
|
|
||||||
func preflightConfig[C any](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) {
|
func preflightConfig[C any](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user