( root: CommandNode, contextPath: readonly string[], tokens: readonly string[], )
| 145 | } |
| 146 | |
| 147 | export function resolveCommand( |
| 148 | root: CommandNode, |
| 149 | contextPath: readonly string[], |
| 150 | tokens: readonly string[], |
| 151 | ): Resolution { |
| 152 | if (tokens.length === 0) return { kind: "noop" }; |
| 153 | |
| 154 | const first = tokens[0].toLowerCase(); |
| 155 | const meta = META_NAMES[first]; |
| 156 | if (meta) { |
| 157 | return { kind: "meta", name: meta, args: tokens.slice(1) }; |
| 158 | } |
| 159 | |
| 160 | // Leading sigil: `evals` strips itself and resolves the remainder |
| 161 | // from root, regardless of current context. |
| 162 | if (first === "evals") { |
| 163 | const rest = tokens.slice(1); |
| 164 | if (rest.length === 0) { |
| 165 | return { kind: "meta", name: "to-root", args: [] }; |
| 166 | } |
| 167 | const m = matchPath(root, rest); |
| 168 | if (m.matchedNames.length > 0) { |
| 169 | return { |
| 170 | kind: "run", |
| 171 | node: m.node, |
| 172 | args: m.remaining, |
| 173 | absolutePath: m.matchedNames, |
| 174 | }; |
| 175 | } |
| 176 | return { kind: "unknown", token: rest[0], context: [] }; |
| 177 | } |
| 178 | |
| 179 | // Relative — match against the current context, no root fallback. |
| 180 | const current = walkPath(root, contextPath); |
| 181 | const m = matchPath(current, tokens); |
| 182 | if (m.matchedNames.length > 0) { |
| 183 | return { |
| 184 | kind: "run", |
| 185 | node: m.node, |
| 186 | args: m.remaining, |
| 187 | absolutePath: [...contextPath, ...m.matchedNames], |
| 188 | }; |
| 189 | } |
| 190 | |
| 191 | return { kind: "unknown", token: tokens[0], context: contextPath }; |
| 192 | } |
| 193 | |
| 194 | // --------------------------------------------------------------------------- |
| 195 | // Prompt rendering |
no test coverage detected