2026-10-07 13:16:52 +00:00
package commandkit
2026-10-19 17:23:32 +00:00
import (
"errors"
"fmt"
2026-10-19 17:27:17 +00:00
"sort"
internalalias "gitea.maximumdirect.net/eric/CommandKit/internal/alias"
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
2026-10-19 17:23:32 +00:00
)
2026-10-07 13:16:52 +00:00
2026-10-19 19:59:41 +00:00
// Register adds a batch of canonical commands and routing rules to the
// application. The complete batch is validated before registry mutation; on
// error the registry and its derived command metadata remain unchanged. A
// successful nonempty batch becomes visible to later invocations. Declarations
// remain reusable. Identical duplicate routing mappings are accepted, while
// competing targets for the same source are errors. Nil and zero declarations
// are invalid; use NoCommand for an intentionally absent optional command.
2026-10-19 17:23:32 +00:00
func ( a * App ) Register ( declarations ... Declaration ) error {
return a . installBatch ( declarations ... )
2026-10-11 01:22:47 +00:00
}
2026-10-19 17:27:17 +00:00
type installationCandidate struct {
2026-10-19 17:33:39 +00:00
commands map [ string ] * commandNode
routingRules [ ] RoutingRule
aliases * internalalias . Resolver
flagAliases map [ string ] resolvedFlagAliases
discoveryFlags map [ string ] resolvedFlagAliases
changed bool
2026-10-19 17:27:17 +00:00
}
type canonicalNode struct {
command Command
path commandpath . Path
children map [ string ] * canonicalNode
}
type commandAliasTrace struct {
exact * canonicalNode
operandOwner * canonicalNode
}
type routingRuleKey struct {
kind routingRuleKind
source string
target string
scope string
}
2026-10-19 17:23:32 +00:00
func ( a * App ) installBatch ( declarations ... Declaration ) error {
2026-10-07 13:16:52 +00:00
if a == nil {
return fmt . Errorf ( "cannot register commands on a nil app" )
}
2026-10-19 17:23:32 +00:00
commands := make ( [ ] Command , 0 , len ( declarations ) )
rules := make ( [ ] RoutingRule , 0 )
for _ , declaration := range declarations {
switch value := declaration . ( type ) {
case Command :
commands = append ( commands , value )
case * Command :
if value == nil {
return fmt . Errorf ( "nil *Command declaration" )
}
commands = append ( commands , * value )
case RoutingRule :
rules = append ( rules , value )
case * RoutingRule :
if value == nil {
return fmt . Errorf ( "nil *RoutingRule declaration" )
}
rules = append ( rules , * value )
case nil :
return fmt . Errorf ( "nil declaration" )
default :
return fmt . Errorf ( "unsupported declaration type %T" , declaration )
}
}
2026-10-07 13:16:52 +00:00
2026-10-19 17:27:17 +00:00
candidate , err := a . buildInstallationCandidate ( commands , rules )
if err != nil {
return err
}
if ! candidate . changed {
return nil
}
a . commands = candidate . commands
a . routingRules = candidate . routingRules
a . aliases = candidate . aliases
2026-10-19 17:33:39 +00:00
a . flagAliases = candidate . flagAliases
a . discoveryFlags = candidate . discoveryFlags
2026-10-19 17:27:17 +00:00
a . snapshot = nil
return nil
}
func ( a * App ) buildInstallationCandidate ( commands [ ] Command , addedRules [ ] RoutingRule ) ( * installationCandidate , error ) {
candidate := & installationCandidate {
commands : make ( map [ string ] * commandNode , len ( a . commands ) + len ( commands ) ) ,
}
for name , node := range a . commands {
candidate . commands [ name ] = node
}
2026-10-07 13:16:52 +00:00
for _ , command := range commands {
2026-10-11 01:22:47 +00:00
if err := validateCommandPlacement ( command ) ; err != nil {
2026-10-19 17:27:17 +00:00
return nil , err
2026-10-07 13:16:52 +00:00
}
2026-10-11 01:15:13 +00:00
if command . record . absent {
continue
2026-10-09 01:32:36 +00:00
}
2026-10-11 01:15:13 +00:00
name := command . record . name
2026-10-19 17:27:17 +00:00
if _ , exists := candidate . commands [ name ] ; exists {
return nil , fmt . Errorf ( "command path %q is already registered" , name )
2026-10-07 13:16:52 +00:00
}
2026-10-19 17:27:17 +00:00
candidate . commands [ name ] = & commandNode { command : command }
candidate . changed = true
}
rules , err := normalizeRoutingRules ( append ( append ( [ ] RoutingRule ( nil ) , a . routingRules ... ) , addedRules ... ) )
if err != nil {
return nil , err
}
candidate . routingRules = rules
if ! sameRoutingRules ( rules , a . routingRules ) {
candidate . changed = true
}
var mappings [ ] internalalias . Mapping
for _ , rule := range rules {
if rule . record . kind == commandAliasRule {
mappings = append ( mappings , internalalias . Mapping {
Source : rule . record . sourcePath . Clone ( ) ,
Target : rule . record . targetPath . Clone ( ) ,
} )
2026-10-11 01:15:13 +00:00
}
2026-10-09 01:32:36 +00:00
}
2026-10-19 17:27:17 +00:00
if err := internalalias . ValidateTermination ( mappings ) ; err != nil {
return nil , err
}
resolver , err := internalalias . NewResolver ( mappings )
if err != nil {
return nil , err
}
candidate . aliases = resolver
tree := buildCanonicalTree ( candidate . commands )
if err := validateCommandAliases ( rules , resolver , tree ) ; err != nil {
return nil , err
}
2026-10-19 17:33:39 +00:00
flagAliases , discoveryFlags , err := compileFlagAliases ( rules , tree , resolver )
if err != nil {
return nil , err
}
candidate . flagAliases = flagAliases
candidate . discoveryFlags = discoveryFlags
2026-10-19 17:27:17 +00:00
return candidate , nil
}
func normalizeRoutingRules ( rules [ ] RoutingRule ) ( [ ] RoutingRule , error ) {
unique := make ( map [ routingRuleKey ] RoutingRule , len ( rules ) )
commandTargets := make ( map [ string ] string )
2026-10-19 17:33:39 +00:00
type scopedFlagKey struct {
kind routingRuleKind
scope string
source string
}
flagTargets := make ( map [ scopedFlagKey ] string )
2026-10-19 17:23:32 +00:00
for _ , rule := range rules {
if rule . record == nil {
2026-10-19 17:27:17 +00:00
return nil , fmt . Errorf ( "invalid zero routing rule declaration" )
2026-10-19 17:23:32 +00:00
}
if len ( rule . record . issues ) > 0 {
2026-10-19 17:27:17 +00:00
return nil , fmt . Errorf ( "routing rule: %w" , errors . Join ( rule . record . issues ... ) )
}
key , err := routingKey ( rule . record )
if err != nil {
return nil , err
}
if key . kind == commandAliasRule {
if previous , exists := commandTargets [ key . source ] ; exists && previous != key . target {
return nil , fmt . Errorf ( "conflicting command aliases for %q" , rule . record . sourcePath . String ( ) )
}
commandTargets [ key . source ] = key . target
2026-10-19 17:33:39 +00:00
} else {
scopeKey := scopedFlagKey { kind : key . kind , scope : key . scope , source : key . source }
if previous , exists := flagTargets [ scopeKey ] ; exists && previous != key . target {
return nil , fmt . Errorf ( "conflicting flag aliases for %q" , rule . record . sourceFlag . token ( ) )
}
flagTargets [ scopeKey ] = key . target
2026-10-19 17:23:32 +00:00
}
2026-10-19 17:27:17 +00:00
unique [ key ] = rule
2026-10-19 17:23:32 +00:00
}
2026-10-19 17:27:17 +00:00
keys := make ( [ ] routingRuleKey , 0 , len ( unique ) )
for key := range unique {
keys = append ( keys , key )
}
sort . Slice ( keys , func ( i , j int ) bool {
left , right := keys [ i ] , keys [ j ]
if left . kind != right . kind {
return left . kind < right . kind
}
if left . source != right . source {
return left . source < right . source
}
if left . scope != right . scope {
return left . scope < right . scope
}
return left . target < right . target
} )
result := make ( [ ] RoutingRule , 0 , len ( keys ) )
for _ , key := range keys {
result = append ( result , unique [ key ] )
}
return result , nil
}
func routingKey ( record * routingRuleRecord ) ( routingRuleKey , error ) {
key := routingRuleKey { kind : record . kind }
switch record . kind {
case commandAliasRule :
if len ( record . sourcePath ) == 0 || len ( record . targetPath ) == 0 {
return routingRuleKey { } , fmt . Errorf ( "invalid command alias path" )
}
key . source = record . sourcePath . Key ( )
key . target = record . targetPath . Key ( )
case globalFlagAliasRule :
key . source = flagSpellingKey ( record . sourceFlag )
key . target = flagSpellingKey ( record . targetFlag )
case scopedFlagAliasRule :
if len ( record . scopePath ) == 0 {
return routingRuleKey { } , fmt . Errorf ( "invalid flag alias command scope" )
}
key . source = flagSpellingKey ( record . sourceFlag )
key . target = flagSpellingKey ( record . targetFlag )
key . scope = record . scopePath . Key ( )
default :
return routingRuleKey { } , fmt . Errorf ( "unsupported routing rule kind %d" , record . kind )
2026-10-09 01:32:36 +00:00
}
2026-10-19 17:27:17 +00:00
return key , nil
}
func flagSpellingKey ( spelling capturedFlagSpelling ) string {
if spelling . short {
return "short:" + spelling . name
2026-10-09 01:32:36 +00:00
}
2026-10-19 17:27:17 +00:00
return "long:" + spelling . name
}
func sameRoutingRules ( left , right [ ] RoutingRule ) bool {
if len ( left ) != len ( right ) {
return false
2026-10-07 13:16:52 +00:00
}
2026-10-19 17:27:17 +00:00
leftKeys := make ( map [ routingRuleKey ] struct { } , len ( left ) )
for _ , rule := range left {
key , err := routingKey ( rule . record )
if err != nil {
return false
}
leftKeys [ key ] = struct { } { }
}
for _ , rule := range right {
key , err := routingKey ( rule . record )
if err != nil {
return false
}
if _ , exists := leftKeys [ key ] ; ! exists {
return false
}
}
return true
}
func buildCanonicalTree ( commands map [ string ] * commandNode ) map [ string ] * canonicalNode {
roots := make ( map [ string ] * canonicalNode , len ( commands ) )
for name , node := range commands {
roots [ name ] = buildCanonicalNode ( node . command , commandpath . Path { name } )
}
return roots
}
func buildCanonicalNode ( command Command , path commandpath . Path ) * canonicalNode {
node := & canonicalNode { command : command , path : path . Clone ( ) , children : make ( map [ string ] * canonicalNode , len ( command . record . children ) ) }
for _ , child := range command . record . children {
if child . record == nil || child . record . absent {
continue
}
childPath := append ( path . Clone ( ) , child . record . name )
node . children [ child . record . name ] = buildCanonicalNode ( child , childPath )
}
return node
}
func validateCommandAliases ( rules [ ] RoutingRule , resolver * internalalias . Resolver , roots map [ string ] * canonicalNode ) error {
for _ , rule := range rules {
if rule . record . kind != commandAliasRule {
continue
}
record := rule . record
sourceTrace := traceCanonicalPath ( roots , record . sourcePath )
if sourceTrace . exact != nil {
return fmt . Errorf ( "command alias source %q conflicts with canonical command %q" , record . sourcePath . String ( ) , sourceTrace . exact . path . String ( ) )
}
2026-10-20 00:22:25 +00:00
if sourceTrace . operandOwner != nil && acceptsOperandPrefix ( sourceTrace . operandOwner . command , len ( record . sourcePath ) - len ( sourceTrace . operandOwner . path ) ) {
2026-10-19 17:27:17 +00:00
return fmt . Errorf ( "command alias source %q intercepts operands of canonical command %q" , record . sourcePath . String ( ) , sourceTrace . operandOwner . path . String ( ) )
}
resolvedTarget := resolver . Resolve ( record . targetPath )
trace := traceCanonicalPath ( roots , resolvedTarget )
if trace . exact != nil {
continue
}
if trace . operandOwner != nil {
return fmt . Errorf ( "command alias %q target %q includes operands for canonical command %q" , record . sourcePath . String ( ) , record . targetPath . String ( ) , trace . operandOwner . path . String ( ) )
}
return fmt . Errorf ( "command alias %q target %q does not resolve to a canonical command or group" , record . sourcePath . String ( ) , record . targetPath . String ( ) )
2026-10-19 17:23:32 +00:00
}
2026-10-07 13:16:52 +00:00
return nil
}
2026-10-19 17:27:17 +00:00
func traceCanonicalPath ( roots map [ string ] * canonicalNode , path commandpath . Path ) commandAliasTrace {
var current * canonicalNode
children := roots
2026-10-20 00:22:25 +00:00
for _ , component := range path {
2026-10-19 17:27:17 +00:00
next := children [ component ]
if next != nil {
current = next
children = current . children
continue
}
2026-10-20 00:22:25 +00:00
if current != nil && current . command . record . payload != nil && current . command . record . payload . hasRun ( ) {
2026-10-19 17:27:17 +00:00
return commandAliasTrace { operandOwner : current }
}
return commandAliasTrace { }
}
return commandAliasTrace { exact : current }
}
2026-10-20 00:20:40 +00:00
// acceptsOperandPrefix reports whether more operands could complete this prefix.
// A source need not already satisfy the command's minimum to intercept it.
func acceptsOperandPrefix ( command Command , count int ) bool {
2026-10-19 17:27:17 +00:00
if command . record == nil || command . record . payload == nil || ! command . record . payload . hasRun ( ) {
return false
}
arguments := command . record . payload . bridgeArguments ( )
2026-10-20 00:20:40 +00:00
maximum := 0
2026-10-19 17:27:17 +00:00
for _ , argument := range arguments {
if maximum >= 0 {
if argument . Max < 0 {
maximum = - 1
} else {
maximum += argument . Max
}
}
}
2026-10-20 00:20:40 +00:00
return maximum < 0 || count <= maximum
2026-10-19 17:27:17 +00:00
}