From 666c006a899c89b70eaaf052b12346242d68bd1b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 19 Oct 2026 19:56:33 +0000 Subject: [PATCH] Add aliases to maintained examples --- README.md | 2 +- docs/roadmap/implementation.md | 13 +++- examples/core/README.md | 22 +++++++ examples/core/main.go | 11 +++- examples/core/main_test.go | 63 +++++++++++++++++++ .../ckprompt/examples/composed/README.md | 21 +++++++ .../ckprompt/examples/composed/main.go | 24 ++++--- .../ckprompt/examples/composed/main_test.go | 49 +++++++++++++++ 8 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 examples/core/README.md create mode 100644 examples/core/main_test.go diff --git a/README.md b/README.md index eb3ea97..7769dea 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ integration. ## Examples -- [Core-only example](examples/core/main.go) +- [Core-only example](examples/core/README.md) - [ConfigKit example](integrations/ckconfig/examples/simple/README.md) - [Composed ConfigKit and PromptKit example](integrations/ckprompt/examples/composed/README.md) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a3ecf9a..b425efd 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Application-owned aliases implementation plan -Status: in progress; Stages 1–13 completed. +Status: in progress; Stages 1–14 completed. ## Authority, baseline, and scope @@ -657,6 +657,17 @@ the examples remain complete, copyable artifacts. Explain local source testing and forthcoming core dependency adoption in their developer-facing guidance; do not claim published core v0.2.0 has alias support. +**Completed:** The core example now installs command and group shortcuts, +global and scoped flag aliases in a mixed declaration slice, and prints entered +and canonical invocation paths. The composed example installs collection, +PromptKit group, bundle, smoke, scoped format, and global help aliases alongside +its complete canonical declarations. Example tests cover shortcut and +canonical operations, help with zero PromptKit engine loads, and local smoke +report delivery. Both example READMEs explain local-source use and that +published core v0.2.0 lacks alias support. Verified with `go test ./... -count=1` +at the root and the complete ckprompt module suite under the local workspace; +all newly documented alias commands also ran successfully. + ## Stage 15: Reconcile public and internal documentation **Work** diff --git a/examples/core/README.md b/examples/core/README.md new file mode 100644 index 0000000..ec989f0 --- /dev/null +++ b/examples/core/README.md @@ -0,0 +1,22 @@ +# Core example + +Run the example from the repository root: + +```sh +go run ./examples/core hello --times=0 --tag=first +go run ./examples/core ws greet --count=2 +go run ./examples/core ws --help +go run ./examples/core hello --hlep +``` + +The example installs a canonical `workspace greet` command together with a +command shortcut, a group shortcut, a scoped flag alias, and a global help +alias. Its declaration slice mixes the canonical group and routing rules as +`[]commandkit.Declaration`. The handler prints `Invocation.EnteredPath()` and +`Invocation.CanonicalPath()` so the alternate route and its canonical target +are visible in the output. + +These aliases use the current local CommandKit source. Published CommandKit +v0.2.0 does not include alias declarations; consumers can use them after the +next core release. Repository contributors can run its test with +`go test ./examples/core` from the root. diff --git a/examples/core/main.go b/examples/core/main.go index 7cc4a56..ce4aa70 100644 --- a/examples/core/main.go +++ b/examples/core/main.go @@ -37,14 +37,21 @@ func newApp() (*commandkit.App, error) { if input.Tags.Set { tags = fmt.Sprintf("explicit %v", input.Tags.Value) } - if _, err := fmt.Fprintf(invocation.Streams.Out, "Hello; count=%s; tags=%s\n", count, tags); err != nil { + if _, err := fmt.Fprintf(invocation.Streams.Out, "Hello; count=%s; tags=%s; entered=%q; canonical=%q\n", count, tags, invocation.EnteredPath(), invocation.CanonicalPath()); err != nil { return commandkit.Failure(err) } return commandkit.Success() }, }) workspace := commandkit.Group("workspace", "Workspace commands.", greet) - return commandkit.New(commandkit.Identity{Name: "core-example", Version: "1.0"}, workspace) + declarations := []commandkit.Declaration{ + workspace, + commandkit.Alias("hello", "workspace greet"), + commandkit.Alias("ws", "workspace"), + commandkit.FlagAliasFor("workspace greet", "--times", "--count"), + commandkit.FlagAlias("--hlep", "--help"), + } + return commandkit.New(commandkit.Identity{Name: "core-example", Version: "1.0"}, declarations...) } func main() { diff --git a/examples/core/main_test.go b/examples/core/main_test.go new file mode 100644 index 0000000..b68a001 --- /dev/null +++ b/examples/core/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + + commandkit "gitea.maximumdirect.net/eric/CommandKit" +) + +func TestAliasesReachCoreExampleCommand(t *testing.T) { + app, err := newApp() + if err != nil { + t.Fatal(err) + } + for _, test := range []struct { + name string + args []string + entered string + canonical string + greeting string + }{ + {name: "command shortcut and scoped flag", args: []string{"hello", "--times=0", "--tag", "one"}, entered: "hello", canonical: "workspace greet", greeting: "count=explicit 0; tags=explicit [one]"}, + {name: "group shortcut", args: []string{"ws", "greet", "--count=2"}, entered: "ws greet", canonical: "workspace greet", greeting: "count=explicit 2"}, + {name: "canonical command", args: []string{"workspace", "greet"}, entered: "workspace greet", canonical: "workspace greet", greeting: "count=default 1 (not supplied)"}, + } { + t.Run(test.name, func(t *testing.T) { + var output bytes.Buffer + outcome := app.Execute(context.Background(), test.args, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess { + t.Fatalf("Execute(%q) = %#v; output %q", test.args, outcome, output.String()) + } + for _, fragment := range []string{ + test.greeting, + "entered=\"" + test.entered + "\"", + "canonical=\"" + test.canonical + "\"", + } { + if !strings.Contains(output.String(), fragment) { + t.Errorf("output %q does not contain %q", output.String(), fragment) + } + } + }) + } +} + +func TestGlobalHelpAliasDoesNotRunCoreExampleCommand(t *testing.T) { + app, err := newApp() + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + outcome := app.Execute(context.Background(), []string{"hello", "--hlep"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "--hlep → --help") || strings.Contains(output.String(), "Hello;") { + t.Fatalf("help alias outcome/output = (%#v, %q)", outcome, output.String()) + } + output.Reset() + outcome = app.Execute(context.Background(), []string{"ws", "--help"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "greet") || strings.Contains(output.String(), "Hello;") { + t.Fatalf("group shortcut help outcome/output = (%#v, %q)", outcome, output.String()) + } +} diff --git a/integrations/ckprompt/examples/composed/README.md b/integrations/ckprompt/examples/composed/README.md index 694b405..3bb7298 100644 --- a/integrations/ckprompt/examples/composed/README.md +++ b/integrations/ckprompt/examples/composed/README.md @@ -15,8 +15,11 @@ go run . promptkit list profiles --config fixtures/after.yml --campaign autumn - go run . promptkit list backends --campaign spring go run . promptkit list namespaces --campaign spring go run . promptkit list collections --campaign spring +go run . list collections --campaign spring +go run . pk list bundles --campaign spring go run . promptkit list prompts --namespace field-notes --campaign spring go run . promptkit show prompt briefing --campaign spring --format yaml +go run . pk show prompt briefing --campaign spring --fmt yaml go run . promptkit show profile spring --campaign spring go run . promptkit requirements show briefing --campaign spring go run . promptkit requirements show briefing --campaign spring --var mode=full --format yaml @@ -24,10 +27,19 @@ go run . promptkit requirements validate briefing --campaign spring --input topi go run . promptkit requirements validate briefing --campaign spring --input topic=fixtures/topic.txt --var mode=full --input notes=fixtures/notes.txt --format yaml go run . promptkit requirements validate briefing --campaign spring go run . promptkit test text_inference --campaign spring --prompt briefing --profile spring +go run . smoke text_inference --campaign spring --prompt briefing --profile spring go run . promptkit test all --campaign spring --prompt briefing --profile spring +go run . pk --hlep go run . session restore session-42 --campaign spring ``` +`newApp` installs application-owned command and flag aliases alongside the +complete `ckconfig` and `ckprompt` declarations. `list collections` and +`promptkit list bundles` both reach the canonical collection command; `pk` +shortens the PromptKit group. `--fmt` is scoped to named prompt inspection and +`--hlep` is available globally. The `smoke` shortcut retains the canonical +`promptkit test KIND` operand, including the special `all` aggregate. + The sources and session store are small in-memory fixtures. `--campaign` is a typed selector shared by the app and PromptKit configuration scopes. Their `ckconfig.Suite[*inspectionInput, *runCache]` uses `NewInput` to initialize each @@ -92,3 +104,12 @@ required `topic` artifact is not supplied. The builder defaults variables from the selected campaign but leaves artifact bindings to the operator. The `requirements show` commands remain discovery reports: partial discovery can succeed even when a concrete request would fail validation. + +The alias examples use the local CommandKit source because the composed +module's published core dependency, v0.2.0, predates alias declarations. From +the repository root, add the three local modules to `go.work` as described in +the [developer guide](../../../../docs/development.md#documentation-validation), +then run `go test ./...` from this example directory. This checks the example +against the current local core and integration sources; it does not claim that +published v0.2.0 supports aliases. The next core dependency adoption is handled +after the core release is prepared. diff --git a/integrations/ckprompt/examples/composed/main.go b/integrations/ckprompt/examples/composed/main.go index 0f3b015..f94390f 100644 --- a/integrations/ckprompt/examples/composed/main.go +++ b/integrations/ckprompt/examples/composed/main.go @@ -200,6 +200,10 @@ func newApp() (*commandkit.App, error) { } func newAppWithLLMClient(client promptkit.LLMClient) (*commandkit.App, error) { + return newAppWithEngineLoader(client, loadPromptEngine) +} + +func newAppWithEngineLoader(client promptkit.LLMClient, engineLoader func(context.Context, ckconfig.Selection, *inspectionInput, *runCache) (*promptkit.Engine, error)) (*commandkit.App, error) { if client == nil { client = &exampleLLMClient{} } @@ -210,7 +214,7 @@ func newAppWithLLMClient(client promptkit.LLMClient) (*commandkit.App, error) { stateFactory := func() *runCache { return &runCache{llmClient: client} } appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApplicationConfig) - engineScope := ckprompt.ConfigScope(loadPromptEngine) + engineScope := ckprompt.ConfigScope(engineLoader) configCommand := ckconfig.Command(ckconfig.Suite[*inspectionInput, *runCache]{ NewInput: inputFactory, NewState: stateFactory, @@ -227,12 +231,12 @@ func newAppWithLLMClient(client promptkit.LLMClient) (*commandkit.App, error) { Scopes: []ckconfig.Scope[*inspectionInput, *runCache]{appScope, engineScope}, }) - definitions := ckprompt.FromEngine(loadPromptEngine) + definitions := ckprompt.FromEngine(engineLoader) definitions.NewInput = inputFactory definitions.NewState = stateFactory definitions.Selectors = []commandkit.Flag[*inspectionInput]{campaignSelector()} - definitions = ckprompt.WithRequestValidation(definitions, loadPromptEngine, buildValidationRequest) - definitions = ckprompt.WithSmokeTesting(definitions, loadPromptEngine, nil) + definitions = ckprompt.WithRequestValidation(definitions, engineLoader, buildValidationRequest) + definitions = ckprompt.WithSmokeTesting(definitions, engineLoader, nil) definitionCommand := ckprompt.Command(definitions) store := map[string]session{ @@ -270,12 +274,18 @@ func newAppWithLLMClient(client promptkit.LLMClient) (*commandkit.App, error) { return commandkit.Success() }, }) - return commandkit.New( - commandkit.Identity{Name: "composed-example", Version: "1.0"}, + declarations := []commandkit.Declaration{ configCommand, definitionCommand, commandkit.Group("session", "Manage saved sessions.", restore), - ) + commandkit.Alias("list collections", "promptkit list collections"), + commandkit.Alias("promptkit list bundles", "promptkit list collections"), + commandkit.Alias("pk", "promptkit"), + commandkit.Alias("smoke", "promptkit test"), + commandkit.FlagAliasFor("promptkit show prompt", "--fmt", "--format"), + commandkit.FlagAlias("--hlep", "--help"), + } + return commandkit.New(commandkit.Identity{Name: "composed-example", Version: "1.0"}, declarations...) } func parseString(raw string) (string, error) { return raw, nil } diff --git a/integrations/ckprompt/examples/composed/main_test.go b/integrations/ckprompt/examples/composed/main_test.go index a5953cf..cca2c50 100644 --- a/integrations/ckprompt/examples/composed/main_test.go +++ b/integrations/ckprompt/examples/composed/main_test.go @@ -5,6 +5,7 @@ import ( "context" "io" "reflect" + "strings" "testing" commandkit "gitea.maximumdirect.net/eric/CommandKit" @@ -350,6 +351,54 @@ func TestComposedCatalogsAndSmokeUseInjectedClientOnlyForInference(t *testing.T) } } +func TestComposedAliasesReachCanonicalCommands(t *testing.T) { + clientCalls := 0 + engineLoads := 0 + engineLoader := func(ctx context.Context, selection ckconfig.Selection, input *inspectionInput, cache *runCache) (*promptkit.Engine, error) { + engineLoads++ + return loadPromptEngine(ctx, selection, input, cache) + } + app, err := newAppWithEngineLoader(&exampleLLMClient{calls: &clientCalls}, engineLoader) + if err != nil { + t.Fatal(err) + } + + var output bytes.Buffer + outcome := app.Execute(context.Background(), []string{"pk", "--hlep"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "--hlep → --help") || strings.Contains(output.String(), "field-notes") || engineLoads != 0 || clientCalls != 0 { + t.Fatalf("group help alias outcome/output/engine loads/client calls = (%#v, %q, %d, %d)", outcome, output.String(), engineLoads, clientCalls) + } + + for _, args := range [][]string{ + {"list", "collections", "--campaign", "spring"}, + {"pk", "list", "bundles", "--campaign", "spring"}, + {"promptkit", "list", "collections", "--campaign", "spring"}, + } { + output.Reset() + outcome = app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "field-notes") { + t.Fatalf("collection route %q outcome/report = (%#v, %q)", args, outcome, output.String()) + } + } + + for _, args := range [][]string{ + {"pk", "show", "prompt", "briefing", "--campaign", "spring", "--fmt", "yaml"}, + {"promptkit", "show", "prompt", "briefing", "--campaign", "spring", "--format", "yaml"}, + } { + output.Reset() + outcome = app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "prompt_id: briefing") { + t.Fatalf("prompt inspection route %q outcome/report = (%#v, %q)", args, outcome, output.String()) + } + } + + output.Reset() + outcome = app.Execute(context.Background(), []string{"smoke", "text_inference", "--campaign", "spring", "--prompt", "briefing", "--profile", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "kind: text_inference") || !strings.Contains(output.String(), "completed: true") || clientCalls != 1 { + t.Fatalf("smoke shortcut outcome/report/client calls = (%#v, %q, %d)", outcome, output.String(), clientCalls) + } +} + func composedReportValue(t *testing.T, data []byte, category string, fields ...string) *yaml.Node { t.Helper() var document yaml.Node