Install command batches through one path
This commit is contained in:
30
app.go
30
app.go
@@ -8,15 +8,13 @@ import (
|
|||||||
|
|
||||||
// App owns one command registry and its application identity.
|
// App owns one command registry and its application identity.
|
||||||
type App struct {
|
type App struct {
|
||||||
identity Identity
|
identity Identity
|
||||||
commands map[string]*commandNode
|
commands map[string]*commandNode
|
||||||
commandTree *internalcobra.Command
|
commandTree *internalcobra.Command
|
||||||
configRegistered bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type commandNode struct {
|
type commandNode struct {
|
||||||
command Command
|
command Command
|
||||||
reserved bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates an application with the supplied identity and initial commands.
|
// New creates an application with the supplied identity and initial commands.
|
||||||
@@ -27,23 +25,9 @@ func New(identity Identity, commands ...Command) (*App, error) {
|
|||||||
return nil, fmt.Errorf("application identity name must not be empty")
|
return nil, fmt.Errorf("application identity name must not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
app := &App{
|
app := &App{identity: identity, commands: make(map[string]*commandNode)}
|
||||||
identity: identity,
|
all := append(app.builtinCommands(), commands...)
|
||||||
commands: map[string]*commandNode{
|
if err := app.installBatch(all...); err != nil {
|
||||||
"help": {
|
|
||||||
command: Command{record: &commandRecord{name: "help", summary: "Show help for commands."}},
|
|
||||||
reserved: true,
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
command: Command{record: &commandRecord{name: "version", summary: "Show application version."}},
|
|
||||||
reserved: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := app.installBuiltins(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := app.Register(commands...); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return app, nil
|
return app, nil
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type versionDocument struct {
|
|||||||
Revision string `yaml:"revision"`
|
Revision string `yaml:"revision"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) installBuiltins() error {
|
func (a *App) builtinCommands() []Command {
|
||||||
help := captureCommand(CommandSpec[helpInput]{
|
help := captureCommand(CommandSpec[helpInput]{
|
||||||
Name: "help",
|
Name: "help",
|
||||||
Summary: "Show help for commands.",
|
Summary: "Show help for commands.",
|
||||||
@@ -61,9 +61,7 @@ func (a *App) installBuiltins() error {
|
|||||||
return renderVersionOutcome(identity, input.Format, invocation.Streams)
|
return renderVersionOutcome(identity, input.Format, invocation.Streams)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
a.commands["help"] = &commandNode{command: help, reserved: true}
|
return []Command{help, version}
|
||||||
a.commands["version"] = &commandNode{command: version, reserved: true}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseVersionScope(value string) (string, error) {
|
func parseVersionScope(value string) (string, error) {
|
||||||
|
|||||||
13
command.go
13
command.go
@@ -263,12 +263,16 @@ func captureComposition(record *commandRecord) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateCommandDeclaration(command Command) error {
|
func validateCommandDeclaration(command Command) error {
|
||||||
|
return validateCommandPlacement(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCommandPlacement(command Command) error {
|
||||||
var issues []error
|
var issues []error
|
||||||
collectDeclarationIssues(command, nil, &issues)
|
collectDeclarationIssues(command, nil, true, &issues)
|
||||||
return errors.Join(issues...)
|
return errors.Join(issues...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectDeclarationIssues(command Command, parents []string, issues *[]error) {
|
func collectDeclarationIssues(command Command, parents []string, root bool, issues *[]error) {
|
||||||
if command.record == nil {
|
if command.record == nil {
|
||||||
*issues = append(*issues, errors.New("invalid zero command declaration"))
|
*issues = append(*issues, errors.New("invalid zero command declaration"))
|
||||||
return
|
return
|
||||||
@@ -283,6 +287,9 @@ func collectDeclarationIssues(command Command, parents []string, issues *[]error
|
|||||||
*issues = append(*issues, issue)
|
*issues = append(*issues, issue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if record.rootOnly && !root {
|
||||||
|
*issues = append(*issues, fmt.Errorf("root-only command %q is nested beneath another command", label))
|
||||||
|
}
|
||||||
if record.invalid || record.absent {
|
if record.invalid || record.absent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -290,7 +297,7 @@ func collectDeclarationIssues(command Command, parents []string, issues *[]error
|
|||||||
if child.record == nil {
|
if child.record == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
collectDeclarationIssues(child, path, issues)
|
collectDeclarationIssues(child, path, false, issues)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Implement the optional integration modules and captured registration API
|
# Implement the optional integration modules and captured registration API
|
||||||
|
|
||||||
Status: implementation in progress. Stages 1 through 5 are complete; subsequent work follows the ordered roadmap.
|
Status: implementation in progress. Stages 1 through 6 are complete; subsequent work follows the ordered roadmap.
|
||||||
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
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
|
assignment. This document authorizes no publication or changes to sibling
|
||||||
repositories.
|
repositories.
|
||||||
@@ -321,10 +321,14 @@ for routing snapshot/session consolidation.
|
|||||||
`configRegistered`, integration collision prechecks, and root namespace
|
`configRegistered`, integration collision prechecks, and root namespace
|
||||||
allowlists. Integration factories install through the public declaration path.
|
allowlists. Integration factories install through the public declaration path.
|
||||||
|
|
||||||
**Completion evidence:** conflicting roots in either order, invalid final batch
|
**Completion evidence — complete:** `New` now submits real help/version
|
||||||
member, nested root-only commands, no-op errors, explicit absence, empty real
|
commands and caller declarations to the same private batch installer used by
|
||||||
groups, and declaration reuse across two apps. Inspect that registry writes have
|
`Register`. Retained errors are checked before absence; nested root-only placement,
|
||||||
one boundary, without making its private name a public contract.
|
root/batch collisions, and invalid final members fail before registry mutation.
|
||||||
|
No-op-only registration leaves the cached tree intact; real empty groups and
|
||||||
|
reused declarations install correctly. Direct built-in writes, placeholders,
|
||||||
|
`configRegistered`, and the registry reservation flag were removed. Registry
|
||||||
|
mutation now has one boundary. Root tests pass.
|
||||||
|
|
||||||
## Stage 7: Connect neutral routing, Cobra sessions, and execution
|
## Stage 7: Connect neutral routing, Cobra sessions, and execution
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func (a *App) commandAt(path []string) (Command, bool) {
|
|||||||
return Command{}, true
|
return Command{}, true
|
||||||
}
|
}
|
||||||
command, ok := a.commands[path[0]]
|
command, ok := a.commands[path[0]]
|
||||||
if !ok || (command.reserved && command.command.record.payload == nil) {
|
if !ok {
|
||||||
return Command{}, false
|
return Command{}, false
|
||||||
}
|
}
|
||||||
current := command.command
|
current := command.command
|
||||||
|
|||||||
@@ -26,13 +26,8 @@ func (a *App) bridgeTree() *internalcobra.Command {
|
|||||||
func (a *App) buildBridgeTree() *internalcobra.Command {
|
func (a *App) buildBridgeTree() *internalcobra.Command {
|
||||||
root := &internalcobra.Command{Name: a.identity.Name}
|
root := &internalcobra.Command{Name: a.identity.Name}
|
||||||
names := make([]string, 0, len(a.commands))
|
names := make([]string, 0, len(a.commands))
|
||||||
for name, node := range a.commands {
|
for name := range a.commands {
|
||||||
if node.command.record != nil && node.command.record.absent {
|
names = append(names, name)
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !node.reserved || node.command.record.payload != nil {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
sort.Strings(names)
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ import "fmt"
|
|||||||
// Register adds a batch of root commands to the application. The batch is
|
// Register adds a batch of root commands to the application. The batch is
|
||||||
// validated before the registry is changed.
|
// validated before the registry is changed.
|
||||||
func (a *App) Register(commands ...Command) error {
|
func (a *App) Register(commands ...Command) error {
|
||||||
|
return a.installBatch(commands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) installBatch(commands ...Command) error {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return fmt.Errorf("cannot register commands on a nil app")
|
return fmt.Errorf("cannot register commands on a nil app")
|
||||||
}
|
}
|
||||||
|
|
||||||
pending := make(map[string]*commandNode, len(commands))
|
pending := make(map[string]*commandNode, len(commands))
|
||||||
for _, command := range commands {
|
for _, command := range commands {
|
||||||
if err := validateCommandDeclaration(command); err != nil {
|
if err := validateCommandPlacement(command); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if command.record.absent {
|
if command.record.absent {
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ func TestAppsHaveIndependentRegistries(t *testing.T) {
|
|||||||
if _, exists := second.commands["custom"]; exists {
|
if _, exists := second.commands["custom"]; exists {
|
||||||
t.Fatal("registration leaked to another app")
|
t.Fatal("registration leaked to another app")
|
||||||
}
|
}
|
||||||
if !first.commands["help"].reserved || !first.commands["version"].reserved {
|
if first.commands["help"] == nil || first.commands["help"].command.record.payload == nil || first.commands["version"] == nil || first.commands["version"].command.record.payload == nil {
|
||||||
t.Fatal("New did not reserve generated root commands")
|
t.Fatal("New did not install real help and version declarations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +138,55 @@ func TestRootGroupRetainsPlacementMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBatchInstallationRejectsNestedRootOnlyAndKeepsExistingState(t *testing.T) {
|
||||||
|
root := RootGroup("config", "Configuration.")
|
||||||
|
app, err := New(Identity{Name: "Example"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := app.Register(Group("outer", "", root)); err == nil {
|
||||||
|
t.Fatal("installation accepted nested root-only declaration")
|
||||||
|
}
|
||||||
|
if _, exists := app.commands["outer"]; exists {
|
||||||
|
t.Fatal("failed batch partially installed its parent")
|
||||||
|
}
|
||||||
|
if err := app.Register(root); err != nil {
|
||||||
|
t.Fatalf("root-level declaration rejected: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoOpOnlyRegistrationPreservesRoutingSnapshot(t *testing.T) {
|
||||||
|
app, err := New(Identity{Name: "Example"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before := app.bridgeTree()
|
||||||
|
if err := app.Register(NoCommand()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if after := app.bridgeTree(); after != before {
|
||||||
|
t.Fatal("no-op-only registration invalidated derived metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapturedDeclarationCanBeInstalledIntoMultipleApps(t *testing.T) {
|
||||||
|
declaration := Group("tools", "", NewCommand(CommandSpec[struct{}]{Name: "run"}))
|
||||||
|
first, err := New(Identity{Name: "First"}, declaration)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
second, err := New(Identity{Name: "Second"}, declaration)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := first.commandAt([]string{"tools", "run"}); !ok {
|
||||||
|
t.Fatal("first app lost reused declaration")
|
||||||
|
}
|
||||||
|
if _, ok := second.commandAt([]string{"tools", "run"}); !ok {
|
||||||
|
t.Fatal("second app lost reused declaration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWithErrorCopiesDeclarationAndRetainsWrappedCauses(t *testing.T) {
|
func TestWithErrorCopiesDeclarationAndRetainsWrappedCauses(t *testing.T) {
|
||||||
first := errors.New("first construction issue")
|
first := errors.New("first construction issue")
|
||||||
second := errors.New("second construction issue")
|
second := errors.New("second construction issue")
|
||||||
|
|||||||
Reference in New Issue
Block a user