From f2d0a837058983586934ec7f93d3f749c33d1295 Mon Sep 17 00:00:00 2001 From: "CORE-FOLDCC\\Core" <1813547935@qq.com> Date: Wed, 6 May 2026 20:34:43 +0800 Subject: [PATCH] Add shell completion support --- README.md | 21 ++++ examples/lottery/README.md | 1 + specs/versions/0.1/commands.md | 14 +++ toolkit/README.md | 2 + toolkit/cmd/gsp/completion.go | 198 +++++++++++++++++++++++++++++++++ toolkit/cmd/gsp/main.go | 77 +++++++++++++ toolkit/scripts/install.ps1 | 7 +- 7 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 toolkit/cmd/gsp/completion.go diff --git a/README.md b/README.md index 47177cf..ba8e6ae 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 ` 会补全子命令,`gsp graph --format ` 会补全格式,`gsp graph ` 会读取当前工程的 GSP id。 diff --git a/examples/lottery/README.md b/examples/lottery/README.md index fc044b4..5b51a95 100644 --- a/examples/lottery/README.md +++ b/examples/lottery/README.md @@ -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 ``` diff --git a/specs/versions/0.1/commands.md b/specs/versions/0.1/commands.md index 0d40bc2..8d2942a 100644 --- a/specs/versions/0.1/commands.md +++ b/specs/versions/0.1/commands.md @@ -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. diff --git a/toolkit/README.md b/toolkit/README.md index 934e2e0..d233245 100644 --- a/toolkit/README.md +++ b/toolkit/README.md @@ -172,6 +172,7 @@ gsp init gsp validate gsp ai-init gsp version +gsp completion powershell gsp index gsp trace gsp flatten @@ -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. 输出 diff --git a/toolkit/cmd/gsp/completion.go b/toolkit/cmd/gsp/completion.go new file mode 100644 index 0000000..30ed73b --- /dev/null +++ b/toolkit/cmd/gsp/completion.go @@ -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'')' +` +} diff --git a/toolkit/cmd/gsp/main.go b/toolkit/cmd/gsp/main.go index 49c29e9..edb5db9 100644 --- a/toolkit/cmd/gsp/main.go +++ b/toolkit/cmd/gsp/main.go @@ -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 [--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) diff --git a/toolkit/scripts/install.ps1 b/toolkit/scripts/install.ps1 index 2dde215..ba49181 100644 --- a/toolkit/scripts/install.ps1 +++ b/toolkit/scripts/install.ps1 @@ -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 +}