From acd6c389d04f9c5cac160721f9b98578827be362 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Mon, 13 Oct 2025 08:19:06 +1100 Subject: [PATCH 1/2] fix tab completion issues --- MANIFEST.in | 5 + setup.py | 20 +- shell_completions/README.md | 65 ++++++ .../stackql-deploy-completion.bash | 108 ++++++++++ .../stackql-deploy-completion.fish | 83 ++++++++ .../stackql-deploy-completion.ps1 | 146 +++++++++++++ .../stackql-deploy-completion.zsh | 71 +++++++ stackql_deploy/cli.py | 196 ++++++++++++++---- 8 files changed, 645 insertions(+), 49 deletions(-) create mode 100644 shell_completions/README.md create mode 100644 shell_completions/stackql-deploy-completion.bash create mode 100644 shell_completions/stackql-deploy-completion.fish create mode 100644 shell_completions/stackql-deploy-completion.ps1 create mode 100644 shell_completions/stackql-deploy-completion.zsh diff --git a/MANIFEST.in b/MANIFEST.in index b4d760a..6e3cd24 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,9 @@ # MANIFEST.in +include LICENSE include README.rst recursive-include stackql_deploy/templates *.template include stackql_deploy/inc/contributors.csv +include shell_completions/*.bash +include shell_completions/*.zsh +include shell_completions/*.fish +include shell_completions/*.ps1 \ No newline at end of file diff --git a/setup.py b/setup.py index 05cf1e8..553d631 100644 --- a/setup.py +++ b/setup.py @@ -22,18 +22,28 @@ package_data={ 'stackql_deploy': [ 'templates/**/*.template', # Include template files recursively - 'contributors.csv' + 'inc/contributors.csv' # Fixed path for contributors ], }, - + + # Install shell completion scripts to system share directory + data_files=[ + ('share/stackql-deploy/completions', [ + 'shell_completions/stackql-deploy-completion.bash', + 'shell_completions/stackql-deploy-completion.zsh', + 'shell_completions/stackql-deploy-completion.fish', + 'shell_completions/stackql-deploy-completion.ps1', + ]) + ], + include_package_data=True, install_requires=[ 'click', 'python-dotenv', 'jinja2', - 'pystackql>=3.6.1', + 'pystackql>=3.8.1', 'PyYAML' - ], + ], entry_points={ 'console_scripts': [ 'stackql-deploy = stackql_deploy.cli:cli', @@ -51,4 +61,4 @@ 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', ] -) +) \ No newline at end of file diff --git a/shell_completions/README.md b/shell_completions/README.md new file mode 100644 index 0000000..dc02ec6 --- /dev/null +++ b/shell_completions/README.md @@ -0,0 +1,65 @@ +# Shell Completions for stackql-deploy + +This directory contains tab completion scripts for various shells. + +## Automatic Installation + +The easiest way to install completions: + +```bash +stackql-deploy completion bash --install # for bash +stackql-deploy completion zsh --install # for zsh +stackql-deploy completion fish --install # for fish +stackql-deploy completion powershell --install # for PowerShell +``` + +### Activation + +To activate immediately (`bash` example shown, similar logic for other shells): + +```bash +eval "$(stackql-deploy completion bash)" +``` + +## Manual Installation + +### Bash + +```bash +# Add to ~/.bashrc +echo 'eval "$(stackql-deploy completion bash)"' >> ~/.bashrc +source ~/.bashrc +``` + +### Zsh + +```bash +# Add to ~/.zshrc +echo 'eval "$(stackql-deploy completion zsh)"' >> ~/.zshrc +source ~/.zshrc +``` + +### Fish + +```fish +# Add to ~/.config/fish/config.fish +echo 'stackql-deploy completion fish | source' >> ~/.config/fish/config.fish +source ~/.config/fish/config.fish +``` + +### PowerShell + +```powershell +# Add to your PowerShell profile +Add-Content $PROFILE "`n# stackql-deploy completion`n. (stackql-deploy completion powershell)" +. $PROFILE +``` + +## Files + +- `stackql-deploy-completion.bash` - Bash completion script +- `stackql-deploy-completion.zsh` - Zsh completion script +- `stackql-deploy-completion.fish` - Fish completion script +- `stackql-deploy-completion.ps1` - PowerShell completion script + +All scripts are static (no Python subprocess calls) for instant performance. diff --git a/shell_completions/stackql-deploy-completion.bash b/shell_completions/stackql-deploy-completion.bash new file mode 100644 index 0000000..6b3a6d0 --- /dev/null +++ b/shell_completions/stackql-deploy-completion.bash @@ -0,0 +1,108 @@ +_stackql_deploy_completion() { + local cur prev opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Main commands + local commands="build test teardown info init upgrade shell completion" + + # Common options for build/test/teardown + local common_opts="--log-level --env-file -e --env --dry-run --show-queries --on-failure --custom-registry --download-dir --help" + + # Get the command (first non-option argument) + local cmd="" + for ((i=1; i<${#COMP_WORDS[@]}-1; i++)); do + if [[ ${COMP_WORDS[i]} != -* ]]; then + cmd=${COMP_WORDS[i]} + break + fi + done + + # Completion logic + case "${cmd}" in + build|test|teardown) + # After command, need stack_dir then stack_env + local args=() + for ((i=2; i<${#COMP_WORDS[@]}-1; i++)); do + if [[ ${COMP_WORDS[i]} != -* ]]; then + args+=("${COMP_WORDS[i]}") + fi + done + + if [ ${#args[@]} -eq 0 ]; then + # Complete directory names for stack_dir + compopt -o dirnames + COMPREPLY=( $(compgen -d -- "${cur}") ) + elif [ ${#args[@]} -eq 1 ]; then + # Complete common environment names + COMPREPLY=( $(compgen -W "dev staging prod test prd sit uat" -- "${cur}") ) + else + # Complete options + COMPREPLY=( $(compgen -W "${common_opts}" -- "${cur}") ) + fi + ;; + + init) + # init [--provider] + case "${prev}" in + --provider) + COMPREPLY=( $(compgen -W "aws google azure" -- "${cur}") ) + ;; + init) + # Just type the stack name, no completion + ;; + *) + COMPREPLY=( $(compgen -W "--provider --help" -- "${cur}") ) + ;; + esac + ;; + + completion) + COMPREPLY=( $(compgen -W "bash zsh fish powershell" -- "${cur}") ) + ;; + + info|upgrade|shell) + COMPREPLY=( $(compgen -W "--help --custom-registry --download-dir" -- "${cur}") ) + ;; + + *) + # No command yet, show main commands and global options + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "--help --version" -- "${cur}") ) + else + COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") ) + fi + ;; + esac + + # Handle option arguments + case "${prev}" in + --log-level) + COMPREPLY=( $(compgen -W "DEBUG INFO WARNING ERROR CRITICAL" -- "${cur}") ) + return 0 + ;; + --env-file) + compopt -o default + COMPREPLY=( $(compgen -f -X '!*.env' -- "${cur}") $(compgen -d -- "${cur}") ) + return 0 + ;; + --on-failure) + COMPREPLY=( $(compgen -W "rollback ignore error" -- "${cur}") ) + return 0 + ;; + --custom-registry) + # URL completion - just let user type + return 0 + ;; + --download-dir) + compopt -o dirnames + COMPREPLY=( $(compgen -d -- "${cur}") ) + return 0 + ;; + esac + + return 0 +} + +complete -F _stackql_deploy_completion stackql-deploy diff --git a/shell_completions/stackql-deploy-completion.fish b/shell_completions/stackql-deploy-completion.fish new file mode 100644 index 0000000..9e4cedf --- /dev/null +++ b/shell_completions/stackql-deploy-completion.fish @@ -0,0 +1,83 @@ +# stackql-deploy completions for fish + +# Remove any existing completions +complete -c stackql-deploy -e + +# Main commands +complete -c stackql-deploy -n "__fish_use_subcommand" -a "build" -d "Create or update resources" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "test" -d "Run test queries for the stack" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "teardown" -d "Teardown a provisioned stack" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "info" -d "Display version information" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "init" -d "Initialize a new project structure" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "upgrade" -d "Upgrade pystackql and stackql binary" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "shell" -d "Launch the stackql shell" +complete -c stackql-deploy -n "__fish_use_subcommand" -a "completion" -d "Install tab completion" + +# Common options for build/test/teardown +set -l common_cmds "build test teardown" + +# --log-level +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l log-level -d "Set logging level" -a "DEBUG INFO WARNING ERROR CRITICAL" + +# --env-file +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l env-file -d "Environment variables file" -r -F + +# -e/--env +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -s e -l env -d "Set additional environment variables" + +# --dry-run +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l dry-run -d "Perform a dry run" + +# --show-queries +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l show-queries -d "Show queries in output logs" + +# --on-failure +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l on-failure -d "Action on failure" -a "rollback ignore error" + +# --custom-registry +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l custom-registry -d "Custom registry URL" + +# --download-dir +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l download-dir -d "Download directory" -r -a "(__fish_complete_directories)" + +# --help +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds" -l help -d "Show help message" + +# build/test/teardown positional arguments +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds; and not __fish_seen_argument -l log-level -l env-file -s e -l env -l dry-run -l show-queries -l on-failure -l custom-registry -l download-dir" -a "(__fish_complete_directories)" -d "Stack directory" + +# Environment names (for second positional argument) +function __stackql_deploy_needs_env + set -l cmd (commandline -opc) + set -l cmd_count (count $cmd) + # If we have: stackql-deploy build [] + if test $cmd_count -ge 3 + set -l has_opts 0 + for arg in $cmd[3..-1] + if string match -q -- '-*' $arg + set has_opts 1 + break + end + end + if test $has_opts -eq 0 + return 0 + end + end + return 1 +end + +complete -c stackql-deploy -n "__fish_seen_subcommand_from $common_cmds; and __stackql_deploy_needs_env" -a "dev staging prod test prd sit uat" -d "Environment" + +# init command +complete -c stackql-deploy -n "__fish_seen_subcommand_from init" -l provider -d "Specify provider" -a "aws google azure" +complete -c stackql-deploy -n "__fish_seen_subcommand_from init" -l help -d "Show help message" + +# completion command +complete -c stackql-deploy -n "__fish_seen_subcommand_from completion" -a "bash zsh fish powershell" -d "Shell type" +complete -c stackql-deploy -n "__fish_seen_subcommand_from completion" -l install -d "Install completion" +complete -c stackql-deploy -n "__fish_seen_subcommand_from completion" -l help -d "Show help message" + +# info/upgrade/shell commands +complete -c stackql-deploy -n "__fish_seen_subcommand_from info upgrade shell" -l help -d "Show help message" +complete -c stackql-deploy -n "__fish_seen_subcommand_from info upgrade shell" -l custom-registry -d "Custom registry URL" +complete -c stackql-deploy -n "__fish_seen_subcommand_from info upgrade shell" -l download-dir -d "Download directory" -r -a "(__fish_complete_directories)" diff --git a/shell_completions/stackql-deploy-completion.ps1 b/shell_completions/stackql-deploy-completion.ps1 new file mode 100644 index 0000000..77de13e --- /dev/null +++ b/shell_completions/stackql-deploy-completion.ps1 @@ -0,0 +1,146 @@ +# stackql-deploy PowerShell completion + +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName stackql-deploy -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commands = @{ + 'build' = 'Create or update resources' + 'test' = 'Run test queries for the stack' + 'teardown' = 'Teardown a provisioned stack' + 'info' = 'Display version information' + 'init' = 'Initialize a new project structure' + 'upgrade' = 'Upgrade pystackql and stackql binary' + 'shell' = 'Launch the stackql shell' + 'completion' = 'Install tab completion' + } + + $commonOptions = @{ + '--log-level' = @('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + '--env-file' = @() # File completion + '-e' = @() + '--env' = @() + '--dry-run' = @() + '--show-queries' = @() + '--on-failure' = @('rollback', 'ignore', 'error') + '--custom-registry' = @() + '--download-dir' = @() # Directory completion + '--help' = @() + } + + $environments = @('dev', 'staging', 'prod', 'test', 'prd', 'sit', 'uat') + $providers = @('aws', 'google', 'azure') + $shells = @('bash', 'zsh', 'fish', 'powershell') + + # Parse command line + $tokens = $commandAst.ToString().Split(' ', [StringSplitOptions]::RemoveEmptyEntries) + $command = $null + $argCount = 0 + + for ($i = 1; $i -lt $tokens.Count; $i++) { + if ($tokens[$i] -notmatch '^-') { + if ($null -eq $command) { + $command = $tokens[$i] + } else { + $argCount++ + } + } + } + + # Complete based on position + if ($null -eq $command) { + # Complete main commands + $commands.GetEnumerator() | Where-Object { $_.Key -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_.Key, $_.Key, 'ParameterValue', $_.Value) + } + return + } + + # Command-specific completion + switch ($command) { + { $_ -in 'build', 'test', 'teardown' } { + if ($argCount -eq 0) { + # Complete directories for stack_dir + Get-ChildItem -Directory -Path . -Filter "$wordToComplete*" | ForEach-Object { + [CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', 'Stack directory') + } + } + elseif ($argCount -eq 1) { + # Complete environment names + $environments | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterValue', 'Environment') + } + } + else { + # Complete options + $commonOptions.GetEnumerator() | Where-Object { $_.Key -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_.Key, $_.Key, 'ParameterName', $_.Key) + } + } + } + + 'init' { + if ($wordToComplete -like '--*') { + '--provider', '--help' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterName', $_) + } + } + elseif ($tokens[-2] -eq '--provider') { + $providers | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterValue', 'Provider') + } + } + } + + 'completion' { + if ($argCount -eq 0) { + $shells | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterValue', 'Shell type') + } + } + '--install', '--help' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterName', $_) + } + } + + { $_ -in 'info', 'upgrade', 'shell' } { + if ($wordToComplete -like '--*') { + '--help', '--custom-registry', '--download-dir' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterName', $_) + } + } + } + } + + # Handle option values + $lastToken = $tokens[-2] + switch ($lastToken) { + '--log-level' { + $commonOptions['--log-level'] | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterValue', 'Log level') + } + } + '--on-failure' { + $commonOptions['--on-failure'] | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterValue', 'Failure action') + } + } + '--env-file' { + Get-ChildItem -File -Path . -Filter "*$wordToComplete*.env" | ForEach-Object { + [CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', 'Environment file') + } + } + '--download-dir' { + Get-ChildItem -Directory -Path . -Filter "$wordToComplete*" | ForEach-Object { + [CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', 'Download directory') + } + } + '--provider' { + $providers | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [CompletionResult]::new($_, $_, 'ParameterValue', 'Provider') + } + } + } +} diff --git a/shell_completions/stackql-deploy-completion.zsh b/shell_completions/stackql-deploy-completion.zsh new file mode 100644 index 0000000..94651c7 --- /dev/null +++ b/shell_completions/stackql-deploy-completion.zsh @@ -0,0 +1,71 @@ +#compdef stackql-deploy + +_stackql_deploy() { + local -a commands + commands=( + 'build:Create or update resources' + 'test:Run test queries for the stack' + 'teardown:Teardown a provisioned stack' + 'info:Display version information' + 'init:Initialize a new project structure' + 'upgrade:Upgrade pystackql and stackql binary' + 'shell:Launch the stackql shell' + 'completion:Install tab completion' + ) + + local -a common_opts + common_opts=( + '--log-level[Set logging level]:level:(DEBUG INFO WARNING ERROR CRITICAL)' + '--env-file[Environment variables file]:file:_files -g "*.env"' + '(-e --env)'{-e,--env}'[Set additional environment variables]:var:' + '--dry-run[Perform a dry run]' + '--show-queries[Show queries in output logs]' + '--on-failure[Action on failure]:action:(rollback ignore error)' + '--custom-registry[Custom registry URL]:url:' + '--download-dir[Download directory]:dir:_directories' + '--help[Show help message]' + ) + + _arguments -C \ + '1: :->command' \ + '*::arg:->args' + + case $state in + command) + _describe -t commands 'stackql-deploy commands' commands + ;; + args) + case $words[1] in + build|test|teardown) + if (( CURRENT == 2 )); then + _arguments '2:stack directory:_directories' + elif (( CURRENT == 3 )); then + _arguments '3:environment:(dev staging prod test prd sit uat)' + else + _arguments $common_opts + fi + ;; + init) + _arguments \ + '2:stack name:' \ + '--provider[Specify provider]:provider:(aws google azure)' \ + '--help[Show help message]' + ;; + completion) + _arguments \ + '2:shell:(bash zsh fish powershell)' \ + '--install[Install completion]' \ + '--help[Show help message]' + ;; + info|upgrade|shell) + _arguments \ + '--help[Show help message]' \ + '--custom-registry[Custom registry URL]:url:' \ + '--download-dir[Download directory]:dir:_directories' + ;; + esac + ;; + esac +} + +_stackql_deploy "$@" diff --git a/stackql_deploy/cli.py b/stackql_deploy/cli.py index 683b5f0..1cabecd 100644 --- a/stackql_deploy/cli.py +++ b/stackql_deploy/cli.py @@ -5,11 +5,12 @@ import subprocess from . import __version__ as deploy_version + from .lib.bootstrap import logger from .lib.utils import print_unicode_box, BorderColor -from .cmd.build import StackQLProvisioner -from .cmd.test import StackQLTestRunner -from .cmd.teardown import StackQLDeProvisioner +# from .cmd.build import StackQLProvisioner +# from .cmd.test import StackQLTestRunner +# from .cmd.teardown import StackQLDeProvisioner from jinja2 import Environment, FileSystemLoader from dotenv import dotenv_values from pystackql import StackQL @@ -166,6 +167,9 @@ def build(ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir ): """Create or update resources.""" + + from .cmd.build import StackQLProvisioner + stackql, env_vars = setup_command_context( ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir, 'build' @@ -198,6 +202,9 @@ def teardown(ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir ): """Teardown a provisioned stack.""" + + from .cmd.teardown import StackQLDeProvisioner + stackql, env_vars = setup_command_context( ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir, 'teardown' @@ -229,6 +236,9 @@ def teardown(ctx, stack_dir, stack_env, log_level, env_file, def test(ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir): """Run test queries for the stack.""" + + from .cmd.test import StackQLTestRunner + stackql, env_vars = setup_command_context( ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir, 'test' @@ -381,47 +391,6 @@ def upgrade(ctx): stackql.upgrade() -# -# completion command -# - -@cli.command() -@click.argument( - 'shell', - type=click.Choice(['bash', 'zsh', 'fish', 'powershell'], case_sensitive=False) -) -def completion(shell): - """Generate shell completion script for the specified shell. - To enable tab completion, run one of the following: - For bash (add to ~/.bashrc): - eval "$(_STACKQL_DEPLOY_COMPLETE=bash_source stackql-deploy)" - For zsh (add to ~/.zshrc): - eval "$(_STACKQL_DEPLOY_COMPLETE=zsh_source stackql-deploy)" - For fish (add to ~/.config/fish/config.fish): - eval (env _STACKQL_DEPLOY_COMPLETE=fish_source stackql-deploy) - For PowerShell (add to $PROFILE): - Invoke-Expression (& stackql-deploy completion powershell) - """ - shell_lower = shell.lower() - - if shell_lower == 'bash': - click.echo('eval "$(_STACKQL_DEPLOY_COMPLETE=bash_source stackql-deploy)"') - elif shell_lower == 'zsh': - click.echo('eval "$(_STACKQL_DEPLOY_COMPLETE=zsh_source stackql-deploy)"') - elif shell_lower == 'fish': - click.echo('eval (env _STACKQL_DEPLOY_COMPLETE=fish_source stackql-deploy)') - elif shell_lower == 'powershell': - click.echo('Register-ArgumentCompleter -Native -CommandName stackql-deploy -ScriptBlock {') - click.echo(' param($wordToComplete, $commandAst, $cursorPosition)') - click.echo(' $env:_STACKQL_DEPLOY_COMPLETE = "complete"') - click.echo(' $env:COMP_WORDS = $commandAst.ToString()') - click.echo(' $env:COMP_CWORD = $cursorPosition') - click.echo(' stackql-deploy | ForEach-Object {') - click.echo(' [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)') - click.echo(' }') - click.echo('}') - - # # init command # @@ -491,6 +460,145 @@ def init(stack_name, provider): create_project_structure(stack_name, provider=provider) click.echo(f"project {stack_name} initialized successfully.") +# +# completion command +# + +@cli.command("completion") +@click.argument( + "shell", + type=click.Choice(["bash", "zsh", "fish", "powershell"], case_sensitive=False), + required=False, +) +@click.option("--install", is_flag=True, help="Install completion to shell profile") +def completion(shell, install): + """ + Shell tab completion for stackql-deploy. + + Examples: + eval "$(stackql-deploy completion bash)" # activate now + stackql-deploy completion bash --install # install permanently + stackql-deploy completion # auto-detect shell + """ + from pathlib import Path + + # Auto-detect shell if not provided + if not shell: + shell = os.environ.get("SHELL", "").split("/")[-1] or "bash" + shell = shell.lower() + + # Map shells to completion script files + completion_scripts = { + "bash": "stackql-deploy-completion.bash", + "zsh": "stackql-deploy-completion.zsh", + "fish": "stackql-deploy-completion.fish", + "powershell": "stackql-deploy-completion.ps1" + } + + script_name = completion_scripts.get(shell) + if not script_name: + click.echo(f"❌ Shell '{shell}' not supported. Supported: bash, zsh, fish, powershell", err=True) + sys.exit(1) + + # Find the completion script + script_path = _find_completion_script(script_name) + if not script_path: + click.echo(f"❌ Completion script not found: {script_name}", err=True) + sys.exit(1) + + # Output script for eval/source (default behavior) + if not install: + with open(script_path, 'r') as f: + click.echo(f.read()) + return + + # Install to shell profile + _install_completion_for_shell(shell, script_path) + +def _find_completion_script(script_name): + """Find completion script in development or installed locations.""" + from pathlib import Path + + # Development mode: relative to project root + cli_file = Path(__file__).resolve() + project_root = cli_file.parent.parent + dev_path = project_root / "shell_completions" / script_name + + if dev_path.exists(): + logger.debug(f"Found completion script: {dev_path}") + return dev_path + + # Installed mode: check common install locations + for prefix in [sys.prefix, sys.base_prefix, '/usr', '/usr/local']: + installed_path = Path(prefix) / "share" / "stackql-deploy" / "completions" / script_name + if installed_path.exists(): + logger.debug(f"Found completion script: {installed_path}") + return installed_path + + logger.error(f"Completion script {script_name} not found") + return None + +def _install_completion_for_shell(shell, script_path): + """Install completion to shell profile.""" + from pathlib import Path + + profiles = { + "bash": Path.home() / ".bashrc", + "zsh": Path.home() / ".zshrc", + "fish": Path.home() / ".config/fish/config.fish", + "powershell": Path.home() / "Documents/PowerShell/Microsoft.PowerShell_profile.ps1" + } + + eval_commands = { + "bash": 'eval "$(stackql-deploy completion bash)"', + "zsh": 'eval "$(stackql-deploy completion zsh)"', + "fish": 'stackql-deploy completion fish | source', + "powershell": '. (stackql-deploy completion powershell)' + } + + profile_path = profiles.get(shell) + eval_cmd = eval_commands.get(shell) + + if not profile_path: + click.echo(f"❌ Unknown profile for {shell}", err=True) + return + + # Ensure profile directory and file exist + profile_path.parent.mkdir(parents=True, exist_ok=True) + if not profile_path.exists(): + profile_path.touch() + + # Check if already installed + try: + content = profile_path.read_text() + if "stackql-deploy completion" in content: + click.echo(f"✅ Completion already installed in {profile_path}") + _show_activation_instructions(shell) + return + except Exception as e: + click.echo(f"❌ Error reading profile: {e}", err=True) + return + + # Append completion line + try: + with open(profile_path, "a") as f: + f.write(f"\n# stackql-deploy completion\n{eval_cmd}\n") + click.echo(f"✅ Completion installed to {profile_path}") + _show_activation_instructions(shell) + except Exception as e: + click.echo(f"❌ Error installing completion: {e}", err=True) + +def _show_activation_instructions(shell): + """Show shell-specific activation instructions.""" + instructions = { + "bash": 'source ~/.bashrc', + "zsh": 'source ~/.zshrc', + "fish": 'source ~/.config/fish/config.fish', + "powershell": '. $PROFILE' + } + + click.echo(f"🚀 Activate now: {instructions.get(shell, 'restart your shell')}") + click.echo("✨ Or restart your terminal") cli.add_command(build) cli.add_command(test) From 9cb488de4fc82bfcbada919f319d0ccd9f4b09a0 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Mon, 13 Oct 2025 08:22:10 +1100 Subject: [PATCH 2/2] fixed linting issues --- setup.py | 6 +++--- stackql_deploy/cli.py | 42 ++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index 553d631..76ac07e 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ 'inc/contributors.csv' # Fixed path for contributors ], }, - + # Install shell completion scripts to system share directory data_files=[ ('share/stackql-deploy/completions', [ @@ -35,7 +35,7 @@ 'shell_completions/stackql-deploy-completion.ps1', ]) ], - + include_package_data=True, install_requires=[ 'click', @@ -61,4 +61,4 @@ 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', ] -) \ No newline at end of file +) diff --git a/stackql_deploy/cli.py b/stackql_deploy/cli.py index 1cabecd..60b3109 100644 --- a/stackql_deploy/cli.py +++ b/stackql_deploy/cli.py @@ -169,7 +169,7 @@ def build(ctx, stack_dir, stack_env, log_level, env_file, """Create or update resources.""" from .cmd.build import StackQLProvisioner - + stackql, env_vars = setup_command_context( ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir, 'build' @@ -236,7 +236,7 @@ def teardown(ctx, stack_dir, stack_env, log_level, env_file, def test(ctx, stack_dir, stack_env, log_level, env_file, env, dry_run, show_queries, on_failure, custom_registry, download_dir): """Run test queries for the stack.""" - + from .cmd.test import StackQLTestRunner stackql, env_vars = setup_command_context( @@ -474,19 +474,17 @@ def init(stack_name, provider): def completion(shell, install): """ Shell tab completion for stackql-deploy. - Examples: eval "$(stackql-deploy completion bash)" # activate now stackql-deploy completion bash --install # install permanently stackql-deploy completion # auto-detect shell """ - from pathlib import Path - + # Auto-detect shell if not provided if not shell: shell = os.environ.get("SHELL", "").split("/")[-1] or "bash" shell = shell.lower() - + # Map shells to completion script files completion_scripts = { "bash": "stackql-deploy-completion.bash", @@ -494,80 +492,80 @@ def completion(shell, install): "fish": "stackql-deploy-completion.fish", "powershell": "stackql-deploy-completion.ps1" } - + script_name = completion_scripts.get(shell) if not script_name: click.echo(f"❌ Shell '{shell}' not supported. Supported: bash, zsh, fish, powershell", err=True) sys.exit(1) - + # Find the completion script script_path = _find_completion_script(script_name) if not script_path: click.echo(f"❌ Completion script not found: {script_name}", err=True) sys.exit(1) - + # Output script for eval/source (default behavior) if not install: with open(script_path, 'r') as f: click.echo(f.read()) return - + # Install to shell profile _install_completion_for_shell(shell, script_path) def _find_completion_script(script_name): """Find completion script in development or installed locations.""" from pathlib import Path - + # Development mode: relative to project root cli_file = Path(__file__).resolve() project_root = cli_file.parent.parent dev_path = project_root / "shell_completions" / script_name - + if dev_path.exists(): logger.debug(f"Found completion script: {dev_path}") return dev_path - + # Installed mode: check common install locations for prefix in [sys.prefix, sys.base_prefix, '/usr', '/usr/local']: installed_path = Path(prefix) / "share" / "stackql-deploy" / "completions" / script_name if installed_path.exists(): logger.debug(f"Found completion script: {installed_path}") return installed_path - + logger.error(f"Completion script {script_name} not found") return None def _install_completion_for_shell(shell, script_path): """Install completion to shell profile.""" from pathlib import Path - + profiles = { "bash": Path.home() / ".bashrc", "zsh": Path.home() / ".zshrc", "fish": Path.home() / ".config/fish/config.fish", "powershell": Path.home() / "Documents/PowerShell/Microsoft.PowerShell_profile.ps1" } - + eval_commands = { "bash": 'eval "$(stackql-deploy completion bash)"', "zsh": 'eval "$(stackql-deploy completion zsh)"', "fish": 'stackql-deploy completion fish | source', "powershell": '. (stackql-deploy completion powershell)' } - + profile_path = profiles.get(shell) eval_cmd = eval_commands.get(shell) - + if not profile_path: click.echo(f"❌ Unknown profile for {shell}", err=True) return - + # Ensure profile directory and file exist profile_path.parent.mkdir(parents=True, exist_ok=True) if not profile_path.exists(): profile_path.touch() - + # Check if already installed try: content = profile_path.read_text() @@ -578,7 +576,7 @@ def _install_completion_for_shell(shell, script_path): except Exception as e: click.echo(f"❌ Error reading profile: {e}", err=True) return - + # Append completion line try: with open(profile_path, "a") as f: @@ -596,7 +594,7 @@ def _show_activation_instructions(shell): "fish": 'source ~/.config/fish/config.fish', "powershell": '. $PROFILE' } - + click.echo(f"🚀 Activate now: {instructions.get(shell, 'restart your shell')}") click.echo("✨ Or restart your terminal")