Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
82d68e0
chore: add completion-helper ownership to CODEOWNERS
xHeaven Feb 13, 2026
3633d47
feat(console): add CompletionRuntime class
xHeaven Feb 13, 2026
ce5c69c
feat(console): add completion metadata and helper actions
xHeaven Feb 13, 2026
911e09c
feat(console): rewrite shell completion scripts
xHeaven Feb 13, 2026
ef128d9
refactor(console): update Shell enum for native completion
xHeaven Feb 13, 2026
edea7d0
feat(console): update completion commands for native helper
xHeaven Feb 13, 2026
1554e02
feat(console): add Rust completion helper crate and build script
xHeaven Feb 13, 2026
d7aafae
ci: add workflow to build completion binaries on release
xHeaven Feb 13, 2026
7260705
refactor(console): download completion binary from GitHub releases
xHeaven Feb 13, 2026
15e1973
feat(console): add completion:update-bin command
xHeaven Feb 13, 2026
f47a2de
docs(console): update shell completion documentation
xHeaven Feb 13, 2026
9816dd9
refactor(console): drop macOS x86_64 completion support
xHeaven Feb 13, 2026
ac5b72b
ci: drop x86_64 macOS from completion binary builds
xHeaven Feb 13, 2026
1847f8a
docs(console): note Apple Silicon requirement for macOS
xHeaven Feb 13, 2026
c618fcf
fix(console): update ShellTest for unified completion directory
xHeaven Feb 13, 2026
a49877f
style(console): fix formatting
xHeaven Feb 13, 2026
ab82da0
Merge branch '3.x' into completion
xHeaven Feb 13, 2026
dd9018d
refactor(console): skip tests on windows
xHeaven Feb 13, 2026
23fb7ef
refactor(console): drop static keywords
xHeaven Feb 16, 2026
bb1bcac
fix(console): force binary download in update-bin command
xHeaven Feb 16, 2026
c9569e6
refactor(console): make CompletionRuntime an injectable singleton
xHeaven Feb 16, 2026
9f59692
feat(console): add PHP completion helper
xHeaven Feb 16, 2026
65fecbb
refactor(console): remove Rust completion helper
xHeaven Feb 16, 2026
e44e06a
refactor(console): simplify completion commands and runtime
xHeaven Feb 16, 2026
5f811ec
test(console): update tests for PHP completion helper
xHeaven Feb 16, 2026
fad862c
style(console): add spaces
xHeaven Feb 16, 2026
679f5cb
chore(console): update gitignore
xHeaven Feb 16, 2026
187b92d
docs: update completion documentation
xHeaven Feb 16, 2026
04bbf5d
test(console): skip windows
xHeaven Feb 16, 2026
85c7b89
Merge branch '3.x' into completion
xHeaven Feb 17, 2026
401f3e5
fix(console): static analysis issues
xHeaven Feb 17, 2026
22000b9
chore: add codeowner entry for completion
xHeaven Feb 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
/packages/clock/ @aidan-casey
/packages/command-bus/ @brendt @aidan-casey
/packages/console/ @brendt
/packages/console/src/Completion @xHeaven
/packages/container/ @brendt
/packages/core/ @brendt
/packages/cryptography/ @innocenzi
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@
}
},
"bin": [
"packages/console/bin/tempest"
"packages/console/bin/tempest",
"packages/console/bin/tempest-complete"
],
"config": {
"allow-plugins": {
Expand Down
42 changes: 27 additions & 15 deletions docs/1-essentials/04-console-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ Interactive components are only supported on Mac and Linux. On Windows, Tempest

## Shell completion

Tempest provides shell completion for Zsh and Bash. This allows you to press `Tab` to autocomplete command names and options.
Tempest provides shell completion for Zsh and Bash on Linux and macOS. This allows you to press `Tab` to autocomplete command names and options. On Windows, use WSL.

Completion relies on two things: a **completion script** sourced by your shell, and a **helper executable** (`vendor/bin/tempest-complete`) that performs the actual matching.

### Installing completions

Expand All @@ -250,28 +252,38 @@ Run the install command and follow the prompts:
<dim>./</dim>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:
This will:

```zsh
# Add to ~/.zshrc
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit
```
1. Detect your shell (or use `--shell=zsh` / `--shell=bash`).
2. Generate completion metadata (`commands.json`) for all registered commands.
3. Install the completion script to the appropriate location.

For Bash, source the completion file in your `~/.bashrc`:
After installation, add the following line to your shell configuration file and restart your terminal:

```bash
source ~/.bash_completion.d/tempest.bash
# Zsh: add to ~/.zshrc
source ~/.tempest/completion/tempest.zsh

# Bash: add to ~/.bashrc
source ~/.tempest/completion/tempest.bash
```

### Additional commands
### Keeping completions up to date

After adding or removing commands, regenerate the metadata:

```console
<dim>./</dim>tempest completion:generate
```

You may also use these related commands:
### Available commands

- `completion:show` — Output the completion script to stdout (useful for custom installation)
- `completion:uninstall` — Remove the installed completion script
| Command | Description |
|------------------------|--------------------------------------------------------------------------|
| `completion:install` | Install the completion script and generate metadata. |
| `completion:generate` | Regenerate the completion metadata JSON. |
| `completion:show` | Output the completion script to stdout (useful for custom installation). |
| `completion:uninstall` | Remove the installed completion script. |

## Middleware

Expand Down
2 changes: 1 addition & 1 deletion packages/console/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
console.log
debug.log
tempest.log
tempest.log
30 changes: 30 additions & 0 deletions packages/console/bin/tempest-complete
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

use Tempest\Console\Completion\CompletionApplication;

$metadataPath = $_SERVER['argv'][1] ?? null;

if (! $metadataPath) {
return;
}

require_once dirname($metadataPath, 3) . '/vendor/autoload.php';

if (! class_exists(CompletionApplication::class)) {
return;
}

if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
return;
}

$script = $_SERVER['SCRIPT_FILENAME'] ?? null;

if (! is_string($script) || realpath($script) !== __FILE__) {
return;
}

new CompletionApplication()->run(array_slice($_SERVER['argv'] ?? [], 1));
3 changes: 2 additions & 1 deletion packages/console/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
}
},
"bin": [
"bin/tempest"
"bin/tempest",
"bin/tempest-complete"
]
}
103 changes: 103 additions & 0 deletions packages/console/src/Actions/BuildCompletionMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Actions;

