diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index e41a723b89..71fd0bfaec 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -184,6 +184,7 @@ import { sanitizeCliOutputText, shellQuoteArg, } from "./tooling"; +import { generatePowerShellCompletions } from "./powershell-completions"; // Embedded web UI — baked into compiled binaries via `with { type: "file" }` import embeddedWebUI from "./embedded-web-ui.gen"; @@ -3351,6 +3352,16 @@ const runCli = Command.run(root, { version: CLI_VERSION, }); +const isPowerShellCompletionRequest = (): boolean => { + const index = process.argv.indexOf("--completions"); + return index >= 0 && process.argv[index + 1] === "pwsh"; +}; + +if (isPowerShellCompletionRequest()) { + console.log(generatePowerShellCompletions()); + process.exit(0); +} + if (process.argv.includes("-v")) { console.log(CLI_VERSION); process.exit(0); diff --git a/apps/cli/src/powershell-completions.test.ts b/apps/cli/src/powershell-completions.test.ts new file mode 100644 index 0000000000..99289d0248 --- /dev/null +++ b/apps/cli/src/powershell-completions.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { generatePowerShellCompletions } from "./powershell-completions"; + +describe("generatePowerShellCompletions", () => { + it("registers a native completer that discovers contextual help", () => { + const script = generatePowerShellCompletions(); + + expect(script).toContain("Register-ArgumentCompleter -Native -CommandName executor"); + expect(script).toContain("& executor @arguments --help"); + expect(script).toContain("GLOBAL FLAGS"); + expect(script).toContain("SUBCOMMANDS"); + expect(script).toContain("[regex]::Escape($previous)"); + expect(script).toContain("$choices += 'pwsh'"); + expect(script).toContain("^\\s+(--[a-z0-9-]+)"); + expect(script).toContain("^\\s+([a-z][a-z0-9-]*)"); + }); +}); diff --git a/apps/cli/src/powershell-completions.ts b/apps/cli/src/powershell-completions.ts new file mode 100644 index 0000000000..4059c0d898 --- /dev/null +++ b/apps/cli/src/powershell-completions.ts @@ -0,0 +1,65 @@ +const completionScript = `# Register with: executor --completions pwsh | Invoke-Expression +# +# The completer reads Executor's contextual help at completion time. That keeps +# commands and flags aligned with the installed CLI without a second command tree. +Register-ArgumentCompleter -Native -CommandName executor -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $elements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text }) + $arguments = @($elements | Select-Object -Skip 1) + if ($arguments.Count -gt 0 -and $arguments[-1] -eq $wordToComplete) { + $arguments = @($arguments | Select-Object -SkipLast 1) + } + + $help = & executor @arguments --help 2>$null + if ($LASTEXITCODE -ne 0) { + return + } + + $previous = if ($arguments.Count -gt 0) { $arguments[-1] } else { "" } + if ($previous -like '--*') { + $escapedFlag = [regex]::Escape($previous) + $choiceLine = $help | Where-Object { + $_ -match "^\\s+$escapedFlag\\s+choice\\s+.*\\(choices:\\s+(.+)\\)$" + } | Select-Object -First 1 + if ($choiceLine -match '\\(choices:\\s+(.+)\\)$') { + $choices = @($matches[1] -split ',\\s*') + if ($previous -eq '--completions') { $choices += 'pwsh' } + $choices | + Where-Object { $_ -like "$wordToComplete*" } | + Sort-Object -Unique | + ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } + return + } + } + + $section = "" + $candidates = foreach ($line in $help) { + if ($line -match '^[A-Z ]+$') { + $section = $line.Trim() + continue + } + + if ($wordToComplete.StartsWith('-') -and ($section -eq 'GLOBAL FLAGS' -or $section -eq 'FLAGS')) { + if ($line -match '^\\s+(--[a-z0-9-]+)(?:,\\s+(-[a-z]))?\\s{2,}') { + $matches[1] + if ($matches[2]) { $matches[2] } + } + continue + } + + if (-not $wordToComplete.StartsWith('-') -and $section -eq 'SUBCOMMANDS') { + if ($line -match '^\\s+([a-z][a-z0-9-]*)\\s{2,}') { + $matches[1] + } + } + } + + $candidates | + Where-Object { $_ -like "$wordToComplete*" } | + Sort-Object -Unique | + ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } +} +`; + +export const generatePowerShellCompletions = (): string => completionScript;