Metadata-Version: 2.3
Name: cliffy
Version: 0.4.0
Summary: $ cli load from.yaml
Home-page: https://github.com/jaykv/cliffy
Author: Jay
Author-email: jay.github0@gmail.com
Requires-Python: >=3.9,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Provides-Extra: rich
Requires-Dist: click (>=8.1.0,<9.0.0)
Requires-Dist: deepmerge (>=1.1.0,<2.0.0)
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
Requires-Dist: packaging (>=23.1,<24.0)
Requires-Dist: pybash (>=0.3.4,<0.4.0)
Requires-Dist: pydantic (>=2.6.1,<3.0.0)
Requires-Dist: pyyaml (>=6.0,<7.0)
Requires-Dist: rich-click (>=1.6.1,<2.0.0) ; extra == "rich"
Requires-Dist: shellingham (>=1.5.0.post1,<2.0.0) ; extra == "rich"
Requires-Dist: shiv (>=1.0.3,<2.0.0)
Requires-Dist: typer-slim (>=0.12.3,<0.13.0)
Requires-Dist: watchdog (>=4.0.1,<5.0.0)
Project-URL: Repository, https://github.com/jaykv/cliffy
Description-Content-Type: text/markdown

[![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/jaykv/cliffy/python-app.yaml?branch=main)](https://github.com/jaykv/cliffy/actions)
[![PyPI](https://img.shields.io/pypi/v/cliffy)](https://pypi.org/project/cliffy/)
![GitHub](https://img.shields.io/github/license/jaykv/cliffy)
[![cliffy docs](https://img.shields.io/badge/cliffy%20docs-8A2BE2)](https://jaykv.github.io/cliffy)

# cliffy :mountain:
**cliffy** simplifies the creation, management, and deployment of CLIs. Define your CLI's structure and behavior in a YAML manifest, and let cliffy handle the rest.

## Features
* Write CLIs with YAML manifests
* Manage CLIs- load, test, update, list, and remove
* Built-in shell and Python scripting support
* Supports Jinja2 templating
* Hot-reload CLIs on manifest changes for easier development
* Build CLIs into self-contained, single-file portable zipapps for sharing

### Load

1. Define a manifest
```yaml
# hello.yaml
name: hello
version: 0.1.0

commands:
  shell: 
    help: Print hello in shell
    run: $echo "hello from shell"
  python: print("hello from python")
```

2. Load CLI
```
$ cli load hello.yaml
```
Parses `hello.yaml` to generate a Typer CLI and load it into the running Python environment.

3. Run CLI directly

`hello -h`

![hello-demo](docs/images/hello.png)

For more examples, check [examples](examples/) directory.

### Build

1. Define a manifest
```yaml
# requires.yaml
name: requires
version: 0.1.0

requires:
  - requests >= 2.30.0
  - six

imports:
  - import six

commands:
  shell: $echo "hello from shell"
  python: print("hello from python")
  py: |
    if six.PY2:
        print("python 2")
    if six.PY3:
        print("python 3")
```

2. Build CLI
```
$ cli build requires.yaml -o dist
```

Builds a portable zipapp containing the CLI and its package requirements.

3. Run CLI
```
./dist/requires -h
```

## Usage
`cli <command>`


| Command | Description |
|---|---|
| init \<cli name> | Generate a template CLI manifest for a new CLI |
| load \<manifest> | Add a new CLI based on the manifest |
| render \<manifest> | View generated CLI script for a manifest |
| list, ls | Output a list of loaded CLIs |
| update \<cli name> | Reload a loaded CLI |
| remove \<cli name>, rm \<cli name> | Remove a loaded CLI |
| run \<manifest> -- \<args> | Runs a CLI manifest command in isolation|
| build \<cli name or manifest> | Build a CLI manifest or a loaded CLI into a self-contained zipapp |
| info \<cli name> | Display CLI metadata |
| dev \<manifest> | Start hot-reloader for a manifest for active development |
| test \<manifest> | Run tests defined in a manifest |
| validate \<manifest> | Validate the syntax and structure of a CLI manifest |

## How it works
1. Define CLI manifests in YAML files
2. Run `cli` commands to load, build, and manage CLIs
3. When loaded, cliffy parses the manifest and generates a [Typer](https://github.com/tiangolo/typer) CLI that is deployed directly as a script
4. Any code starting with `$` will translate to subprocess calls via [PyBash](https://github.com/cliffy-sh/pybash)
5. Run loaded CLIs straight from the terminal
6. When ready to share, run `build` to generate portable zipapps built with [Shiv](https://github.com/linkedin/shiv)

## Get started

Cliffy can be installed using either pip or uv package managers.

### With pip
* `pip install "cliffy[rich]"` to include [rich-click](https://github.com/ewels/rich-click) for colorful CLI help output formatted with [rich](https://github.com/Textualize/rich).

or 

* `pip install cliffy` to use the default help output.

### With uv
* `uvx cliffy --help`
* Load: `uvx cliffy load examples/hello.yaml`
* Run: `uvx --from cliffy hello`

## Manifest template
Generated by `cli init`. For a barebones template, run `cli init --raw`

```yaml
manifestVersion: v2

# The name of the CLI, used when invoking from command line.
name: cliffy

# CLI version
version: 0.1.0

# Brief description of the CLI
help: A brief description of your CLI

# List of Python package dependencies for the CLI.Supports requirements specifier syntax.
requires: []
  # - requests>=2.25.1
  # - pyyaml~=5.4

# List of external CLI manifests to include.Performs a deep merge of manifests sequentially in the order given to assemble a merged manifest
# and finally, deep merges the merged manifest with this manifest.
includes: []
  # - path/to/other/manifest.yaml

# Mapping defining manifest variables that can be referenced in any other blocks
# Environments variables can be used in this section with ${some_env_var} for dynamic parsing
# Supports jinja2 formatted expressions as values
# Interpolate defined vars in other blocks jinja2-styled {{ var_name }}.
vars:
  data_file: "data.json"
  debug_mode: "{{ env['DEBUG'] or 'False' }}"

# String block or list of strings containing any module imports
# These can be used to import any python modules that the CLI depends on.
imports: |
  import json
  import os
  from pathlib import Path

# List of helper function definitions
# These functions should be defined as strings that can be executed by the Python interpreter.
functions:
  - |
    def load_data() -> dict:
        data_path = Path("{{ data_file }}")
        if data_path.exists():
            with data_path.open() as f:
                return json.load(f)
        return {}
  - |
      def save_data(data):
          with open("{{data_file}}", "w") as f:
              json.dump(data, f, indent=2)
# A mapping containing any shared type definitions
# These types can be referenced by name in the args section to provide type annotations for params and options defined in the args section.
types:
  Filename: str = typer.Argument(..., help="Name of the file to process")
  Verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output")

# Arguments applied to all commands
global_args:
  - verbose: Verbose

# Reusable command templates
command_templates:
  with_confirmation:
    args:
      - "yes": bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt")
    pre_run: |
      if not yes:
        typer.confirm("Are you sure you want to proceed?", abort=True)

# A mapping containing the command definitions for the CLI
# Each command should have a unique key- which can be either a group command or nested subcommands
# Nested subcommands are joined by '.' in between each level
# Aliases for commands can be separated in the key by '|'
# A special '(*)' wildcard can be used to spread the subcommand to all group-level commands
commands:
  hello:
    help: Greet the user
    args:
      - name: str = typer.Option("World", "--name", "-n", help="Name to greet")
    run: |
      print(f"Hello, {name}!")
      $ echo "i can also mix-and-match this command script to run shell commands"

  file.process:
    help: Process a file
    args:
      - filename: Filename
    run: |
      data = load_data()
      print(f"Processing {filename}")
      if verbose:
        print("Verbose output enabled")
      data["processed"] = [filename]
      # Process the file here
      save_data(data)

  delete|rm:
    help: Delete a file
    template: with_confirmation
    args: [filename: Filename]
    run: |
      if verbose:
        print(f"Deleting {filename}")
      os.remove(filename)
      print("File deleted successfully")

# Additional CLI configuration options
cli_options:
  rich_help_panel: True

# Test cases for commands
tests:
  - hello --name Alice: assert 'Hello, Alice!' in result.output
  - file process test.txt: assert 'Processing test.txt' in result.output
```