use BackedEnum;
use Tempest\Console\ConsoleConfig;
use Tempest\Console\Input\ConsoleArgumentDefinition;

use function Tempest\Support\str;

final readonly class BuildCompletionMetadata
{
public function __construct(
private ConsoleConfig $consoleConfig,
) {}

public function __invoke(): array
{
$commands = [];

foreach ($this->consoleConfig->commands as $name => $command) {
$flags = array_map(
fn (ConsoleArgumentDefinition $definition): array => [
'name' => $definition->name,
'flag' => $this->buildFlagNotation($definition),
'aliases' => $this->buildFlagAliases($definition),
'description' => $definition->description,
'value_options' => $this->buildValueOptions($definition),
'repeatable' => $definition->type === 'array' || $definition->isVariadic,
'requires_value' => $definition->type !== 'bool',
],
$command->getArgumentDefinitions(),
);

usort($flags, static fn (array $a, array $b): int => $a['flag'] <=> $b['flag']);

$commands[$name] = [
'hidden' => $command->hidden,
'description' => $command->description,
'flags' => $flags,
];
}

ksort($commands);

return [
'version' => 1,
'commands' => $commands,
];
}

private function buildFlagNotation(ConsoleArgumentDefinition $definition): string
{
$flag = "--{$definition->name}";

if ($definition->type !== 'bool') {
$flag .= '=';
}

return $flag;
}

private function buildFlagAliases(ConsoleArgumentDefinition $definition): array
{
$aliases = array_values(array_filter(array_map(static function (string $alias): ?string {
$normalized = ltrim(str($alias)->trim()->kebab()->toString(), '-');

if ($normalized === '') {
return null;
}

return match (strlen($normalized)) {
1 => "-{$normalized}",
default => "--{$normalized}",
};
}, $definition->aliases)));

sort($aliases);

return $aliases;
}

private function buildValueOptions(ConsoleArgumentDefinition $definition): array
{
if (! $definition->isBackedEnum()) {
return [];
}

/** @var class-string<BackedEnum> $type */
$type = $definition->type;

$options = array_map(
static fn (BackedEnum $case): string => (string) $case->value,
$type::cases(),
);

sort($options);

return $options;
}
}
48 changes: 48 additions & 0 deletions packages/console/src/Commands/CompletionGenerateCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Commands;

