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
|
||||
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
|
||||
|
||||
- [Contributor guide](docs/development.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)
|
||||
- [CLI standard proposal](docs/roadmap/01-cli-plan.md)
|
||||
- [Dispatcher implementation roadmap](docs/roadmap/02-dispatch.md)
|
||||
|
||||
@@ -116,7 +116,8 @@ type configInput[C any] struct {
|
||||
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 {
|
||||
if app == nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if load == 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
|
||||
// types for the CommandKit Go library.
|
||||
// Package commandkit provides an instance-owned CLI dispatcher with typed
|
||||
// command declarations, configuration scopes, and PromptKit inspection.
|
||||
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
|
||||
provider, or look up credentials.
|
||||
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
|
||||
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
|
||||
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
||||
adapter, public `App.Execute` coordination, and generated help/version commands.
|
||||
Configuration suite registration, scope discovery, ConfigKit integration,
|
||||
validation, native effective YAML display, source inventory, and path
|
||||
explanation reports, PromptKit engine configuration inspection, and optional
|
||||
PromptKit prompt/profile inspection commands are implemented. The
|
||||
Configuration suite registration, scope discovery, ConfigKit validation and
|
||||
reports, PromptKit engine configuration inspection, and optional PromptKit
|
||||
prompt/profile inspection commands are implemented. The maintained
|
||||
[consumer guide](consumers/guide.md) links to maintained examples. The
|
||||
[README](../README.md) provides project orientation; the
|
||||
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
||||
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.
|
||||
Preserve unrelated work; default commit authorization does not authorize pushing
|
||||
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
|
||||
|
||||
Status: implementation underway; Stages 1-13 complete and Stage 14 pending.
|
||||
Status: implementation complete; Stages 1-14 complete.
|
||||
|
||||
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
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 13.
|
||||
**Status:** complete. **Prerequisite:** Stage 13.
|
||||
|
||||
1. Add complete maintained examples using only the public root API and published
|
||||
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
|
||||
the completed unit under the local policy. Publishing remains separate work.
|
||||
|
||||
**Completion evidence:** each included roadmap feature has observable behavior
|
||||
and its appropriate checks; examples compile and use the real public dispatcher;
|
||||
default tests do not require real services/credentials; documents describe only
|
||||
implemented contracts. The final report identifies the commits, checks, and
|
||||
deferred capabilities without claiming downstream migrations or release completion.
|
||||
**Completion evidence:** added maintained `examples/simple` and
|
||||
`examples/composed` consumers, public consumer guidance, internal architecture
|
||||
overview, and updated CLI/development/README/feature-roadmap documentation. The
|
||||
examples compile and use the public dispatcher with synthetic data. Shared
|
||||
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
|
||||
|
||||
Status: selected initial implementation target; unimplemented.
|
||||
Status: selected initial implementation target; implemented.
|
||||
|
||||
Date: 2026-10-07.
|
||||
|
||||
## Purpose and authority
|
||||
|
||||
This document defines the selected end state of the initial library: packages,
|
||||
features, integration contracts, dependencies, and completion evidence. It is
|
||||
prepared using [the agreed checklist](initial-plan.md). Implementation steps belong
|
||||
in [the implementation plan](implementation.md). This document describes the target
|
||||
state; CommandKit currently has no Go module or dispatcher implementation.
|
||||
This document defines the selected initial target for the library: packages,
|
||||
features, integration contracts, dependencies, and completion evidence. It was
|
||||
prepared using [the agreed checklist](initial-plan.md). Implementation steps and
|
||||
stage evidence belong in [the implementation plan](implementation.md). The
|
||||
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
|
||||
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` |
|
||||
| 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
|
||||
the required public contracts. These accepted standard operations remain future
|
||||
implementation targets. Their absence must appear accurately in help and
|
||||
capabilities. Initial delivery uses the current released inspection APIs; upstream
|
||||
snapshot/catalog enhancements are separate follow-up work.
|
||||
Catalog listing and semantic comparison remain deferred until the owning
|
||||
libraries expose the required public contracts. Their absence appears accurately
|
||||
in help and capabilities. The implemented target uses the released inspection
|
||||
APIs; upstream snapshot/catalog enhancements are separate follow-up work.
|
||||
|
||||
This scope provides meaningful configuration inspection immediately and avoids
|
||||
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
|
||||
}
|
||||
|
||||
type promptKitSelectorState[C any] struct {
|
||||
scopes []string
|
||||
apply func(*C)
|
||||
clear func(*C)
|
||||
}
|
||||
|
||||
type promptKitInput[C any] struct {
|
||||
Selection Selection
|
||||
Format string
|
||||
ID string
|
||||
Version string
|
||||
SelectorContext C
|
||||
selectors []promptKitSelectorState[C]
|
||||
selectors []selectorState[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]) {
|
||||
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 {
|
||||
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]) {
|
||||
@@ -204,34 +196,16 @@ func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[pro
|
||||
}))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if _, err := parseConfigFormat(input.Format); err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
)
|
||||
|
||||
// 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](
|
||||
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
||||
) (ConfigScope[C], error) {
|
||||
|
||||
@@ -250,6 +250,11 @@ func TestPromptKitInspectionSelectionErrorsHelpAndOptionalCapabilities(t *testin
|
||||
if outcome.Kind != commandkit.OutcomeUsage || loaderCalls != beforeMissing {
|
||||
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"})
|
||||
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 {
|
||||
keys := make([]string, 0, len(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]) {
|
||||
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 {
|
||||
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]] {
|
||||
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
|
||||
meta := binding.meta
|
||||
if len(selector.Scopes) == 0 {
|
||||
@@ -36,13 +41,13 @@ func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
|
||||
} else {
|
||||
meta.Usage += " (applies to: " + joinNames(selector.Scopes) + ")"
|
||||
}
|
||||
return Flag[configInput[C]]{binding: flagBinding[configInput[C]]{
|
||||
return Flag[I]{binding: flagBinding[I]{
|
||||
meta: meta,
|
||||
defaultText: binding.defaultText,
|
||||
repeated: binding.repeated,
|
||||
boolean: binding.boolean,
|
||||
valid: binding.valid,
|
||||
prepare: func(values []string) (func(*configInput[C]), error) {
|
||||
prepare: func(values []string) (func(*I), error) {
|
||||
apply, err := binding.prepare(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -53,14 +58,21 @@ func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
|
||||
apply: apply,
|
||||
clear: binding.clear,
|
||||
}
|
||||
return func(input *configInput[C]) {
|
||||
apply(&input.SelectorContext)
|
||||
input.selectors = append(input.selectors, state)
|
||||
return func(input *I) {
|
||||
apply(selectContext(input))
|
||||
record(input, state)
|
||||
}, 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 preflightConfig[C any](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) {
|
||||
|
||||
Reference in New Issue
Block a user