( command: string, excludeSubcommand?: (element: ParsedCommandElement) => boolean, )
| 202 | * because BashTool.isReadOnly works from regex/patterns, not parsed AST. |
| 203 | */ |
| 204 | export async function getCompoundCommandPrefixesStatic( |
| 205 | command: string, |
| 206 | excludeSubcommand?: (element: ParsedCommandElement) => boolean, |
| 207 | ): Promise<string[]> { |
| 208 | const parsed = await parsePowerShellCommand(command) |
| 209 | if (!parsed.valid) { |
| 210 | return [] |
| 211 | } |
| 212 | |
| 213 | const commands = getAllCommands(parsed).filter( |
| 214 | cmd => cmd.elementType === 'CommandAst', |
| 215 | ) |
| 216 | |
| 217 | // Single command — no compound collapse needed. |
| 218 | if (commands.length <= 1) { |
| 219 | const prefix = commands[0] |
| 220 | ? await extractPrefixFromElement(commands[0]) |
| 221 | : null |
| 222 | return prefix ? [prefix] : [] |
| 223 | } |
| 224 | |
| 225 | const prefixes: string[] = [] |
| 226 | for (const cmd of commands) { |
| 227 | if (excludeSubcommand?.(cmd)) { |
| 228 | continue |
| 229 | } |
| 230 | const prefix = await extractPrefixFromElement(cmd) |
| 231 | if (prefix) { |
| 232 | prefixes.push(prefix) |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | if (prefixes.length === 0) { |
| 237 | return [] |
| 238 | } |
| 239 | |
| 240 | // Group by root command (first word) and collapse each group via |
| 241 | // word-aligned longest common prefix. `npm run test` + `npm run lint` |
| 242 | // → `npm run`. But NEVER collapse down to a bare subcommand-aware root: |
| 243 | // `git add` + `git commit` would LCP to `git`, which extractPrefixFromElement |
| 244 | // explicitly refuses as too broad (line ~119). Collapsing through that gate |
| 245 | // would suggest PowerShell(git:*) → auto-allows git push --force forever. |
| 246 | // When LCP yields a bare subcommand-aware root, drop the group entirely |
| 247 | // rather than suggest either the too-broad root or N un-collapsed rules. |
| 248 | // |
| 249 | // Bash's getCompoundCommandPrefixesStatic has this same collapse without |
| 250 | // the guard (src/utils/bash/prefix.ts:360-365) — that's a separate fix. |
| 251 | // |
| 252 | // Grouping and word-comparison are case-insensitive (PowerShell is |
| 253 | // case-insensitive: Git === git, Get-Process === get-process). The Map key |
| 254 | // is lowercased; the emitted prefix keeps the first-seen casing. |
| 255 | const groups = new Map<string, string[]>() |
| 256 | for (const prefix of prefixes) { |
| 257 | const root = prefix.split(' ')[0]! |
| 258 | const key = root.toLowerCase() |
| 259 | const group = groups.get(key) |
| 260 | if (group) { |
| 261 | group.push(prefix) |
nothing calls this directly
no test coverage detected