use Tempest\Console\Actions\BuildCompletionMetadata;
use Tempest\Console\CompletionRuntime;
use Tempest\Console\Console;
use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\ExitCode;
use Tempest\Support\Filesystem;

final readonly class CompletionGenerateCommand
{
public function __construct(
private Console $console,
private CompletionRuntime $completionRuntime,
private BuildCompletionMetadata $buildCompletionMetadata,
) {}

#[ConsoleCommand(
name: 'completion:generate',
description: 'Generate shell completion metadata as JSON',
)]
public function __invoke(
#[ConsoleArgument(
description: 'Optional output path for the completion metadata JSON',
aliases: ['-p'],
)]
?string $path = null,
): ExitCode {
if (! $this->completionRuntime->isSupportedPlatform()) {
$this->console->error($this->completionRuntime->getUnsupportedPlatformMessage());

return ExitCode::ERROR;
}

$path ??= $this->completionRuntime->getMetadataPath();

Filesystem\write_json($path, ($this->buildCompletionMetadata)(), pretty: false);

$this->console->success("Wrote completion metadata to: {$path}");

return ExitCode::SUCCESS;
}
}
39 changes: 24 additions & 15 deletions packages/console/src/Commands/CompletionInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
namespace Tempest\Console\Commands;

use Symfony\Component\Filesystem\Path;
use Tempest\Console\Actions\BuildCompletionMetadata;
use Tempest\Console\Actions\ResolveShell;
use Tempest\Console\CompletionRuntime;
use Tempest\Console\Console;
use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Enums\Shell;
use Tempest\Console\ExitCode;
use Tempest\Console\Middleware\ForceMiddleware;
use Tempest\Support\Filesystem;

use function Tempest\Support\path;
Expand All @@ -19,25 +22,29 @@
{
public function __construct(
private Console $console,
private CompletionRuntime $completionRuntime,
private ResolveShell $resolveShell,
private BuildCompletionMetadata $buildCompletionMetadata,
) {}

#[ConsoleCommand(
name: 'completion:install',
description: 'Install shell completion for Tempest',
middleware: [ForceMiddleware::class],
)]
public function __invoke(
#[ConsoleArgument(
description: 'The shell to install completions for (zsh, bash)',
aliases: ['-s'],
)]
?Shell $shell = null,
#[ConsoleArgument(
description: 'Skip confirmation prompts',
aliases: ['-f'],
)]
bool $force = false,
): ExitCode {
if (! $this->completionRuntime->isSupportedPlatform()) {
$this->console->error($this->completionRuntime->getUnsupportedPlatformMessage());

return ExitCode::ERROR;
}

$shell ??= ($this->resolveShell)('Which shell do you want to install completions for?');

if ($shell === null) {
Expand All @@ -47,16 +54,16 @@ public function __invoke(
}

$sourcePath = $this->getSourcePath($shell);
$targetDir = $shell->getCompletionsDirectory();
$targetPath = $shell->getInstalledCompletionPath();
$targetDir = $this->completionRuntime->getInstallationDirectory();
$targetPath = $this->completionRuntime->getInstalledCompletionPath($shell);

if (! Filesystem\is_file($sourcePath)) {
$this->console->error("Completion script not found: {$sourcePath}");

return ExitCode::ERROR;
}

if (! $force) {
if (! $this->console->isForced) {
$this->console->info("Installing {$shell->value} completions");
$this->console->keyValue('Source', $sourcePath);
$this->console->keyValue('Target', $targetPath);
Expand All @@ -69,22 +76,24 @@ public function __invoke(
}
}

Filesystem\write_json($this->completionRuntime->getMetadataPath(), ($this->buildCompletionMetadata)(), pretty: false);

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.');
if (Filesystem\is_file($targetPath) && ! $this->console->confirm('Completion file already exists. Overwrite?', default: true)) {
$this->console->warning('Installation cancelled.');

return ExitCode::CANCELLED;
}
return ExitCode::CANCELLED;
}

Filesystem\copy_file($sourcePath, $targetPath, overwrite: true);
$script = Filesystem\read_file($sourcePath);
Filesystem\write_file($targetPath, $script);

$this->console->success("Installed completion script to: {$targetPath}");

$this->console->writeln();
$this->console->info('Next steps:');
$this->console->instructions($shell->getPostInstallInstructions());
$this->console->instructions($this->completionRuntime->getPostInstallInstructions($shell));

return ExitCode::SUCCESS;
}
Expand Down
Loading