Expose canonical paths on command invocations
This commit is contained in:
@@ -232,7 +232,7 @@ func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enfor
|
|||||||
if c.privateRun != nil {
|
if c.privateRun != nil {
|
||||||
return c.privateRun(current.commandContext(), input)
|
return c.privateRun(current.commandContext(), input)
|
||||||
}
|
}
|
||||||
return c.spec.Run(Invocation{Context: current.ctx, Streams: current.streams}, input)
|
return c.spec.Run(current.invocation(), input)
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Application-owned aliases implementation plan
|
# Application-owned aliases implementation plan
|
||||||
|
|
||||||
Status: in progress; Stages 1–8 completed.
|
Status: in progress; Stages 1–9 completed.
|
||||||
|
|
||||||
## Authority, baseline, and scope
|
## Authority, baseline, and scope
|
||||||
|
|
||||||
@@ -495,6 +495,13 @@ and canonical forms reach the same binding/callback once; repeated calls and
|
|||||||
declaration reuse do not leak invocation values; zero Invocation accessors
|
declaration reuse do not leak invocation values; zero Invocation accessors
|
||||||
are empty. Existing cancellation and domain outcome checks remain green.
|
are empty. Existing cancellation and domain outcome checks remain green.
|
||||||
|
|
||||||
|
**Completed:** Invocation now retains the entered and canonical route paths
|
||||||
|
alongside the caller's context and streams. Public typed handlers and private
|
||||||
|
callbacks receive the same resolved identity. Tests cover canonical and alias
|
||||||
|
calls with operands, repeated calls, declaration reuse across Apps, private
|
||||||
|
callback plumbing, and zero-value accessors. Verified with the root, ckconfig,
|
||||||
|
and ckprompt suites under the local workspace.
|
||||||
|
|
||||||
## Stage 10: Add alias-aware help and discovery views
|
## Stage 10: Add alias-aware help and discovery views
|
||||||
|
|
||||||
**Work**
|
**Work**
|
||||||
|
|||||||
11
execute.go
11
execute.go
@@ -62,11 +62,20 @@ type commandExecution struct {
|
|||||||
|
|
||||||
func (current *execution) commandContext() commandExecution {
|
func (current *execution) commandContext() commandExecution {
|
||||||
return commandExecution{
|
return commandExecution{
|
||||||
invocation: Invocation{Context: current.ctx, Streams: current.streams},
|
invocation: current.invocation(),
|
||||||
renderHelpTargetsFunc: current.renderHelpTargets,
|
renderHelpTargetsFunc: current.renderHelpTargets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (current *execution) invocation() Invocation {
|
||||||
|
return Invocation{
|
||||||
|
Context: current.ctx,
|
||||||
|
Streams: current.streams,
|
||||||
|
enteredPath: current.route.enteredPath.String(),
|
||||||
|
canonicalPath: current.route.canonicalPath.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
|
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
|
||||||
return current.renderHelpTargetsFunc(targets)
|
return current.renderHelpTargetsFunc(targets)
|
||||||
}
|
}
|
||||||
|
|||||||
11
identity.go
11
identity.go
@@ -34,4 +34,15 @@ type Invocation struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
// Streams are the caller-selected output destinations.
|
// Streams are the caller-selected output destinations.
|
||||||
Streams Streams
|
Streams Streams
|
||||||
|
|
||||||
|
enteredPath string
|
||||||
|
canonicalPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnteredPath returns the command path used to invoke the selected command.
|
||||||
|
// It is empty for an Invocation that was not created by command execution.
|
||||||
|
func (inv Invocation) EnteredPath() string { return inv.enteredPath }
|
||||||
|
|
||||||
|
// CanonicalPath returns the selected command's canonical path.
|
||||||
|
// It is empty for an Invocation that was not created by command execution.
|
||||||
|
func (inv Invocation) CanonicalPath() string { return inv.canonicalPath }
|
||||||
|
|||||||
@@ -190,6 +190,104 @@ func TestExecuteDispatchesEnteredAliasToCanonicalRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvocationReportsEnteredAndCanonicalPathsPerCall(t *testing.T) {
|
||||||
|
type input struct {
|
||||||
|
label Supplied[string]
|
||||||
|
values Supplied[[]string]
|
||||||
|
}
|
||||||
|
type observed struct {
|
||||||
|
entered string
|
||||||
|
canonical string
|
||||||
|
label string
|
||||||
|
values []string
|
||||||
|
}
|
||||||
|
var calls []observed
|
||||||
|
command := NewCommand(CommandSpec[input]{
|
||||||
|
Name: "run",
|
||||||
|
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "label"}, (*string)(nil),
|
||||||
|
func(value string) (string, error) { return value, nil },
|
||||||
|
func(target *input, label Supplied[string]) { target.label = label },
|
||||||
|
)},
|
||||||
|
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "value"}, 0, -1,
|
||||||
|
func(value string) (string, error) { return value, nil },
|
||||||
|
func(target *input, values Supplied[[]string]) { target.values = values },
|
||||||
|
)},
|
||||||
|
Run: func(inv Invocation, value input) Outcome {
|
||||||
|
calls = append(calls, observed{
|
||||||
|
entered: inv.EnteredPath(),
|
||||||
|
canonical: inv.CanonicalPath(),
|
||||||
|
label: value.label.Value,
|
||||||
|
values: append([]string(nil), value.values.Value...),
|
||||||
|
})
|
||||||
|
return Success()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
shortcut := Alias("shortcut", "workspace run")
|
||||||
|
app, err := New(Identity{Name: "Example"}, Group("workspace", "Workspace", command), shortcut)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, args := range [][]string{{"workspace", "run", "--label=first", "first"}, {"shortcut", "--label=second", "second"}, {"workspace", "run"}} {
|
||||||
|
if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
||||||
|
t.Fatalf("Execute(%q) = %v, want success", args, outcome.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(calls) != 3 {
|
||||||
|
t.Fatalf("handler calls = %d, want 3", len(calls))
|
||||||
|
}
|
||||||
|
want := []observed{
|
||||||
|
{entered: "workspace run", canonical: "workspace run", label: "first", values: []string{"first"}},
|
||||||
|
{entered: "shortcut", canonical: "workspace run", label: "second", values: []string{"second"}},
|
||||||
|
{entered: "workspace run", canonical: "workspace run"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(calls, want) {
|
||||||
|
t.Fatalf("handler invocations = %#v, want %#v", calls, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondApp, err := New(Identity{Name: "Another"}, Group("workspace", "Workspace", command), shortcut)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if outcome := secondApp.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
||||||
|
t.Fatalf("reused declaration execution = %v, want success", outcome.Kind)
|
||||||
|
}
|
||||||
|
if got := calls[3]; got.entered != "shortcut" || got.canonical != "workspace run" || len(got.values) != 0 {
|
||||||
|
t.Fatalf("reused declaration invocation = %#v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zero Invocation
|
||||||
|
if zero.EnteredPath() != "" || zero.CanonicalPath() != "" {
|
||||||
|
t.Fatalf("zero Invocation paths = (%q, %q), want empty paths", zero.EnteredPath(), zero.CanonicalPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivateCommandCallbacksReceiveResolvedInvocationPaths(t *testing.T) {
|
||||||
|
var calls []Invocation
|
||||||
|
command := Command{record: &commandRecord{
|
||||||
|
name: "inspect",
|
||||||
|
payload: typedCommand[struct{}]{
|
||||||
|
spec: CommandSpec[struct{}]{Name: "inspect"},
|
||||||
|
privateRun: func(exec commandExecution, _ struct{}) Outcome {
|
||||||
|
calls = append(calls, exec.invocation)
|
||||||
|
return Success()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{{"inspect"}, {"shortcut"}} {
|
||||||
|
if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
||||||
|
t.Fatalf("Execute(%q) = %v, want success", args, outcome.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(calls) != 2 || calls[0].EnteredPath() != "inspect" || calls[0].CanonicalPath() != "inspect" || calls[1].EnteredPath() != "shortcut" || calls[1].CanonicalPath() != "inspect" {
|
||||||
|
t.Fatalf("private callback invocations = %#v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) {
|
func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) {
|
||||||
type input struct{ color string }
|
type input struct{ color string }
|
||||||
var got string
|
var got string
|
||||||
|
|||||||
Reference in New Issue
Block a user