Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,58 @@ pip install hiverge-cli
source start.sh
```

## Shell Completion

Hive CLI supports shell tab completion for commands, experiment names, sandbox names, and file paths. After installing the CLI, enable completion for your shell:

### Bash

Add the following to your `~/.bashrc`:

```bash
eval "$(register-python-argcomplete hive)"
```

Then reload your shell configuration:

```bash
source ~/.bashrc
```

### Zsh

Add the following to your `~/.zshrc`:

```bash
autoload -U bashcompinit
bashcompinit
eval "$(register-python-argcomplete hive)"
```

Then reload your shell configuration:

```bash
source ~/.zshrc
```

### Verify

Test that completion is working:

```bash
hive delete exp<TAB> # Should list available experiments
hive log <TAB> # Should list available sandbox pods
```

### Features

- Command and subcommand completion for all hive commands
- Dynamic completion of experiment names for `delete` and `show sandboxes --experiment`
- Dynamic completion of sandbox pod names for `log` command
- File path completion for `-f/--config` flags

**Note:** Completion requires access to your Kubernetes cluster to fetch experiment and sandbox names. If the cluster is unavailable, completion will still work for static options but won't show dynamic resources.

## How to run

**Note**: Hive-CLI reads the configuration from a yaml file, by default it will look for the `~/.hive/hive.yaml`. You can also specify a different configuration file using the `-f` option. Refer to the [hive.yaml](./hive.yaml) for examples.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies = [
"rich>=12.5.1",
"kubernetes>=33.1.0",
"portforward>=0.3.0",
"argcomplete>=3.0.0",
]

[dependency-groups]
Expand Down
183 changes: 183 additions & 0 deletions src/hive_cli/completers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""
Argcomplete completer functions for the Hive CLI.

These functions provide dynamic tab completion for commands, fetching
resource names from Kubernetes and providing file path completion.
"""

import os
import signal
from functools import wraps

from argcomplete.completers import FilesCompleter


def safe_completer(func):
"""
Decorator to wrap completer functions with error handling.

Ensures that completers never raise exceptions that would break the CLI.
All errors are silently caught and an empty list is returned.
"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception:
# Silent failure - return empty list on any error
return []
return wrapper


class TimeoutError(Exception):
"""Raised when a completion operation times out."""
pass
Comment on lines +32 to +34

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom TimeoutError class shadows Python's built-in TimeoutError exception (available since Python 3.3). Rename this to avoid conflicts, for example: CompletionTimeoutError.

Copilot uses AI. Check for mistakes.


def timeout_handler(signum, frame):
"""Signal handler for timeout."""
raise TimeoutError("Operation timed out")


@safe_completer
def experiment_completer(prefix, parsed_args, **kwargs):
"""
Complete experiment names by fetching from Kubernetes.

Used for:
- hive delete experiment <name>
- hive show sandboxes --experiment <name>

Args:
prefix: The current prefix being completed
parsed_args: Parsed arguments from argparse
**kwargs: Additional arguments from argcomplete

Returns:
List of experiment names matching the prefix
"""
# Set up 2-second timeout

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states a '2-second timeout' but there's no explanation of why 2 seconds was chosen or how users can adjust this if needed. Consider documenting the rationale for this value.

Copilot uses AI. Check for mistakes.
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(2)
Comment on lines +59 to +61

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout duration (2 seconds) is hardcoded in both experiment_completer and sandbox_completer. Extract this to a module-level constant like COMPLETION_TIMEOUT_SECONDS = 2 to improve maintainability.

Copilot uses AI. Check for mistakes.

try:
# Import here to avoid loading K8s client at module import time
from hive_cli.config import load_config
from hive_cli.platform.k8s import K8sPlatform

# Load config - use default if not specified
config_path = getattr(parsed_args, "config", None)
if not config_path:
config_path = os.path.expandvars("$HOME/.hive/hive.yaml")

config = load_config(config_path)

# Create platform and fetch experiments
platform = K8sPlatform(None, config.token_path)
resp = platform.client.list_namespaced_custom_object(
group="core.hiverge.ai",
version="v1alpha1",
namespace="default",
plural="experiments",
)

# Extract experiment names
experiments = [item["metadata"]["name"] for item in resp.get("items", [])]

# Filter by prefix if provided
if prefix:
experiments = [exp for exp in experiments if exp.startswith(prefix)]

return experiments

except TimeoutError:
# Timeout - return empty list
return []
finally:
# Cancel alarm
signal.alarm(0)


@safe_completer
def sandbox_completer(prefix, parsed_args, **kwargs):
"""
Complete sandbox pod names by fetching from Kubernetes.

Used for:
- hive log <sandbox-name>

If --experiment flag is provided, filters sandboxes by experiment label.

Args:
prefix: The current prefix being completed
parsed_args: Parsed arguments from argparse
**kwargs: Additional arguments from argcomplete

Returns:
List of sandbox pod names matching the prefix
"""
# Set up 2-second timeout

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states a '2-second timeout' but there's no explanation of why 2 seconds was chosen or how users can adjust this if needed. Consider documenting the rationale for this value.

Copilot uses AI. Check for mistakes.
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(2)

try:
# Import here to avoid loading K8s client at module import time
from hive_cli.config import load_config
from hive_cli.platform.k8s import K8sPlatform

# Load config - use default if not specified
config_path = getattr(parsed_args, "config", None)
if not config_path:
config_path = os.path.expandvars("$HOME/.hive/hive.yaml")

config = load_config(config_path)

# Create platform and fetch sandboxes
platform = K8sPlatform(None, config.token_path)

# Build label selector
label_selector = "app=hive-sandbox"

# Filter by experiment if provided
experiment = getattr(parsed_args, "experiment", None)
if experiment:
label_selector += f",hiverge.ai/experiment-name={experiment}"

# Fetch pods
pods = platform.core_client.list_namespaced_pod(
namespace="default",
label_selector=label_selector
)

# Extract sandbox names
sandboxes = [pod.metadata.name for pod in pods.items]

# Filter by prefix if provided
if prefix:
sandboxes = [sb for sb in sandboxes if sb.startswith(prefix)]

return sandboxes

except TimeoutError:
# Timeout - return empty list
return []
finally:
# Cancel alarm
signal.alarm(0)


def config_file_completer(prefix, parsed_args, **kwargs):
"""
Complete config file paths for -f/--config flags.

Delegates to argcomplete's built-in FilesCompleter for file path completion.

Args:
prefix: The current prefix being completed
parsed_args: Parsed arguments from argparse
**kwargs: Additional arguments from argcomplete

Returns:
List of file paths matching the prefix
"""
return FilesCompleter()(prefix, **kwargs)
23 changes: 13 additions & 10 deletions src/hive_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import subprocess
from importlib.metadata import PackageNotFoundError, version

import argcomplete
import portforward
from rich.console import Console
from rich.text import Text

from hive_cli.completers import config_file_completer, experiment_completer, sandbox_completer
from hive_cli.config import HiveConfig, load_config
from hive_cli.platform.k8s import K8sPlatform
from hive_cli.utils import event
Expand Down Expand Up @@ -164,7 +166,7 @@ def main():
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, default to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_create_exp.set_defaults(func=create_experiment_cli)

# TODO:
Expand All @@ -190,13 +192,13 @@ def main():
parser_delete_exp = delete_subparsers.add_parser(
"experiment", aliases=["exp"], help="Delete an experiment"
)
parser_delete_exp.add_argument("name", help="Name of the experiment")
parser_delete_exp.add_argument("name", help="Name of the experiment").completer = experiment_completer
parser_delete_exp.add_argument(
"-f",
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, default to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_delete_exp.set_defaults(func=delete_experiment_cli)

# show command
Expand All @@ -212,7 +214,7 @@ def main():
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, default to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_show_exp.set_defaults(func=show_experiment_cli)

## show sandboxes
Expand All @@ -224,12 +226,12 @@ def main():
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, default to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_show_sandbox.add_argument(
"-exp",
"--experiment",
help="Name of the experiment running sandboxes",
)
).completer = experiment_completer
parser_show_sandbox.set_defaults(func=show_sandbox_cli)

# edit command
Expand All @@ -243,7 +245,7 @@ def main():
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, defaults to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_edit_config.set_defaults(func=edit_cli)

# dashboard command
Expand All @@ -259,7 +261,7 @@ def main():
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, default to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_dashboard.set_defaults(func=show_dashboard_cli)

# version command
Expand All @@ -268,13 +270,13 @@ def main():

# log command
parser_log = subparsers.add_parser("log", help="Show Sandbox logs")
parser_log.add_argument("sandbox", help="Name of the sandbox to fetch logs for")
parser_log.add_argument("sandbox", help="Name of the sandbox to fetch logs for").completer = sandbox_completer
parser_log.add_argument(
"-f",
"--config",
default=os.path.expandvars("$HOME/.hive/hive.yaml"),
help="Path to the config file, default to ~/.hive/hive.yaml",
)
).completer = config_file_completer
parser_log.add_argument(
"-t",
"--tail",
Expand All @@ -284,6 +286,7 @@ def main():
)
parser_log.set_defaults(func=display_sandbox_logs_cli)

argcomplete.autocomplete(parser)
args = parser.parse_args()
if hasattr(args, "func"):
args.func(args)
Expand Down
Loading
Loading