{"id":13903011,"url":"https://github.com/jaykv/cliffy","last_synced_at":"2026-02-16T12:02:32.644Z","repository":{"id":113876816,"uuid":"592581698","full_name":"jaykv/cliffy","owner":"jaykv","description":"$ cli load from.yaml","archived":false,"fork":false,"pushed_at":"2024-08-26T04:35:18.000Z","size":338,"stargazers_count":3,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-09-27T07:06:14.144Z","etag":null,"topics":["cli","python","typer","yaml","zipapp"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jaykv.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-24T03:20:03.000Z","updated_at":"2024-08-28T02:24:26.000Z","dependencies_parsed_at":null,"dependency_job_id":"f45209a0-7d93-4cb8-80de-1e9c579676ff","html_url":"https://github.com/jaykv/cliffy","commit_stats":null,"previous_names":["jaykv/cliffy"],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaykv%2Fcliffy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaykv%2Fcliffy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaykv%2Fcliffy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaykv%2Fcliffy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jaykv","download_url":"https://codeload.github.com/jaykv/cliffy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226320844,"owners_count":17606365,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","python","typer","yaml","zipapp"],"created_at":"2024-08-06T22:01:33.546Z","updated_at":"2026-02-16T12:02:32.572Z","avatar_url":"https://github.com/jaykv.png","language":"Python","funding_links":[],"categories":["cli"],"sub_categories":[],"readme":"![Cliffy logo](docs/images/logo.svg)\n\n[![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)\n[![PyPI](https://img.shields.io/pypi/v/cliffy)](https://pypi.org/project/cliffy/)\n![GitHub](https://img.shields.io/github/license/jaykv/cliffy)\n[![codecov](https://codecov.io/github/jaykv/cliffy/graph/badge.svg?token=84SM8XD5IW)](https://codecov.io/github/jaykv/cliffy)\n[![cliffy docs](https://img.shields.io/badge/📝%20docs-8A2BE2)](https://jaykv.github.io/cliffy)\n\nBuild feature-rich CLIs _quickly_.\n\n## Features\n* ⚡ Generate Python CLIs with a simple YAML manifest\n* 📦 Package your CLI into a single executable that runs anywhere\n* 🔄 Hot-reload changes instantly during development with built-in testing\n* 🎨 Mix Python and shell commands naturally with built-in scripting support\n* 🤖 AI-friendly with [manifest generation support](https://jaykv.github.io/cliffy/features#ai-features)\n\n### Load\n\n1. Define a manifest\n```yaml\n# hello.yaml\nname: hello\nversion: 0.1.0\n\ncommands:\n  shell: \n    help: Print hello in shell\n    run: $echo \"hello from shell\"\n  python: print(\"hello from python\")\n```\n\n2. Load CLI\n```\n$ cli load hello.yaml\n```\nParses `hello.yaml` to generate a Typer CLI and load it into the running Python environment.\n\n3. Run CLI directly\n\n`hello -h`\n\n![hello-demo](docs/images/hello.png)\n\n### Build\n\nSimple todo CLI with sqlite3 + tabulate.\n\n1. Define the manifest\n```yaml\n# todo.yaml\nname: todo\nversion: 1.0.0\nrequires:\n  - tabulate  # For pretty table output\n  - rich      # For colored terminal output\nimports: |\n  import sqlite3\n  from pathlib import Path\n  from tabulate import tabulate\n  from rich import print\n\ncommands:\n  create:\n    help: Create a new database with tasks table\n    params:\n      - name: str = typer.Option(..., prompt=True, confirmation_prompt=True)\n    run: |\n      db_path = Path(f\"{name}.db\")\n      conn = sqlite3.connect(db_path)\n      conn.execute(\"CREATE TABLE tasks (id INTEGER PRIMARY KEY, task TEXT NOT NULL, done BOOLEAN NOT NULL)\")\n\n      # insert example tasks\n      conn.execute(\"INSERT INTO tasks (task, done) VALUES ('Fight for your right!', 0)\")\n      conn.execute(\"INSERT INTO tasks (task, done) VALUES ('To party!', 1)\")\n      conn.commit()\n      conn.close()\n      print(f\"✨ Created database {db_path} with tasks table\")\n\n  tasks:\n    help: List tasks in database\n    params: [name: str!]\n    run: |\n      conn = sqlite3.connect(f\"{name}.db\")\n      cursor = conn.execute(\"SELECT * FROM tasks\")\n      tasks = cursor.fetchall()\n      conn.close()\n      print(tabulate(tasks, headers=['ID', 'Task', 'Done'], tablefmt='grid'))\n\n  add:\n    help: Add a new task\n    params: [name: str!, task: str!]\n    run: |\n      conn = sqlite3.connect(f\"{name}.db\")\n      conn.execute(\"INSERT INTO tasks (task, done) VALUES (?, 0)\", (task,))\n      conn.commit()\n      conn.close()\n      print(f\"📝 Added task: {task}\")\n\n  complete:\n    help: Mark a task as complete\n    params: [name: str!, id: int!]\n    run: |\n      conn = sqlite3.connect(f\"{name}.db\")\n      conn.execute(\"UPDATE tasks SET done = 1 WHERE id = ?\", (id,))\n      conn.commit()\n      conn.close()\n      print(f\"🎉 Marked task {id} as complete\")\n```\n\n![todo-demo](docs/images/demo.gif)\n\nFor more examples, check [examples](examples/) directory.\n\n## Usage\n`cli \u003ccommand\u003e`\n\n| Command | Description |\n|---|---|\n| `init \u003ccli name\u003e`| Generate a template CLI manifest for a new CLI |\n| `load \u003cmanifest\u003e` | Add a new CLI based on the manifest |\n| `render \u003cmanifest\u003e` | View generated CLI script for a manifest |\n| `list, ls` | Output a list of loaded CLIs |\n| `update \u003ccli name\u003e`| Reload a loaded CLI |\n| `remove \u003ccli name\u003e, rm \u003ccli name\u003e` | Remove a loaded CLI |\n| `run \u003cmanifest\u003e -- \\\u003cargs\u003e`| Runs a CLI manifest command in isolation|\n| `build \u003ccli name or manifest\u003e` | Build a CLI manifest or a loaded CLI into a self-contained zipapp |\n| `info \u003ccli name\u003e` | Display CLI metadata |\n| `dev \u003cmanifest\u003e` | Start hot-reloader for a manifest for active development |\n| `test \u003cmanifest\u003e` | Run tests defined in a manifest |\n| `validate \u003cmanifest\u003e` | Validate the syntax and structure of a CLI manifest |\n| `docs \u003ccli name or manifest\u003e` | Generate documentation for a CLI |\n| `ai generate \u003ccli name\u003e \u003cdescription\u003e` | Generate a CLI manifest based on a description. |\n| `ai ask \u003cprompt\u003e` | Ask a question about cliffy or a specific CLI manifest. |\n\n## How it works\n1. Define CLI manifests in YAML files\n2. Run `cli` commands to load, build, and manage CLIs\n3. When loaded, cliffy parses the manifest and generates a [Typer](https://github.com/tiangolo/typer) CLI that is deployed directly as a script\n4. Any code starting with `$` will translate to subprocess calls via [PyBash](https://github.com/jaykv/pybash)\n5. Run loaded CLIs straight from the terminal\n6. When ready to share, run `build` to generate portable zipapps built with [Shiv](https://github.com/linkedin/shiv)\n\n## Get started\n\nCliffy can be installed using either pip or uv package managers.\n\n### With pip\n* `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).\n\nor \n\n* `pip install cliffy` to use the default help output.\n* `cli init mycli`\n\n### With uv\n* Init: `uvx cliffy init mycli`\n* Load: `uvx cliffy load mycli.yaml`\n* Run: `uvx --from cliffy mycli -h`\n\n## Manifest template\nGenerated by `cli init`. For a barebones template, run `cli init --raw`\n\n```yaml\nmanifestVersion: v3\n\n# The name of the CLI, used when invoking from command line.\nname: cliffy\n\n# CLI version\nversion: 0.1.0\n\n# Brief description of the CLI\nhelp: A brief description of your CLI\n\n# List of Python package dependencies for the CLI.Supports requirements specifier syntax.\nrequires: []\n  # - requests\u003e=2.25.1\n  # - pyyaml~=5.4\n\n# List of external CLI manifests to include.Performs a deep merge of manifests sequentially in the order given to assemble a merged manifest\n# and finally, deep merges the merged manifest with this manifest.\nincludes: []\n  # - path/to/other/manifest.yaml\n\n# Mapping defining manifest variables that can be referenced in any other blocks\n# Environments variables can be used in this section with ${some_env_var} for dynamic parsing\n# Supports jinja2 formatted expressions as values\n# Interpolate defined vars in other blocks jinja2-styled {{ var_name }}.\nvars:\n  data_file: \"data.json\"\n  debug_mode: \"{{ env['DEBUG'] or 'False' }}\"\n\n# String block or list of strings containing any module imports\n# These can be used to import any python modules that the CLI depends on.\nimports: |\n  import json\n  import os\n  from pathlib import Path\n\n# List of helper function definitions\n# These functions should be defined as strings that can be executed by the Python interpreter.\nfunctions:\n  - |\n    def load_data() -\u003e dict:\n        data_path = Path(\"{{ data_file }}\")\n        if data_path.exists():\n            with data_path.open() as f:\n                return json.load(f)\n        return {}\n  - |\n      def save_data(data):\n          with open(\"{{data_file}}\", \"w\") as f:\n              json.dump(data, f, indent=2)\n# A mapping containing any shared type definitions\n# These types can be referenced by name in the args section to provide type annotations for params and options defined in the args section.\ntypes:\n  Filename: str = typer.Argument(..., help=\"Name of the file to process\")\n  Verbose: bool = typer.Option(False, \"--verbose\", \"-v\", help=\"Enable verbose output\")\n\n# Arguments applied to all commands\nglobal_params:\n  - verbose: Verbose\n\n# Reusable command templates\ncommand_templates:\n  with_confirmation:\n    params:\n      - \"yes\": bool = typer.Option(False, \"--yes\", \"-y\", help=\"Skip confirmation prompt\")\n    pre_run: |\n      if not yes:\n        typer.confirm(\"Are you sure you want to proceed?\", abort=True)\n\n# A mapping containing the command definitions for the CLI\n# Each command should have a unique key- which can be either a group command or nested subcommands\n# Nested subcommands are joined by '.' in between each level\n# Aliases for commands can be separated in the key by '|'\n# A special '(*)' wildcard can be used to spread the subcommand to all group-level commands\ncommands:\n  hello:\n    help: Greet the user\n    params:\n      - name: str = typer.Option(\"World\", \"--name\", \"-n\", help=\"Name to greet\")\n    run: |\n      print(f\"Hello, {name}!\")\n      $ echo \"i can also mix-and-match this command script to run shell commands\"\n\n  file.process:\n    help: Process a file\n    params:\n      - filename: Filename\n    run: |\n      data = load_data()\n      print(f\"Processing {filename}\")\n      if verbose:\n        print(\"Verbose output enabled\")\n      data[\"processed\"] = [filename]\n      # Process the file here\n      save_data(data)\n\n  delete|rm:\n    help: Delete a file\n    template: with_confirmation\n    params: [filename: Filename]\n    run: |\n      if verbose:\n        print(f\"Deleting {filename}\")\n      os.remove(filename)\n      print(\"File deleted successfully\")\n\n# Additional CLI configuration options\ncli_options:\n  rich_help_panel: True\n\n# Test cases for commands\ntests:\n  - hello --name Alice: assert 'Hello, Alice!' in result.output\n  - file process test.txt: assert 'Processing test.txt' in result.output\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaykv%2Fcliffy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjaykv%2Fcliffy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaykv%2Fcliffy/lists"}