| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/command" |
| 8 | "reasonix/internal/skill" |
| 9 | ) |
| 10 | |
| 11 | const ( |
| 12 | DocsSlashName = "docs" |
| 13 | ReasonixDocsSlashName = "reasonix:docs" |
| 14 | ) |
| 15 | |
| 16 | // SlashCommandOwner identifies the runtime owner of a short slash name. Custom |
| 17 | // commands win over skills, matching Controller dispatch. A single plugin skill |
| 18 | // may own a compatible short name even though only its package-qualified name is |
| 19 | // shown in discovery surfaces. |
| 20 | type SlashCommandOwner uint8 |
| 21 | |
| 22 | const ( |
| 23 | SlashOwnerBuiltin SlashCommandOwner = iota |
| 24 | SlashOwnerSkill |
| 25 | SlashOwnerCustom |
| 26 | ) |
| 27 | |
| 28 | // ResolveSlashCommandOwner is the shared precedence decision used by dispatch |
| 29 | // and every slash-command discovery surface. |
| 30 | func ResolveSlashCommandOwner(name string, commands []command.Command, skills []skill.Skill) SlashCommandOwner { |
| 31 | name = strings.TrimPrefix(strings.TrimSpace(name), "/") |
| 32 | if name == "" { |
| 33 | return SlashOwnerBuiltin |
| 34 | } |
| 35 | for _, cmd := range commands { |
| 36 | if cmd.Name == name { |
| 37 | return SlashOwnerCustom |
| 38 | } |
| 39 | } |
| 40 | if _, ok := skill.ResolveSlashSkill(skills, name); ok { |
| 41 | return SlashOwnerSkill |
| 42 | } |
| 43 | return SlashOwnerBuiltin |
| 44 | } |
| 45 | |
| 46 | // ResolvedBuiltinSlashName returns the user-facing name for a built-in slash |
| 47 | // command. Its short name remains the default; when a compatible custom command |
| 48 | // or skill owns that short name, the built-in stays reachable through Reasonix's |
| 49 | // explicit namespace. |
| 50 | func ResolvedBuiltinSlashName(name string, commands []command.Command, skills []skill.Skill) string { |
| 51 | name = strings.TrimPrefix(strings.TrimSpace(name), "/") |
| 52 | if ResolveSlashCommandOwner(name, commands, skills) == SlashOwnerBuiltin { |
| 53 | return name |
| 54 | } |
| 55 | return QualifiedBuiltinSlashName(name, commands, skills) |
| 56 | } |
| 57 | |
| 58 | // QualifiedBuiltinSlashName returns a deterministic Reasonix-owned fallback |
| 59 | // that does not displace any existing custom command or skill. The preferred |
| 60 | // form is /reasonix:<name>; progressively more explicit names are used only if |
| 61 | // a user already owns that spelling too. |
| 62 | func QualifiedBuiltinSlashName(name string, commands []command.Command, skills []skill.Skill) string { |
| 63 | name = strings.TrimPrefix(strings.TrimSpace(name), "/") |
| 64 | candidates := []string{"reasonix:" + name, "reasonix:builtin:" + name} |
| 65 | for _, candidate := range candidates { |
| 66 | if ResolveSlashCommandOwner(candidate, commands, skills) == SlashOwnerBuiltin { |
| 67 | return candidate |
| 68 | } |
| 69 | } |
| 70 | for suffix := 2; ; suffix++ { |
| 71 | candidate := fmt.Sprintf("reasonix:builtin:%s:%d", name, suffix) |
| 72 | if ResolveSlashCommandOwner(candidate, commands, skills) == SlashOwnerBuiltin { |
| 73 | return candidate |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | // IsBuiltinDocsSlash reports whether name should execute the embedded Reasonix |
| 79 | // documentation command. The short name is built-in only when unoccupied; the |
| 80 | // qualified fallback is selected without displacing an existing command or skill. |
| 81 | func IsBuiltinDocsSlash(name string, commands []command.Command, skills []skill.Skill) bool { |
| 82 | name = strings.TrimPrefix(strings.TrimSpace(name), "/") |
| 83 | if name == DocsSlashName { |
| 84 | return ResolveSlashCommandOwner(name, commands, skills) == SlashOwnerBuiltin |
| 85 | } |
| 86 | return name == QualifiedBuiltinSlashName(DocsSlashName, commands, skills) |
| 87 | } |
| 88 |