diff --git a/docs/1-essentials/04-console-commands.md b/docs/1-essentials/04-console-commands.md
index 7ecf4c7b5e..c6623f309f 100644
--- a/docs/1-essentials/04-console-commands.md
+++ b/docs/1-essentials/04-console-commands.md
@@ -238,6 +238,41 @@ Tempest console comes with a range of interactive components that can be used to
Interactive components are only supported on Mac and Linux. On Windows, Tempest will fall back to non-interactive versions of these components.
:::
+## Shell completion
+
+Tempest provides shell completion for Zsh and Bash. This allows you to press `Tab` to autocomplete command names and options.
+
+### Installing completions
+
+Run the install command and follow the prompts:
+
+```console
+./tempest completion:install
+```
+
+The installer will detect your current shell, copy the completion script to the appropriate location, and provide instructions for enabling it.
+
+For Zsh, you'll need to ensure the completions directory is in your `fpath` and reload completions:
+
+```zsh
+# Add to ~/.zshrc
+fpath=(~/.zsh/completions $fpath)
+autoload -Uz compinit && compinit
+```
+
+For Bash, source the completion file in your `~/.bashrc`:
+
+```bash
+source ~/.bash_completion.d/tempest.bash
+```
+
+### Additional commands
+
+You may also use these related commands:
+
+- `completion:show` — Output the completion script to stdout (useful for custom installation)
+- `completion:uninstall` — Remove the installed completion script
+
## Middleware
Console middleware can be applied globally or on a per-command basis. Global console middleware will be discovered and applied automatically, by priority order.
diff --git a/packages/console/src/Actions/ResolveShell.php b/packages/console/src/Actions/ResolveShell.php
new file mode 100644
index 0000000000..89d45e4f3d
--- /dev/null
+++ b/packages/console/src/Actions/ResolveShell.php
@@ -0,0 +1,31 @@
+console->supportsPrompting()) {
+ /** @var Shell */
+ return $this->console->ask(
+ question: $question,
+ options: Shell::class,
+ default: $detected,
+ );
+ }
+
+ return $detected;
+ }
+}
diff --git a/packages/console/src/Commands/CompletionInstallCommand.php b/packages/console/src/Commands/CompletionInstallCommand.php
new file mode 100644
index 0000000000..73e607c0a5
--- /dev/null
+++ b/packages/console/src/Commands/CompletionInstallCommand.php
@@ -0,0 +1,98 @@
+resolveShell)('Which shell do you want to install completions for?');
+
+ if ($shell === null) {
+ $this->console->error('Could not detect shell. Please specify one using the --shell option. Possible values are: zsh, bash.');
+
+ return ExitCode::ERROR;
+ }
+
+ $sourcePath = $this->getSourcePath($shell);
+ $targetDir = $shell->getCompletionsDirectory();
+ $targetPath = $shell->getInstalledCompletionPath();
+
+ if (! Filesystem\is_file($sourcePath)) {
+ $this->console->error("Completion script not found: {$sourcePath}");
+
+ return ExitCode::ERROR;
+ }
+
+ if (! $force) {
+ $this->console->info("Installing {$shell->value} completions");
+ $this->console->keyValue('Source', $sourcePath);
+ $this->console->keyValue('Target', $targetPath);
+ $this->console->writeln();
+
+ if (! $this->console->confirm('Proceed with installation?', default: true)) {
+ $this->console->warning('Installation cancelled.');
+
+ return ExitCode::CANCELLED;
+ }
+ }
+
+ Filesystem\ensure_directory_exists($targetDir);
+
+ if (Filesystem\is_file($targetPath)) {
+ if (! $force && ! $this->console->confirm('Completion file already exists. Overwrite?', default: false)) {
+ $this->console->warning('Installation cancelled.');
+
+ return ExitCode::CANCELLED;
+ }
+ }
+
+ Filesystem\copy_file($sourcePath, $targetPath, overwrite: true);
+ $this->console->success("Installed completion script to: {$targetPath}");
+
+ $this->console->writeln();
+ $this->console->info('Next steps:');
+ $this->console->instructions($shell->getPostInstallInstructions());
+
+ return ExitCode::SUCCESS;
+ }
+
+ private function getSourcePath(Shell $shell): string
+ {
+ return Path::canonicalize(
+ path(__DIR__, '..', $shell->getSourceFilename())->toString(),
+ );
+ }
+}
diff --git a/packages/console/src/Commands/CompletionShowCommand.php b/packages/console/src/Commands/CompletionShowCommand.php
new file mode 100644
index 0000000000..1afeb32213
--- /dev/null
+++ b/packages/console/src/Commands/CompletionShowCommand.php
@@ -0,0 +1,63 @@
+resolveShell)('Which shell completion script do you want to see?');
+
+ if ($shell === null) {
+ $this->console->error('Could not detect shell. Please specify one using the --shell option. Possible values are: zsh, bash.');
+
+ return ExitCode::ERROR;
+ }
+
+ $sourcePath = $this->getSourcePath($shell);
+
+ if (! Filesystem\is_file($sourcePath)) {
+ $this->console->error("Completion script not found: {$sourcePath}");
+
+ return ExitCode::ERROR;
+ }
+
+ $this->console->writeRaw(Filesystem\read_file($sourcePath));
+
+ return ExitCode::SUCCESS;
+ }
+
+ private function getSourcePath(Shell $shell): string
+ {
+ return Path::canonicalize(
+ path(__DIR__, '..', $shell->getSourceFilename())->toString(),
+ );
+ }
+}
diff --git a/packages/console/src/Commands/CompletionUninstallCommand.php b/packages/console/src/Commands/CompletionUninstallCommand.php
new file mode 100644
index 0000000000..2a5cbdca5d
--- /dev/null
+++ b/packages/console/src/Commands/CompletionUninstallCommand.php
@@ -0,0 +1,76 @@
+resolveShell)('Which shell completions do you want to uninstall?');
+
+ if ($shell === null) {
+ $this->console->error('Could not detect shell. Please specify one using the --shell option. Possible values are: zsh, bash.');
+
+ return ExitCode::ERROR;
+ }
+
+ $targetPath = $shell->getInstalledCompletionPath();
+
+ if (! Filesystem\is_file($targetPath)) {
+ $this->console->warning("Completion file not found: {$targetPath}");
+ $this->console->info('Nothing to uninstall.');
+
+ return ExitCode::SUCCESS;
+ }
+
+ if (! $force) {
+ $this->console->info("Uninstalling {$shell->value} completions");
+ $this->console->keyValue('File', $targetPath);
+ $this->console->writeln();
+
+ if (! $this->console->confirm('Proceed with uninstallation?', default: true)) {
+ $this->console->warning('Uninstallation cancelled.');
+
+ return ExitCode::CANCELLED;
+ }
+ }
+
+ Filesystem\delete_file($targetPath);
+ $this->console->success("Removed completion script: {$targetPath}");
+
+ $this->console->writeln();
+ $this->console->info('Remember to remove any related lines from your shell configuration:');
+ $this->console->keyValue('Config file', $shell->getRcFile());
+
+ return ExitCode::SUCCESS;
+ }
+}
diff --git a/packages/console/src/Enums/Shell.php b/packages/console/src/Enums/Shell.php
new file mode 100644
index 0000000000..ddf4ae28df
--- /dev/null
+++ b/packages/console/src/Enums/Shell.php
@@ -0,0 +1,94 @@
+ self::ZSH,
+ str_contains($shell, 'bash') => self::BASH,
+ default => null,
+ };
+ }
+
+ public function getCompletionsDirectory(): string
+ {
+ $home = $_SERVER['HOME'] ?? getenv('HOME') ?: '';
+
+ return match ($this) {
+ self::ZSH => $home . '/.zsh/completions',
+ self::BASH => $home . '/.bash_completion.d',
+ };
+ }
+
+ public function getCompletionFilename(): string
+ {
+ return match ($this) {
+ self::ZSH => '_tempest',
+ self::BASH => 'tempest.bash',
+ };
+ }
+
+ public function getInstalledCompletionPath(): string
+ {
+ return $this->getCompletionsDirectory() . '/' . $this->getCompletionFilename();
+ }
+
+ public function getSourceFilename(): string
+ {
+ return match ($this) {
+ self::ZSH => 'completion.zsh',
+ self::BASH => 'completion.bash',
+ };
+ }
+
+ public function getRcFile(): string
+ {
+ $home = $_SERVER['HOME'] ?? getenv('HOME') ?: '';
+
+ return match ($this) {
+ self::ZSH => $home . '/.zshrc',
+ self::BASH => $home . '/.bashrc',
+ };
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getPostInstallInstructions(): array
+ {
+ return match ($this) {
+ self::ZSH => [
+ 'Add the completions directory to your fpath in ~/.zshrc:',
+ '',
+ ' fpath=(~/.zsh/completions $fpath)',
+ '',
+ 'Then reload completions:',
+ '',
+ ' autoload -Uz compinit && compinit',
+ '',
+ 'Or restart your terminal.',
+ ],
+ self::BASH => [
+ 'Source the completion file in your ~/.bashrc:',
+ '',
+ ' source ~/.bash_completion.d/tempest.bash',
+ '',
+ 'Or restart your terminal.',
+ ],
+ };
+ }
+}
diff --git a/packages/console/src/complete.zsh b/packages/console/src/complete.zsh
deleted file mode 100644
index 92ff620f5e..0000000000
--- a/packages/console/src/complete.zsh
+++ /dev/null
@@ -1,84 +0,0 @@
-# Forked from https://github.com/symfony/console/tree/7.0/Resources
-
-#compdef tempest
-
-# This file is part of the Symfony package.
-#
-# (c) Fabien Potencier
-#
-# For the full copyright and license information, please view
-# https://symfony.com/doc/current/contributing/code/license.html
-
-#
-# zsh completions for tempest
-#
-# References:
-# - https://github.com/spf13/cobra/blob/master/zsh_completions.go
-# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
-#
-_sf_tempest() {
- local lastParam flagPrefix requestComp out comp
- local -a completions
-
- # The user could have moved the cursor backwards on the command-line.
- # We need to trigger completion from the $CURRENT location, so we need
- # to truncate the command-line ($words) up to the $CURRENT location.
- # (We cannot use $CURSOR as its value does not work when a command is an alias.)
- words=("${=words[1,CURRENT]}") lastParam=${words[-1]}
-
- # For zsh, when completing a flag with an = (e.g., tempest -n=)
- # completions must be prefixed with the flag
- setopt local_options BASH_REMATCH
- if [[ "${lastParam}" =~ '-.*=' ]]; then
- # We are dealing with a flag with an =
- flagPrefix="-P ${BASH_REMATCH}"
- fi
-
- # Prepare the command to obtain completions
- requestComp="${words[0]} ${words[1]} _complete --no-interaction --shell=zsh --current=$((CURRENT-1))" i=""
- for w in ${words[@]}; do
- w=$(printf -- '%b' "$w")
- # remove quotes from typed values
- quote="${w:0:1}"
- if [ "$quote" = \' ]; then
- w="${w%\'}"
- w="${w#\'}"
- elif [ "$quote" = \" ]; then
- w="${w%\"}"
- w="${w#\"}"
- fi
- # empty values are ignored
- if [ ! -z "$w" ]; then
- i="${i}--input=\"${w}\" "
- fi
- done
-
- # Ensure at least 1 input
- if [ "${i}" = "" ]; then
- requestComp="${requestComp} --input=\" \""
- else
- requestComp="${requestComp} ${i}"
- fi
-
- # Use eval to handle any environment variables and such
- out=$(eval ${requestComp} 2>/dev/null)
-
- while IFS='\n' read -r comp; do
- if [ -n "$comp" ]; then
- # If requested, completions are returned with a description.
- # The description is preceded by a TAB character.
- # For zsh's _describe, we need to use a : instead of a TAB.
- # We first need to escape any : as part of the completion itself.
- comp=${comp//:/\\:}
- local tab=$(printf '\t')
- comp=${comp//$tab/:}
- completions+=${comp}
- fi
- done < <(printf "%s\n" "${out[@]}")
-
- # Let inbuilt _describe handle completions
- eval _describe "completions" completions $flagPrefix
- return $?
-}
-
-compdef _sf_tempest tempest
diff --git a/packages/console/src/completion.bash b/packages/console/src/completion.bash
new file mode 100644
index 0000000000..b700571e41
--- /dev/null
+++ b/packages/console/src/completion.bash
@@ -0,0 +1,45 @@
+# Tempest Framework Bash Completion
+# Supports: ./tempest, tempest, php tempest, php vendor/bin/tempest, etc.
+
+_tempest() {
+ local cur tempest_cmd shift_count input_args output IFS
+
+ # Initialize current word (use bash-completion if available)
+ if declare -F _init_completion >/dev/null 2>&1; then
+ _init_completion || return
+ else
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ fi
+
+ # Detect invocation pattern and build command
+ if [[ "${COMP_WORDS[0]}" == "php" ]]; then
+ tempest_cmd="${COMP_WORDS[0]} ${COMP_WORDS[1]}"
+ shift_count=2
+ else
+ tempest_cmd="${COMP_WORDS[0]}"
+ shift_count=1
+ fi
+
+ # Build _complete arguments, skipping "php" prefix if present
+ input_args="--current=$((COMP_CWORD - shift_count + 1))"
+ for ((i = shift_count - 1; i < ${#COMP_WORDS[@]}; i++)); do
+ input_args+=" --input=\"${COMP_WORDS[i]}\""
+ done
+
+ # Execute completion command
+ output=$(eval "$tempest_cmd _complete $input_args" 2>/dev/null) || return 0
+ [[ -z "$output" ]] && return 0
+
+ # Parse and filter completions
+ IFS=$'\n'
+ COMPREPLY=($(compgen -W "$output" -- "$cur"))
+
+ # Suppress trailing space for flags expecting values (bash 4.0+)
+ if [[ ${#COMPREPLY[@]} -eq 1 && "${COMPREPLY[0]}" == *= ]] && type compopt &>/dev/null; then
+ compopt -o nospace
+ fi
+}
+
+complete -F _tempest ./tempest
+complete -F _tempest tempest
+complete -F _tempest php
diff --git a/packages/console/src/completion.zsh b/packages/console/src/completion.zsh
new file mode 100644
index 0000000000..1017276ef0
--- /dev/null
+++ b/packages/console/src/completion.zsh
@@ -0,0 +1,54 @@
+#compdef -p '*/tempest' -p 'tempest' php
+
+# Tempest Framework Zsh Completion
+
+_tempest() {
+ local current="${words[CURRENT]}" tempest_cmd shift_count output
+ local -a args with_suffix without_suffix
+
+ # Detect invocation: "php tempest ..." vs "./tempest ..."
+ if [[ "${words[1]}" == "php" ]]; then
+ tempest_cmd="${words[1]} ${words[2]}"
+ shift_count=2
+ else
+ tempest_cmd="${words[1]}"
+ shift_count=1
+ fi
+
+ # Build completion request arguments
+ # Skip "php" from inputs but keep the tempest binary and args
+ local skip=$((shift_count - 1))
+ args=("--current=$((CURRENT - shift_count))")
+ for word in "${words[@]:$skip}"; do
+ args+=("--input=$word")
+ done
+
+ # Fetch completions from tempest
+ output=$(eval "$tempest_cmd _complete ${args[*]}" 2>/dev/null) || return 0
+ [[ -z "$output" ]] && return 0
+
+ # Parse completions, separating by suffix behavior
+ for line in "${(@f)output}"; do
+ [[ -z "$line" ]] && continue
+ if [[ "$line" == *= ]]; then
+ without_suffix+=("$line")
+ else
+ with_suffix+=("${line//:/\\:}")
+ fi
+ done
+
+ # Add completions: no trailing space for "=" options, use _describe for commands
+ (( ${#without_suffix} )) && compadd -Q -S '' -- "${without_suffix[@]}"
+
+ if (( ${#with_suffix} )); then
+ if [[ "$current" == -* || "${with_suffix[1]}" == -* ]]; then
+ compadd -Q -- "${with_suffix[@]}"
+ else
+ _describe -t commands 'tempest commands' with_suffix
+ fi
+ fi
+}
+
+compdef _tempest -p '*/tempest'
+compdef _tempest -p 'tempest'
+compdef _tempest php
diff --git a/packages/console/tests/Enums/ShellTest.php b/packages/console/tests/Enums/ShellTest.php
new file mode 100644
index 0000000000..dcf569e969
--- /dev/null
+++ b/packages/console/tests/Enums/ShellTest.php
@@ -0,0 +1,108 @@
+assertSame($expected, $result);
+ } finally {
+ if ($originalShell === false) {
+ putenv('SHELL');
+ } else {
+ putenv("SHELL={$originalShell}");
+ }
+ }
+ }
+
+ public static function detectDataProvider(): array
+ {
+ return [
+ 'zsh' => ['/bin/zsh', Shell::ZSH],
+ 'bash' => ['/bin/bash', Shell::BASH],
+ 'usr local zsh' => ['/usr/local/bin/zsh', Shell::ZSH],
+ 'usr local bash' => ['/usr/local/bin/bash', Shell::BASH],
+ 'fish' => ['/bin/fish', null],
+ 'empty' => ['', null],
+ 'not set' => [false, null],
+ ];
+ }
+
+ #[Test]
+ public function getCompletionsDirectory(): void
+ {
+ $home = $_SERVER['HOME'] ?? getenv('HOME') ?: '';
+
+ $this->assertSame($home . '/.zsh/completions', Shell::ZSH->getCompletionsDirectory());
+ $this->assertSame($home . '/.bash_completion.d', Shell::BASH->getCompletionsDirectory());
+ }
+
+ #[Test]
+ public function getCompletionFilename(): void
+ {
+ $this->assertSame('_tempest', Shell::ZSH->getCompletionFilename());
+ $this->assertSame('tempest.bash', Shell::BASH->getCompletionFilename());
+ }
+
+ #[Test]
+ public function getInstalledCompletionPath(): void
+ {
+ $home = $_SERVER['HOME'] ?? getenv('HOME') ?: '';
+
+ $this->assertSame($home . '/.zsh/completions/_tempest', Shell::ZSH->getInstalledCompletionPath());
+ $this->assertSame($home . '/.bash_completion.d/tempest.bash', Shell::BASH->getInstalledCompletionPath());
+ }
+
+ #[Test]
+ public function getSourceFilename(): void
+ {
+ $this->assertSame('completion.zsh', Shell::ZSH->getSourceFilename());
+ $this->assertSame('completion.bash', Shell::BASH->getSourceFilename());
+ }
+
+ #[Test]
+ public function getRcFile(): void
+ {
+ $home = $_SERVER['HOME'] ?? getenv('HOME') ?: '';
+
+ $this->assertSame($home . '/.zshrc', Shell::ZSH->getRcFile());
+ $this->assertSame($home . '/.bashrc', Shell::BASH->getRcFile());
+ }
+
+ #[Test]
+ public function getPostInstallInstructions(): void
+ {
+ $zshInstructions = Shell::ZSH->getPostInstallInstructions();
+ $this->assertIsArray($zshInstructions);
+ $this->assertNotEmpty($zshInstructions);
+ $this->assertStringContainsString('fpath', $zshInstructions[0]);
+
+ $bashInstructions = Shell::BASH->getPostInstallInstructions();
+ $this->assertIsArray($bashInstructions);
+ $this->assertNotEmpty($bashInstructions);
+ $this->assertStringContainsStringIgnoringCase('source', $bashInstructions[0]);
+ }
+}
diff --git a/tests/Integration/Console/Commands/CompletionInstallCommandTest.php b/tests/Integration/Console/Commands/CompletionInstallCommandTest.php
new file mode 100644
index 0000000000..ac5d80ccf0
--- /dev/null
+++ b/tests/Integration/Console/Commands/CompletionInstallCommandTest.php
@@ -0,0 +1,123 @@
+installedFile !== null && Filesystem\is_file($this->installedFile)) {
+ Filesystem\delete_file($this->installedFile);
+ $this->installedFile = null;
+ }
+
+ parent::tearDown();
+ }
+
+ #[Test]
+ public function install_with_explicit_shell_flag(): void
+ {
+ $this->installedFile = Shell::ZSH->getInstalledCompletionPath();
+
+ $this->console
+ ->call('completion:install --shell=zsh --force')
+ ->assertSee('Installed completion script to:')
+ ->assertSee('_tempest')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function install_with_invalid_shell(): void
+ {
+ $this->console
+ ->withoutPrompting()
+ ->call('completion:install --shell=fish')
+ ->assertSee('Invalid argument `fish` for `shell` argument')
+ ->assertError();
+ }
+
+ #[Test]
+ public function install_shows_post_install_instructions_for_zsh(): void
+ {
+ $this->installedFile = Shell::ZSH->getInstalledCompletionPath();
+
+ $this->console
+ ->call('completion:install --shell=zsh --force')
+ ->assertSee('fpath=')
+ ->assertSee('compinit')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function install_shows_post_install_instructions_for_bash(): void
+ {
+ $this->installedFile = Shell::BASH->getInstalledCompletionPath();
+
+ $this->console
+ ->call('completion:install --shell=bash --force')
+ ->assertSee('source')
+ ->assertSee('tempest.bash')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function install_cancelled_when_user_denies_confirmation(): void
+ {
+ $this->console
+ ->call('completion:install --shell=zsh')
+ ->assertSee('Installing zsh completions')
+ ->deny()
+ ->assertSee('Installation cancelled')
+ ->assertCancelled();
+ }
+
+ #[Test]
+ public function install_creates_directory_if_not_exists(): void
+ {
+ $targetDir = Shell::ZSH->getCompletionsDirectory();
+ $dirExisted = Filesystem\is_directory($targetDir);
+
+ $this->installedFile = Shell::ZSH->getInstalledCompletionPath();
+
+ $result = $this->console
+ ->call('completion:install --shell=zsh --force');
+
+ if (! $dirExisted) {
+ $result->assertSee('Created directory:');
+ }
+
+ $result->assertSuccess();
+ }
+
+ #[Test]
+ public function install_asks_for_overwrite_when_file_exists(): void
+ {
+ $targetPath = Shell::ZSH->getInstalledCompletionPath();
+ $targetDir = Shell::ZSH->getCompletionsDirectory();
+
+ Filesystem\create_directory($targetDir);
+ Filesystem\write_file($targetPath, '# existing content');
+
+ $this->installedFile = $targetPath;
+
+ $this->console
+ ->call('completion:install --shell=zsh')
+ ->confirm()
+ ->assertSee('Completion file already exists')
+ ->deny()
+ ->assertSee('Installation cancelled')
+ ->assertCancelled();
+ }
+}
diff --git a/tests/Integration/Console/Commands/CompletionShowCommandTest.php b/tests/Integration/Console/Commands/CompletionShowCommandTest.php
new file mode 100644
index 0000000000..d4c1b32510
--- /dev/null
+++ b/tests/Integration/Console/Commands/CompletionShowCommandTest.php
@@ -0,0 +1,42 @@
+console
+ ->call('completion:show --shell=zsh')
+ ->assertSee('_tempest')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function show_bash_completion_script(): void
+ {
+ $this->console
+ ->call('completion:show --shell=bash')
+ ->assertSee('_tempest')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function show_with_invalid_shell(): void
+ {
+ $this->console
+ ->withoutPrompting()
+ ->call('completion:show --shell=fish')
+ ->assertSee('Invalid argument `fish` for `shell` argument')
+ ->assertError();
+ }
+}
diff --git a/tests/Integration/Console/Commands/CompletionUninstallCommandTest.php b/tests/Integration/Console/Commands/CompletionUninstallCommandTest.php
new file mode 100644
index 0000000000..4f042f4583
--- /dev/null
+++ b/tests/Integration/Console/Commands/CompletionUninstallCommandTest.php
@@ -0,0 +1,98 @@
+getInstalledCompletionPath();
+ $targetDir = Shell::ZSH->getCompletionsDirectory();
+
+ Filesystem\create_directory($targetDir);
+ Filesystem\write_file($targetPath, '# completion script');
+
+ $this->console
+ ->call('completion:uninstall --shell=zsh --force')
+ ->assertSee('Removed completion script:')
+ ->assertSee('_tempest')
+ ->assertSuccess();
+
+ $this->assertFalse(Filesystem\is_file($targetPath));
+ }
+
+ #[Test]
+ public function uninstall_with_invalid_shell(): void
+ {
+ $this->console
+ ->withoutPrompting()
+ ->call('completion:uninstall --shell=fish')
+ ->assertSee('Invalid argument `fish` for `shell` argument')
+ ->assertError();
+ }
+
+ #[Test]
+ public function uninstall_when_file_not_exists(): void
+ {
+ $targetPath = Shell::ZSH->getInstalledCompletionPath();
+
+ if (Filesystem\is_file($targetPath)) {
+ Filesystem\delete_file($targetPath);
+ }
+
+ $this->console
+ ->withoutPrompting()
+ ->call('completion:uninstall --shell=zsh --force')
+ ->assertSee('Completion file not found')
+ ->assertSee('Nothing to uninstall')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function uninstall_shows_config_file_reminder(): void
+ {
+ $targetPath = Shell::BASH->getInstalledCompletionPath();
+ $targetDir = Shell::BASH->getCompletionsDirectory();
+
+ Filesystem\create_directory($targetDir);
+ Filesystem\write_file($targetPath, '# completion script');
+
+ $this->console
+ ->call('completion:uninstall --shell=bash --force')
+ ->assertSee('Remember to remove any related lines')
+ ->assertSee('.bashrc')
+ ->assertSuccess();
+ }
+
+ #[Test]
+ public function uninstall_cancelled_when_user_denies_confirmation(): void
+ {
+ $targetPath = Shell::ZSH->getInstalledCompletionPath();
+ $targetDir = Shell::ZSH->getCompletionsDirectory();
+
+ Filesystem\create_directory($targetDir);
+ Filesystem\write_file($targetPath, '# completion script');
+
+ $this->console
+ ->call('completion:uninstall --shell=zsh')
+ ->assertSee('Uninstalling zsh completions')
+ ->deny()
+ ->assertSee('Uninstallation cancelled')
+ ->assertCancelled();
+
+ $this->assertTrue(Filesystem\is_file($targetPath));
+
+ Filesystem\delete_file($targetPath);
+ }
+}