Compile typed command runners

This commit is contained in:
2026-10-11 01:21:52 +00:00
parent 4e0611bc13
commit 74f6bdeade
5 changed files with 69 additions and 42 deletions

View File

@@ -30,16 +30,15 @@ type versionDocument struct {
}
func (a *App) installBuiltins() error {
help := NewCommand(CommandSpec[helpInput]{
help := captureCommand(CommandSpec[helpInput]{
Name: "help",
Summary: "Show help for commands.",
Description: "Describe registered commands and their inputs.",
Arguments: []Argument[helpInput]{RepeatArgument(ArgumentMeta{Name: "COMMAND", Usage: "registered command path"}, 0, -1,
func(value string) (string, error) { return value, nil },
func(input *helpInput, value Supplied[[]string]) { input.Targets = value })},
Run: func(invocation Invocation, input helpInput) Outcome {
return a.renderHelpTargets(input.Targets.Value, invocation.Streams)
},
}, func(current commandExecution, input helpInput) Outcome {
return current.renderHelpTargets(input.Targets.Value)
})
scopeDefault := "app"
formatDefault := "text"

View File

@@ -119,13 +119,12 @@ type commandPayload interface {
hasRun() bool
bridgeFlags() []internalcobra.Flag
bridgeArguments() []internalcobra.Argument
prepare([][]string, []string, bool) (func() any, error)
checkInput(any) error
run(Invocation, any) Outcome
prepare([][]string, []string, bool) (func(*execution) Outcome, error)
}
type typedCommand[I any] struct {
spec CommandSpec[I]
spec CommandSpec[I]
privateRun func(commandExecution, I) Outcome
}
func (c typedCommand[I]) validate() error {
@@ -133,7 +132,7 @@ func (c typedCommand[I]) validate() error {
}
func (c typedCommand[I]) hasRun() bool {
return c.spec.Run != nil
return c.spec.Run != nil || c.privateRun != nil
}
func (c typedCommand[I]) bridgeFlags() []internalcobra.Flag {
@@ -167,36 +166,37 @@ func (c typedCommand[I]) bridgeArguments() []internalcobra.Argument {
return arguments
}
func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enforceRequired bool) (func() any, error) {
func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enforceRequired bool) (func(*execution) Outcome, error) {
prepared, err := prepareInput(c.spec, flagValues, operands, enforceRequired)
if err != nil {
return nil, err
}
return func() any {
return func(current *execution) Outcome {
input := prepared.build()
return &input
if c.spec.CheckInput != nil {
if err := c.spec.CheckInput(input); err != nil {
return classifyInputError(err)
}
}
if c.privateRun != nil {
return c.privateRun(current.commandContext(), input)
}
return c.spec.Run(Invocation{Context: current.ctx, Streams: current.streams}, input)
}, nil
}
func (c typedCommand[I]) run(invocation Invocation, input any) Outcome {
return c.spec.Run(invocation, *input.(*I))
}
func (c typedCommand[I]) checkInput(input any) error {
if c.spec.CheckInput == nil {
return nil
}
return c.spec.CheckInput(*input.(*I))
}
// NewCommand captures a typed command declaration. Construction issues are
// retained and reported when the declaration is installed.
func NewCommand[I any](spec CommandSpec[I]) Command {
return captureCommand(spec, nil)
}
func captureCommand[I any](spec CommandSpec[I], privateRun func(commandExecution, I) Outcome) Command {
copySpec := spec
copySpec.Flags = append([]Flag[I](nil), spec.Flags...)
copySpec.Arguments = append([]Argument[I](nil), spec.Arguments...)
copySpec.Children = append([]Command(nil), spec.Children...)
payload := typedCommand[I]{spec: copySpec}
payload := typedCommand[I]{spec: copySpec, privateRun: privateRun}
record := &commandRecord{
name: spec.Name, summary: spec.Summary, description: spec.Description,
children: copySpec.Children, payload: payload,

View File

@@ -1,6 +1,6 @@
# Implement the optional integration modules and captured registration API
Status: implementation in progress. Stages 1 through 4 are complete; subsequent work follows the ordered roadmap.
Status: implementation in progress. Stages 1 through 5 are complete; subsequent work follows the ordered roadmap.
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
assignment. This document authorizes no publication or changes to sibling
repositories.
@@ -289,9 +289,14 @@ operational/usage outcomes respectively. Root tests pass.
and writer failures. Backend parse errors become neutral usage diagnostics;
public results must not expose Cobra/pflag types.
**Completion evidence:** factories/checks/handlers execute only on the selected
ordinary path; target callbacks remain untouched by help/version. Writer failure
and wrapped wiring classifications retain causes. Stage 7 completes routing.
**Completion evidence — complete:** command payloads now compile into typed
invocation closures without `any` input handoffs. Conversion precedes input
materialization; omitted factories preserve the exact zero value. Public handlers
receive only `Invocation` and their typed input, while help uses a narrow private
presentation context through the same capture/compiler path. The path-name special
case for help is gone. Focused execution, pointer-zero-value, help/version callback
isolation, writer-failure, and wrapped-error tests pass. Stage 7 remains responsible
for routing snapshot/session consolidation.
## Stage 6: Make batch installation the sole registry mutation path

View File

@@ -33,23 +33,11 @@ func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outco
}
return current.present(current.renderHelp(parsed.Path), parsed.Path)
}
buildInput, err := command.record.payload.prepare(parsed.FlagValues, parsed.Operands, true)
run, err := command.record.payload.prepare(parsed.FlagValues, parsed.Operands, true)
if err != nil {
return current.present(Usage(err), parsed.Path)
}
input := buildInput()
if err := command.record.payload.checkInput(input); err != nil {
return current.present(classifyInputError(err), parsed.Path)
}
if len(parsed.Path) == 1 && parsed.Path[0] == "help" {
help, ok := input.(*helpInput)
if !ok {
return current.present(Failure(errors.New("built-in help input has an unexpected type")), parsed.Path)
}
return current.present(current.renderHelpTargets(help.Targets.Value), parsed.Path)
}
outcome := command.record.payload.run(Invocation{Context: ctx, Streams: streams}, input)
return current.present(outcome, parsed.Path)
return current.present(run(current), parsed.Path)
}
type execution struct {
@@ -61,6 +49,22 @@ type execution struct {
session *internalcobra.Session
}
type commandExecution struct {
invocation Invocation
renderHelpTargetsFunc func([]string) Outcome
}
func (current *execution) commandContext() commandExecution {
return commandExecution{
invocation: Invocation{Context: current.ctx, Streams: current.streams},
renderHelpTargetsFunc: current.renderHelpTargets,
}
}
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
return current.renderHelpTargetsFunc(targets)
}
func newExecution(app *App, ctx context.Context, streams Streams) *execution {
current := &execution{
app: app,

View File

@@ -60,6 +60,25 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
}
}
func TestOmittedInputFactoryUsesExactPointerZeroValue(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[*int]{
Name: "run",
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
if value != nil {
return commandkit.Failure(errors.New("omitted input factory allocated a pointer"))
}
return commandkit.Success()
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
if outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{}); outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("pointer zero value outcome = %#v", outcome)
}
}
func TestExecuteUsesFreshSessionForHelpAfterFlagBearingInvocation(t *testing.T) {
type input struct {
name commandkit.Supplied[string]