Add shell completion support

This commit is contained in:
2026-05-06 20:34:43 +08:00
parent f7f5e2c67c
commit f2d0a83705
7 changed files with 319 additions and 1 deletions

View File

@@ -53,6 +53,7 @@ bin/gsp.exe
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format md --out .gsp\graph.md
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\graph.canvas
.\bin\gsp.exe stage-check --root examples\lottery --stage implement --out .gsp\stage-report.json
.\bin\gsp.exe completion powershell
```
输出文件:
@@ -139,6 +140,7 @@ gsp version
gsp init
gsp ai-init
gsp version
gsp completion install powershell
gsp validate
gsp index
gsp flatten <id>
@@ -174,3 +176,22 @@ project/
```bash
gsp init path/to/project --force
```
## 命令补全
输出补全脚本:
```bash
gsp completion powershell
gsp completion bash
gsp completion zsh
gsp completion fish
```
安装 PowerShell 补全:
```powershell
gsp completion install powershell
```
安装后重新打开 PowerShell`gsp <Tab>` 会补全子命令,`gsp graph --format <Tab>` 会补全格式,`gsp graph <Tab>` 会读取当前工程的 GSP id。

View File

@@ -27,4 +27,5 @@ page.lottery.main
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format mermaid
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format md --out .gsp\lottery-graph.md
.\bin\gsp.exe graph page.lottery.main --root examples\lottery --format canvas --out .gsp\lottery-graph.canvas
.\bin\gsp.exe completion install powershell
```

View File

@@ -49,6 +49,20 @@ gsp version
gsp version --json
```
## completion
Print or install shell completion scripts.
```bash
gsp completion powershell
gsp completion bash
gsp completion zsh
gsp completion fish
gsp completion install powershell
```
Completion covers subcommands, common flags, enum values, and GSP ids from `gsp index --root .`.
## validate
Validate GSP files and references.

View File

@@ -172,6 +172,7 @@ gsp init
gsp validate
gsp ai-init
gsp version
gsp completion powershell
gsp index
gsp trace <id>
gsp flatten <id>
@@ -192,6 +193,7 @@ go build -o ../bin/gsp ./cmd/gsp
../bin/gsp graph page.lottery.main --root ../examples/lottery --format mermaid --out ../.gsp/graph.mmd
../bin/gsp graph page.lottery.main --root ../examples/lottery --format md --out ../.gsp/graph.md
../bin/gsp graph page.lottery.main --root ../examples/lottery --format canvas --out ../.gsp/graph.canvas
../bin/gsp completion install powershell
```
## 5. 输出

View File

