Metadata-Version: 2.4
Name: avalon-cli
Version: 0.2.0
Summary: Command-line companion for the Avalon real-time web framework: scaffold projects and run an auto-reloading dev server.
Author: nehz
License-Expression: MIT
Keywords: avalon,cli,scaffold,dev-server,real-time,web
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Code Generators
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# avalon-cli

**The command-line companion for the [Avalon](https://pypi.org/project/avalon/) real-time web framework.**

`avalon-cli` gets you from zero to a running, auto-reloading app in two commands:
`avalon new` scaffolds a project and `avalon dev` runs it, restarting the server
every time you save a file. It uses only the Python standard library and does not
import `avalon` itself, so it installs in seconds and works with any Avalon version
(or with no framework at all, via the `static` template).

## Features

- **Project scaffolding**: `avalon new` generates a ready-to-run project from a built-in template.
- **Auto-reloading dev server**: `avalon dev` runs your dev command, watches files by polling
  (no native dependencies), and restarts the process on change. If the process crashes it waits
  for your fix and starts again on the next save, so you never have to restart `avalon dev`.
- **One config file**: `avalon.toml` holds the dev command, host, port and watch rules, found by
  walking upward from the current directory.
- **Clean shutdown**: Ctrl-C or SIGTERM always terminates the child process; nothing is orphaned.
- **Zero dependencies**: standard library only, Python 3.11+.

## Install

```bash
pip install avalon-cli
```

This installs the `avalon` command. `python -m avalon_cli` works too.

## Quickstart

```bash
avalon new chat-demo            # Avalon app (default "minimal" template)
cd chat-demo
pip install avalon              # the framework the generated app.py uses
avalon dev                      # http://127.0.0.1:8000/, reloads on save
```

No framework yet? The `static` template needs nothing but Python:

```bash
avalon new mysite --template static
avalon dev -C mysite --port 9000
```

## Commands

All commands print errors as `error: <message>` on stderr and exit with status 1;
usage errors exit with status 2.

### `avalon new NAME [-t TEMPLATE] [-d DIRECTORY] [--force]`

Create the directory `DIRECTORY/NAME` and fill it from a template.

| Option | Default | Meaning |
| --- | --- | --- |
| `NAME` | (required) | Project name and directory name. Must start with a letter and contain only letters, digits, `-` and `_`. |
| `-t`, `--template` | `minimal` | One of the templates listed by `avalon templates`. |
| `-d`, `--directory` | `.` | Parent directory to create the project in. |
| `--force` | off | Write into an existing non-empty directory, overwriting files the template provides (other files are left alone). |

An existing *empty* directory is always accepted.

### `avalon templates`

List the built-in templates:

| Template | Files | Notes |
| --- | --- | --- |
| `minimal` (default) | `avalon.toml`, `app.py`, `templates/index.html`, `README.md`, `.gitignore` | An Avalon app with one page and a WebSocket echo endpoint. Requires `avalon`. |
| `static` | `avalon.toml`, `public/index.html`, `public/style.css`, `public/app.js`, `README.md`, `.gitignore` | Served by `python -m http.server`; no dependencies. |

### `avalon dev [-C PROJECT] [--host HOST] [-p PORT] [--interval SECONDS] [--no-reload]`

Find `avalon.toml` (in `PROJECT` or any parent directory), start the configured dev
command from the project root, and restart it whenever a watched file is added,
modified or removed.

| Option | Default | Meaning |
| --- | --- | --- |
| `-C`, `--project` | `.` | Directory to start looking for `avalon.toml`. |
| `--host` | `[dev].host` | Overrides the host. |
| `-p`, `--port` | `[dev].port` | Overrides the port (1-65535). |
| `--interval` | `[dev].interval` | Seconds between file-change polls. |
| `--no-reload` | off | Run the command once, without watching; `avalon dev` exits with the command's exit code. |

The child process receives `AVALON_HOST`, `AVALON_PORT` and `AVALON_ENV=development`
in its environment. With reloading on, `avalon dev` exits with status 0 on SIGTERM
and 130 on Ctrl-C.

### `avalon info [-C PROJECT]`

Print the `avalon-cli` and Python versions and, if a project is found, its name,
root, fully-resolved dev command, address, watch paths and extensions.

### `avalon --version`

Print `avalon 0.2.0`.

## Configuration: `avalon.toml`

```toml
[project]
name = "chat-demo"          # default: the directory name

[dev]
command = "{python} app.py" # placeholders: {host}, {port}, {python}
host = "127.0.0.1"
port = 8000
watch = ["."]               # files or directories, relative to the project root
extensions = [".py", ".html", ".css", ".js", ".toml"]  # [] watches every file
ignore = [".venv", ".git", "__pycache__", "node_modules"]  # glob patterns matched against names
interval = 0.5              # seconds between polls
```

Every key is optional; the values above are the defaults. Extensions without a
leading dot get one (`"py"` becomes `".py"`). Unknown keys in `[dev]` are rejected
so typos are caught early. The command is split shell-style *before* placeholders
are substituted, so a `{python}` path containing spaces stays a single argument.
`{python}` is the interpreter running `avalon-cli`.

## Python API

Everything the CLI does is available from `avalon_cli`:

```python
from avalon_cli import DevServer, build_command, create_project, load_config

result = create_project("chat-demo", template="minimal", parent=".", force=False)
print(result.root, result.template, result.files)   # ScaffoldResult

config = load_config(result.root)                   # ProjectConfig(root, name, dev=DevConfig(...))
argv = build_command(config.dev.command, host=config.dev.host, port=config.dev.port)

server = DevServer(
    argv,
    cwd=config.root,
    watch=config.dev.watch,
    extensions=config.dev.extensions,
    ignore=config.dev.ignore,
    interval=config.dev.interval,
)
server.run()   # blocks; pass a threading.Event to stop it from another thread
```

| Name | Description |
| --- | --- |
| `create_project(name, template="minimal", parent=".", *, force=False) -> ScaffoldResult` | Scaffold a project; raises `ScaffoldError`. |
| `load_config(start=".") -> ProjectConfig` | Locate and validate `avalon.toml`; raises `ConfigError`. |
| `find_project_root(start=".") -> Path` | Directory containing the nearest `avalon.toml`; raises `ConfigError`. |
| `build_command(template, *, host, port, python=None) -> list[str]` | Turn a dev command template into argv; raises `ValueError`. |
| `DevServer(argv, *, cwd=".", env=None, watch=(".",), extensions=(), ignore=(), interval=0.5, reload=True, log=...)` | Reloading process runner with `start()`, `stop(timeout=5.0)`, `restart()`, `poll_changes()`, `run(stop_event=None) -> int`, and a `restarts` counter. |
| `take_snapshot(roots, extensions=(), ignore=()) -> dict[Path, int]` | File-to-mtime map of watched files. |
| `diff_snapshots(old, new) -> set[Path]` | Paths added, removed or modified between two snapshots. |
| `TEMPLATES`, `get_template(name)`, `ProjectTemplate` | The built-in template registry. |

## Development

```bash
python3 -m venv .venv && . .venv/bin/activate
pip install -e .
python -m unittest discover -s tests -t .
```

## License

MIT
