* Extract a static prefix from a single parsed command element. * Returns null for commands we won't suggest (shells, eval cmdlets, path-like * invocations) or can't extract a meaningful prefix from.
( cmd: ParsedCommandElement, )
| 28 | * invocations) or can't extract a meaningful prefix from. |
| 29 | */ |
| 30 | async function extractPrefixFromElement( |
| 31 | cmd: ParsedCommandElement, |
| 32 | ): Promise<string | null> { |
| 33 | // nameType === 'application' means the raw name had path chars (./x, x\y, |
| 34 | // x.exe) — PowerShell will run a file, not a named cmdlet. Don't suggest. |
| 35 | // Same reasoning as the permission engine's nameType gate (PR #20096). |
| 36 | if (cmd.nameType === 'application') { |
| 37 | return null |
| 38 | } |
| 39 | |
| 40 | const name = cmd.name |
| 41 | if (!name) { |
| 42 | return null |
| 43 | } |
| 44 | |
| 45 | if (NEVER_SUGGEST.has(name.toLowerCase())) { |
| 46 | return null |
| 47 | } |
| 48 | |
| 49 | // Cmdlets (Verb-Noun): the name alone is the right prefix granularity. |
| 50 | // Get-Process -Name pwsh → Get-Process. There's no subcommand concept. |
| 51 | if (cmd.nameType === 'cmdlet') { |
| 52 | return name |
| 53 | } |
| 54 | |
| 55 | // External command. Guard the argv before feeding it to buildPrefix. |
| 56 | // |
| 57 | // elementTypes[0] (command name) must be a literal. `& $cmd status` has |
| 58 | // elementTypes[0]='Variable', name='$cmd' — classifies as 'unknown' (no path |
| 59 | // chars), passes NEVER_SUGGEST, getCommandSpec('$cmd')=null → returns bare |
| 60 | // '$cmd' → dead rule. Cheap to gate here. |
| 61 | // |
| 62 | // elementTypes[1..] (args) must all be StringConstant or Parameter. Anything |
| 63 | // dynamic (Variable/SubExpression/ScriptBlock/ExpandableString) would embed |
| 64 | // `$foo`/`$(...)` in the prefix → dead rule. |
| 65 | if (cmd.elementTypes?.[0] !== 'StringConstant') { |
| 66 | return null |
| 67 | } |
| 68 | for (let i = 0; i < cmd.args.length; i++) { |
| 69 | const t = cmd.elementTypes[i + 1] |
| 70 | if (t !== 'StringConstant' && t !== 'Parameter') { |
| 71 | return null |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | // Consult the fig spec — same oracle bash uses. If git's spec says -C takes |
| 76 | // a value, buildPrefix skips -C /repo and finds `status` as a subcommand. |
| 77 | // Lowercase for lookup: fig specs are filesystem paths (git.js), case- |
| 78 | // sensitive on Linux. PowerShell is case-insensitive (Git === git) so `Git` |
| 79 | // must resolve to the git spec. macOS hides this bug (case-insensitive fs). |
| 80 | // Call buildPrefix unconditionally — calculateDepth consults DEPTH_RULES |
| 81 | // before its own `if (!spec) return 2` fallback, so gcloud/aws/kubectl/az |
| 82 | // get depth-aware prefixes even without a loaded spec. The old |
| 83 | // `if (!spec) return name` short-circuit produced bare `gcloud:*` which |
| 84 | // auto-allows every gcloud subcommand. |
| 85 | const nameLower = name.toLowerCase() |
| 86 | const spec = await getCommandSpec(nameLower) |
| 87 | const prefix = await buildPrefix(name, cmd.args, spec) |
no test coverage detected