@@ -0,0 +1,198 @@
package main
func powerShellCompletionScript() string {
return `# GSP PowerShell completion
$script:GspSubcommands = @(
'init',
'ai-init',
'version',
'completion',
'validate',
'index',
'trace',
'flatten',
'pack',
'graph',
'stage-check',
'help'
)
$script:GspFlags = @{
'init' = @('--name', '--entry', '--force')
'ai-init' = @('--root', '--agents', '--skill', '--all', '--force')
'version' = @('--json')
'completion' = @('powershell', 'bash', 'zsh', 'fish', 'install')
'validate' = @('--root', '--out')
'index' = @('--root', '--out')
'trace' = @('--root', '--depth', '--out')
'flatten' = @('--root', '--depth', '--include-type', '--exclude-type', '--out')
'pack' = @('--root', '--depth', '--budget', '--include-type', '--exclude-type', '--out')
'graph' = @('--root', '--depth', '--format', '--out')
'stage-check' = @('--stage', '--root', '--out')
}
function script:Get-GspIds {
try {
$json = & gsp index --root . 2>$null
if (-not $json) { return @() }
$items = $json | ConvertFrom-Json
return @($items | ForEach-Object { $_.id })
} catch {
return @()
}
}
function script:New-GspCompletion($value, $tooltip = $null) {
if (-not $tooltip) { $tooltip = $value }
[System.Management.Automation.CompletionResult]::new($value, $value, 'ParameterValue', $tooltip)
}
Register-ArgumentCompleter -Native -CommandName gsp -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$words = @($commandAst.CommandElements | ForEach-Object { $_.ToString() })
if ($words.Count -le 1) {
return $script:GspSubcommands |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
$command = $words[1]
if ($words.Count -eq 2 -and $wordToComplete -ne '') {
return $script:GspSubcommands |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
$previous = ''
if ($wordToComplete -eq '' -and $words.Count -ge 1) {
$previous = $words[$words.Count - 1]
} elseif ($words.Count -ge 2) {
$previous = $words[$words.Count - 2]
}
switch ($previous) {
'--format' {
return @('json', 'mermaid', 'md', 'canvas') |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
'--stage' {
return @('design', 'integrate', 'implement', 'bind', 'release') |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
'--skill' {
return @('generic', 'codex') |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
}
if ($command -eq 'completion') {
if ($words -contains 'install') {
return @('powershell') |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
return @('powershell', 'bash', 'zsh', 'fish', 'install') |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
if ($wordToComplete -like '-*') {
return @($script:GspFlags[$command]) |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
if (@('trace', 'flatten', 'pack', 'graph') -contains $command) {
return Get-GspIds |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object { New-GspCompletion $_ }
}
return @()
}
`
}
func bashCompletionScript() string {
return `# GSP bash completion
_gsp_completion() {
local cur prev cmd
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
cmd="${COMP_WORDS[1]}"
local commands="init ai-init version completion validate index trace flatten pack graph stage-check help"
case "$prev" in
--format) COMPREPLY=( $(compgen -W "json mermaid md canvas" -- "$cur") ); return ;;
--stage) COMPREPLY=( $(compgen -W "design integrate implement bind release" -- "$cur") ); return ;;
--skill) COMPREPLY=( $(compgen -W "generic codex" -- "$cur") ); return ;;
esac
if [[ $COMP_CWORD -eq 1 ]]; then
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
return
fi
if [[ "$cur" == -* ]]; then
case "$cmd" in
init) COMPREPLY=( $(compgen -W "--name --entry --force" -- "$cur") ) ;;
ai-init) COMPREPLY=( $(compgen -W "--root --agents --skill --all --force" -- "$cur") ) ;;
version) COMPREPLY=( $(compgen -W "--json" -- "$cur") ) ;;
validate|index) COMPREPLY=( $(compgen -W "--root --out" -- "$cur") ) ;;
trace) COMPREPLY=( $(compgen -W "--root --depth --out" -- "$cur") ) ;;
flatten) COMPREPLY=( $(compgen -W "--root --depth --include-type --exclude-type --out" -- "$cur") ) ;;
pack) COMPREPLY=( $(compgen -W "--root --depth --budget --include-type --exclude-type --out" -- "$cur") ) ;;
graph) COMPREPLY=( $(compgen -W "--root --depth --format --out" -- "$cur") ) ;;
stage-check) COMPREPLY=( $(compgen -W "--stage --root --out" -- "$cur") ) ;;
completion) COMPREPLY=( $(compgen -W "powershell bash zsh fish install" -- "$cur") ) ;;
esac
return
fi
case "$cmd" in
trace|flatten|pack|graph)
local ids
ids=$(gsp index --root . 2>/dev/null | sed -n 's/.*"id": "\([^"]*\)".*/\1/p')
COMPREPLY=( $(compgen -W "$ids" -- "$cur") )
;;
esac
}
complete -F _gsp_completion gsp
`
}
func zshCompletionScript() string {
return `#compdef gsp
# GSP zsh completion
_gsp() {
local -a commands
commands=(
'init'
'ai-init'
'version'
'completion'
'validate'
'index'
'trace'
'flatten'
'pack'
'graph'
'stage-check'
'help'
)
_describe 'command' commands
}
_gsp "$@"
`
}
func fishCompletionScript() string {
return `# GSP fish completion
complete -c gsp -f -n '__fish_use_subcommand' -a 'init ai-init version completion validate index trace flatten pack graph stage-check help'
complete -c gsp -n '__fish_seen_subcommand_from graph' -l format -a 'json mermaid md canvas'
complete -c gsp -n '__fish_seen_subcommand_from stage-check' -l stage -a 'design integrate implement bind release'
complete -c gsp -n '__fish_seen_subcommand_from ai-init' -l skill -a 'generic codex'
complete -c gsp -n '__fish_seen_subcommand_from trace flatten pack graph' -a '(gsp index --root . 2>/dev/null | string match -r ''"id": "([^"]+)"'' | string replace -r ''.*"id": "([^"]+)".*'' ''$1'')'
`
}

