Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rich import traceback

from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group


def _opt_log_level_callback(ctx, param, value):
Expand Down Expand Up @@ -34,7 +35,7 @@ def close(self):
v.close()

def cli(self):
@click.group
@driver_click_group(self)
@click.option(
"--log-level",
"log_level",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from jumpstarter_driver_flashers.bundle import FlasherBundleManifestV1Alpha1

from jumpstarter.client.decorators import driver_click_group
from jumpstarter.common.exceptions import ArgumentError

debug_console_option = click.option("--console-debug", is_flag=True, help="Enable console debug mode")
Expand Down Expand Up @@ -750,7 +751,7 @@ def _validate_bearer_token(self, token: str | None) -> str | None:
return token

def cli(self):
@click.group
@driver_click_group(self)
def base():
"""Software-defined flasher interface"""
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from jumpstarter_driver_power.client import PowerClient

from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group


class PinState(Enum):
Expand Down Expand Up @@ -34,7 +35,7 @@ def read(self):
return PinState(int(self.call("read_pin")))

def cli(self):
@click.group()
@driver_click_group(self)
def gpio():
"""GPIO power control commands."""
pass
Expand Down Expand Up @@ -79,7 +80,7 @@ def read(self):
return PinState(int(self.call("read_pin")))

def cli(self):
@click.group()
@driver_click_group(self)
def gpio():
"""GPIO input commands."""
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .driver import DbusNetwork
from jumpstarter.client import DriverClient
from jumpstarter.client.core import DriverMethodNotImplemented
from jumpstarter.client.decorators import driver_click_group


class NetworkClient(DriverClient):
Expand All @@ -20,7 +21,7 @@ def address(self):
return self.call("address")

def cli(self):
@click.group
@driver_click_group(self)
def base():
"""Generic Network Connection"""
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .adapter import OpendalAdapter
from .common import Capability, HashAlgo, Metadata, Mode, PathBuf, PresignedRequest
from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group
from jumpstarter.common.exceptions import ArgumentError
from jumpstarter.streams.encoding import Compression

Expand Down Expand Up @@ -413,9 +414,10 @@ def cli(self): # noqa: C901
arg_dst = click.argument("dst", type=click.Path())
opt_expire_second = click.option("--expire-second", type=int, required=True)

@click.group
@driver_click_group(self)
def base():
"""Opendal Storage"""
pass

@base.command
@arg_path
Expand Down Expand Up @@ -548,7 +550,7 @@ def dump(
...

def cli(self):
@click.group
@driver_click_group(self)
def base():
"""Generic flasher interface"""
pass
Expand Down Expand Up @@ -716,7 +718,10 @@ def read_local_file(self, filepath):

def cli(self, base=None):
if base is None:
base = click.group(lambda: None)
@driver_click_group(self)
def base():
"""Storage operations"""
pass

@base.command()
def host():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .common import PowerReading
from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group


class PowerClient(DriverClient):
Expand Down Expand Up @@ -35,7 +36,7 @@ def read(self) -> Generator[PowerReading, None, None]:
yield PowerReading.model_validate(v, strict=True)

def cli(self):
@click.group
@driver_click_group(self)
def base():
"""Generic power"""
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from opendal import Operator

from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group
from jumpstarter.common.exceptions import ArgumentError


Expand Down Expand Up @@ -60,7 +61,7 @@ def read(self, width: int, address: int, words: int) -> list[int]:
return [int(data, 16) for data in data_strs]

def cli(self): # noqa: C901
@click.group
@driver_click_group(self)
def base():
"""probe-rs client"""
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .console import Console
from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group


class PySerialClient(DriverClient):
Expand Down Expand Up @@ -36,7 +37,7 @@ def pexpect(self):
yield adapter

def cli(self):
@click.group
@driver_click_group(self)
def base():
"""Serial port client"""
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from pathlib import Path
from typing import Dict, Optional

import click
from jumpstarter_driver_composite.client import CompositeClient
from jumpstarter_driver_opendal.client import FlasherClient, operator_for_path
from jumpstarter_driver_power.client import PowerClient
from opendal import Operator

from jumpstarter.client.decorators import driver_click_group

PROMPT = "CMD >> "


Expand Down Expand Up @@ -102,8 +103,9 @@ def flash(
def cli(self):
generic_cli = FlasherClient.cli(self)

@click.group()
@driver_click_group(self)
def storage():
"""RideSX storage operations"""
pass

for name, cmd in generic_cli.commands.items():
Expand Down
107 changes: 95 additions & 12 deletions packages/jumpstarter-driver-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ $ pip3 install --extra-index-url {{index_url}} jumpstarter-driver-shell

## Configuration

Example configuration:
The shell driver supports two configuration formats for methods:

### Format 1: Simple String e.g. for self-descriptive short commands

```yaml
export:
Expand All @@ -20,12 +22,40 @@ export:
config:
methods:
ls: "ls"
method2: "echo 'Hello World 2'"
#multi line method
method3: |
echo 'Hello World $1'
echo 'Hello World $2'
env_var: "echo $1,$2,$ENV_VAR"
echo_hello: "echo 'Hello World'"
```

### Format 2: Unified Format with Descriptions

```yaml
export:
shell:
type: jumpstarter_driver_shell.driver.Shell
config:
methods:
ls:
command: "ls -la"
description: "List directory contents with details"
deploy:
command: "ansible-playbook deploy.yml"
description: "Deploy application using Ansible"
# Multi-line commands work too
setup:
command: |
echo 'Setting up environment'
export PATH=$PATH:/usr/local/bin
./setup.sh
description: "Set up the development environment"
# Description-only (uses default "echo Hello" command)
placeholder:
description: "Placeholder method for testing"
# Custom timeout for long-running operations
long_backup:
command: "tar -czf backup.tar.gz /data && rsync backup.tar.gz remote:/backups/"
description: "Create and sync backup (may take a while)"
timeout: 1800 # 30 minutes instead of default 5 minutes
# You can mix both formats
simple_echo: "echo 'simple'"
# optional parameters
cwd: "/tmp"
log_level: "INFO"
Expand All @@ -34,6 +64,25 @@ export:
- "-c"
```

### Configuration Parameters

| Parameter | Description | Type | Required | Default |
|-----------|-------------|------|----------|---------|
| `methods` | Dictionary of methods. Values can be:<br/>- String: just the command<br/>- Dict: `{command: "...", description: "...", timeout: ...}` | `dict[str, str \| dict]` | Yes | - |
| `cwd` | Working directory for shell commands | `str` | No | `None` |
| `log_level` | Logging level | `str` | No | `"INFO"` |
| `shell` | Shell command to execute scripts | `list[str]` | No | `["bash", "-c"]` |
| `timeout` | Command timeout in seconds | `int` | No | `300` |

**Method Configuration Options:**

For the dict format, each method supports:
- `command`: The shell command to execute (optional, defaults to `"echo Hello"`)
- `description`: CLI help text (optional, defaults to `"Execute the {method_name} shell method"`)
- `timeout`: Command-specific timeout in seconds (optional, defaults to global `timeout` value)

**Note:** You can mix both formats in the same configuration - use string format for simple commands and dict format when you want custom descriptions or timeouts.

## API Reference

Assuming the exporter driver is configured as in the example above, the client
Expand Down Expand Up @@ -66,7 +115,11 @@ methods will be generated dynamically, and they will be available as follows:

## CLI Usage

The shell driver also provides a CLI when using `jmp shell`. All configured methods become available as CLI commands, except for methods starting with `_` which are considered private and hidden from the end user:
The shell driver also provides a CLI when using `jmp shell`. All configured methods become available as CLI commands, except for methods starting with `_` which are considered private and hidden from the end user.

### CLI Help Output

With unified format (custom descriptions):

```console
$ jmp shell --exporter shell-exporter
Expand All @@ -76,10 +129,40 @@ Usage: j shell [OPTIONS] COMMAND [ARGS]...
Shell command executor

Commands:
env_var Execute the env_var shell method
ls Execute the ls shell method
method2 Execute the method2 shell method
method3 Execute the method3 shell method
deploy Deploy application using Ansible
ls List directory contents with details
setup Set up the development environment
```

With simple string format (default descriptions):

```console
$ j shell
Usage: j shell [OPTIONS] COMMAND [ARGS]...

Shell command executor

Commands:
deploy Execute the deploy shell method
ls Execute the ls shell method
setup Execute the setup shell method
```

**Mixed format example:**

```yaml
methods:
deploy:
command: "ansible-playbook deploy.yml"
description: "Deploy using Ansible"
restart: "systemctl restart myapp" # Simple format
```

Results in:
```console
Commands:
deploy Deploy using Ansible
restart Execute the restart shell method
```

### CLI Command Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import click

from jumpstarter.client import DriverClient
from jumpstarter.client.decorators import driver_click_group


@dataclass(kw_only=True)
Expand Down Expand Up @@ -41,7 +42,7 @@ def execute(*args, **kwargs):

def cli(self):
"""Create CLI interface for dynamically configured shell methods"""
@click.group
@driver_click_group(self)
def base():
"""Shell command executor"""
pass
Expand All @@ -58,13 +59,6 @@ def base():

def _add_method_command(self, group, method_name):
"""Add a Click command for a specific shell method"""
@group.command(
name=method_name,
context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
)
@click.argument('args', nargs=-1, type=click.UNPROCESSED)
@click.option('--env', '-e', multiple=True,
help='Environment variables in KEY=VALUE format')
def method_command(args, env):
# Parse environment variables
env_dict = {}
Expand All @@ -81,5 +75,12 @@ def method_command(args, env):
if returncode != 0:
raise click.exceptions.Exit(returncode)

# Update the docstring dynamically
method_command.__doc__ = f"Execute the {method_name} shell method"
# Decorate and register the command with help text
method_command = click.argument('args', nargs=-1, type=click.UNPROCESSED)(method_command)
method_command = click.option('--env', '-e', multiple=True,
help='Environment variables in KEY=VALUE format')(method_command)
method_command = group.command(
name=method_name,
help=self.methods_description.get( method_name, f"Execute the {method_name} shell method"),
context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
)(method_command)
Loading
Loading