| @@ -56,8 +56,16 @@ | ||
| 56 | 56 | } |
| 57 | 57 | $map[ $name ] = array( |
| 58 | 58 | 'callback' => $callback, |
| 59 | 59 | 'shortdesc' => isset( $cmd['shortdesc'] ) ? (string) $cmd['shortdesc'] : '', |
| 60 | + // Optional AI-facing description. `shortdesc` is CLI help | |
| 61 | + // text, written for someone who has ALREADY decided to run | |
| 62 | + // the command — "Show GZIP status (server type, active, | |
| 63 | + // mode)". A model reading tools/list has not decided yet | |
| 64 | + // and needs the opposite: when to reach for this, and what | |
| 65 | + // question it answers. Same string cannot serve both, and | |
| 66 | + // rewriting shortdesc would degrade `--help`. (#184) | |
| 67 | + 'ai_hint' => isset( $cmd['ai_hint'] ) ? (string) $cmd['ai_hint'] : '', | |
| 60 | 68 | 'synopsis' => isset( $cmd['synopsis'] ) && is_array( $cmd['synopsis'] ) ? $cmd['synopsis'] : array(), |
| 61 | 69 | 'module' => $module->slug(), |
| 62 | 70 | ); |
| 63 | 71 | } |
| @@ -154,8 +162,68 @@ | ||
| 154 | 162 | if ( '' !== $error ) { |
| 155 | 163 | $result['error'] = $error; |
| 156 | 164 | } |
| 157 | 165 | return $result; |
| 166 | + } | |
| 167 | + | |
| 168 | + /** | |
| 169 | + * Resolve any (command, args) pair to the canonical command name plus | |
| 170 | + * its leading action, using the SAME resolution `run()` performs. | |
| 171 | + * | |
| 172 | + * Callers reach one action by many spellings — `("db", ["clean"])`, | |
| 173 | + * `("database clean")`, `("xspeed db", ["clean", "--types=x"])` — and a | |
| 174 | + * guard that compares raw strings only stops the spelling it was written | |
| 175 | + * against. Anything deciding whether a call is destructive must classify | |
| 176 | + * it here, not parse the caller's input itself. | |
| 177 | + * | |
| 178 | + * @param string $command Raw command string, any accepted spelling. | |
| 179 | + * @param string[] $args Positional args, if any. | |
| 180 | + * @return array{name:string,action:string} name is '' when unresolved. | |
| 181 | + */ | |
| 182 | + public static function classify( string $command, array $args = array() ): array { | |
| 183 | + $input = self::normalize( $command ); | |
| 184 | + if ( '' === $input ) { | |
| 185 | + return array( | |
| 186 | + 'name' => '', | |
| 187 | + 'action' => '', | |
| 188 | + ); | |
| 189 | + } | |
| 190 | + | |
| 191 | + list( $name, $extra ) = self::resolve( $input, self::commands() ); | |
| 192 | + | |
| 193 | + /* | |
| 194 | + * Fall back to a structural split when the registry cannot resolve the | |
| 195 | + * input — an unbooted module, a command that is not registered on this | |
| 196 | + * install, or a bare unit-test context all leave commands() empty. | |
| 197 | + * | |
| 198 | + * Callers that ask "is this destructive?" must never be told "no" | |
| 199 | + * merely because the registry was unavailable: that turns a missing | |
| 200 | + * module into a disarmed confirmation. Splitting "xspeed db clean" | |
| 201 | + * into name "xspeed db" + action "clean" costs nothing when the | |
| 202 | + * command does not exist (run() rejects it as unknown a moment later) | |
| 203 | + * and keeps the guard closed when it does. | |
| 204 | + */ | |
| 205 | + if ( '' === $name ) { | |
| 206 | + $parts = explode( ' ', $input ); | |
| 207 | + $name = implode( ' ', array_slice( $parts, 0, 2 ) ); | |
| 208 | + $extra = array_slice( $parts, 2 ); | |
| 209 | + } | |
| 210 | + | |
| 211 | + // Trailing words from the command string come before explicit args — | |
| 212 | + // identical to run(), so the action seen here is the action that runs. | |
| 213 | + $merged = array_merge( $extra, array_values( $args ) ); | |
| 214 | + $action = ''; | |
| 215 | + foreach ( $merged as $candidate ) { | |
| 216 | + if ( is_scalar( $candidate ) && '' !== trim( (string) $candidate ) ) { | |
| 217 | + $action = strtolower( trim( (string) $candidate ) ); | |
| 218 | + break; | |
| 219 | + } | |
| 220 | + } | |
| 221 | + | |
| 222 | + return array( | |
| 223 | + 'name' => $name, | |
| 224 | + 'action' => $action, | |
| 225 | + ); | |
| 158 | 226 | } |
| 159 | 227 | |
| 160 | 228 | /** |
| 161 | 229 | * Normalize a command name: trim, collapse whitespace, and ensure the |