View File

@@ -36,6 +36,8 @@ func run(args []string) error {
return runAIInit(args[1:])
case "version":
return runVersion(args[1:])
case "completion":
return runCompletion(args[1:])
case "validate":
return runValidate(args[1:])
case "index":
@@ -65,6 +67,8 @@ Usage:
gsp init [path] [--name project-name] [--entry project.entry] [--force]
gsp ai-init [--root .] [--agents] [--skill generic|codex] [--all] [--force]
gsp version [--json]
gsp completion powershell|bash|zsh|fish
gsp completion install powershell
gsp validate [--root .] [--out report.json]
gsp index [--root .] [--out index.json]
gsp trace <id> [--root .] [--depth 3] [--out trace.json]
@@ -75,6 +79,79 @@ Usage:
`)
}
func runCompletion(args []string) error {
if len(args) == 0 {
return fmt.Errorf("completion requires powershell, bash, zsh, fish, or install powershell")
}
if args[0] == "install" {
if len(args) != 2 {
return fmt.Errorf("completion install requires one shell")
}
if args[1] != "powershell" {
return fmt.Errorf("completion install currently supports powershell")
}
return installPowerShellCompletion()
}
if len(args) != 1 {
return fmt.Errorf("completion accepts one shell")
}
switch args[0] {
case "powershell":
return writeText("", powerShellCompletionScript())
case "bash":
return writeText("", bashCompletionScript())
case "zsh":
return writeText("", zshCompletionScript())
case "fish":
return writeText("", fishCompletionScript())
default:
return fmt.Errorf("unsupported completion shell %q", args[0])
}
}
func installPowerShellCompletion() error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
completionDir := filepath.Join(home, ".gsp", "completion")
completionFile := filepath.Join(completionDir, "gsp.ps1")
if err := os.MkdirAll(completionDir, 0755); err != nil {
return err
}
if err := os.WriteFile(completionFile, []byte(powerShellCompletionScript()), 0644); err != nil {
return err
}
profilePath := filepath.Join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
if err := os.MkdirAll(filepath.Dir(profilePath), 0755); err != nil {
return err
}
sourceLine := fmt.Sprintf(". '%s'", strings.ReplaceAll(completionFile, "'", "''"))
existing, err := os.ReadFile(profilePath)
if err != nil && !os.IsNotExist(err) {
return err
}
if !strings.Contains(string(existing), sourceLine) {
var builder strings.Builder
if len(existing) > 0 {
builder.Write(existing)
if !strings.HasSuffix(string(existing), "\n") {
builder.WriteString("\n")
}
}
builder.WriteString(sourceLine)
builder.WriteString("\n")
if err := os.WriteFile(profilePath, []byte(builder.String()), 0644); err != nil {
return err
}
}
fmt.Printf("Installed PowerShell completion to %s\n", completionFile)
fmt.Printf("Updated PowerShell profile %s\n", profilePath)
fmt.Println("Open a new PowerShell terminal or run the profile file to enable completion.")
return nil
}
func runAIInit(args []string) error {
fs := flag.NewFlagSet("ai-init", flag.ContinueOnError)
root := commonRoot(fs)

View File

@@ -1,5 +1,6 @@
param(
[string]$InstallDir = "$HOME\.gsp\bin"
[string]$InstallDir = "$HOME\.gsp\bin",
[switch]$NoCompletion
)
$ErrorActionPreference = "Stop"
@@ -36,3 +37,7 @@ else {
}
& $TargetExe version
if (-not $NoCompletion) {
& $TargetExe completion install powershell
}