avalon-cli 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nehz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: avalon-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line companion for the Avalon real-time web framework: scaffold projects and run an auto-reloading dev server.
5
+ Author: nehz
6
+ License-Expression: MIT
7
+ Keywords: avalon,cli,scaffold,dev-server,real-time,web
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
+ Classifier: Topic :: Software Development :: Code Generators
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # avalon-cli
27
+
28
+ **The command-line companion for the [Avalon](https://pypi.org/project/avalon/) real-time web framework.**
29
+
30
+ `avalon-cli` gets you from zero to a running, auto-reloading app in two commands:
31
+ `avalon new` scaffolds a project and `avalon dev` runs it, restarting the server
32
+ every time you save a file. It uses only the Python standard library and does not
33
+ import `avalon` itself, so it installs in seconds and works with any Avalon version
34
+ (or with no framework at all, via the `static` template).
35
+
36
+ ## Features
37
+
38
+ - **Project scaffolding**: `avalon new` generates a ready-to-run project from a built-in template.
39
+ - **Auto-reloading dev server**: `avalon dev` runs your dev command, watches files by polling
40
+ (no native dependencies), and restarts the process on change. If the process crashes it waits
41
+ for your fix and starts again on the next save, so you never have to restart `avalon dev`.
42
+ - **One config file**: `avalon.toml` holds the dev command, host, port and watch rules, found by
43
+ walking upward from the current directory.
44
+ - **Clean shutdown**: Ctrl-C or SIGTERM always terminates the child process; nothing is orphaned.
45
+ - **Zero dependencies**: standard library only, Python 3.11+.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install avalon-cli
51
+ ```
52
+
53
+ This installs the `avalon` command. `python -m avalon_cli` works too.
54
+
55
+ ## Quickstart
56
+
57
+ ```bash
58
+ avalon new chat-demo # Avalon app (default "minimal" template)
59
+ cd chat-demo
60
+ pip install avalon # the framework the generated app.py uses
61
+ avalon dev # http://127.0.0.1:8000/, reloads on save
62
+ ```
63
+
64
+ No framework yet? The `static` template needs nothing but Python:
65
+
66
+ ```bash
67
+ avalon new mysite --template static
68
+ avalon dev -C mysite --port 9000
69
+ ```
70
+
71
+ ## Commands
72
+
73
+ All commands print errors as `error: <message>` on stderr and exit with status 1;
74
+ usage errors exit with status 2.
75
+
76
+ ### `avalon new NAME [-t TEMPLATE] [-d DIRECTORY] [--force]`
77
+
78
+ Create the directory `DIRECTORY/NAME` and fill it from a template.
79
+
80
+ | Option | Default | Meaning |
81
+ | --- | --- | --- |
82
+ | `NAME` | (required) | Project name and directory name. Must start with a letter and contain only letters, digits, `-` and `_`. |
83
+ | `-t`, `--template` | `minimal` | One of the templates listed by `avalon templates`. |
84
+ | `-d`, `--directory` | `.` | Parent directory to create the project in. |
85
+ | `--force` | off | Write into an existing non-empty directory, overwriting files the template provides (other files are left alone). |
86
+
87
+ An existing *empty* directory is always accepted.
88
+
89
+ ### `avalon templates`
90
+
91
+ List the built-in templates:
92
+
93
+ | Template | Files | Notes |
94
+ | --- | --- | --- |
95
+ | `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`. |
96
+ | `static` | `avalon.toml`, `public/index.html`, `public/style.css`, `public/app.js`, `README.md`, `.gitignore` | Served by `python -m http.server`; no dependencies. |
97
+
98
+ ### `avalon dev [-C PROJECT] [--host HOST] [-p PORT] [--interval SECONDS] [--no-reload]`
99
+
100
+ Find `avalon.toml` (in `PROJECT` or any parent directory), start the configured dev
101
+ command from the project root, and restart it whenever a watched file is added,
102
+ modified or removed.
103
+
104
+ | Option | Default | Meaning |
105
+ | --- | --- | --- |
106
+ | `-C`, `--project` | `.` | Directory to start looking for `avalon.toml`. |
107
+ | `--host` | `[dev].host` | Overrides the host. |
108
+ | `-p`, `--port` | `[dev].port` | Overrides the port (1-65535). |
109
+ | `--interval` | `[dev].interval` | Seconds between file-change polls. |
110
+ | `--no-reload` | off | Run the command once, without watching; `avalon dev` exits with the command's exit code. |
111
+
112
+ The child process receives `AVALON_HOST`, `AVALON_PORT` and `AVALON_ENV=development`
113
+ in its environment. With reloading on, `avalon dev` exits with status 0 on SIGTERM
114
+ and 130 on Ctrl-C.
115
+
116
+ ### `avalon info [-C PROJECT]`
117
+
118
+ Print the `avalon-cli` and Python versions and, if a project is found, its name,
119
+ root, fully-resolved dev command, address, watch paths and extensions.
120
+
121
+ ### `avalon --version`
122
+
123
+ Print `avalon 0.2.0`.
124
+
125
+ ## Configuration: `avalon.toml`
126
+
127
+ ```toml
128
+ [project]
129
+ name = "chat-demo" # default: the directory name
130
+
131
+ [dev]
132
+ command = "{python} app.py" # placeholders: {host}, {port}, {python}
133
+ host = "127.0.0.1"
134
+ port = 8000
135
+ watch = ["."] # files or directories, relative to the project root
136
+ extensions = [".py", ".html", ".css", ".js", ".toml"] # [] watches every file
137
+ ignore = [".venv", ".git", "__pycache__", "node_modules"] # glob patterns matched against names
138
+ interval = 0.5 # seconds between polls
139
+ ```
140
+
141
+ Every key is optional; the values above are the defaults. Extensions without a
142
+ leading dot get one (`"py"` becomes `".py"`). Unknown keys in `[dev]` are rejected
143
+ so typos are caught early. The command is split shell-style *before* placeholders
144
+ are substituted, so a `{python}` path containing spaces stays a single argument.
145
+ `{python}` is the interpreter running `avalon-cli`.
146
+
147
+ ## Python API
148
+
149
+ Everything the CLI does is available from `avalon_cli`:
150
+
151
+ ```python
152
+ from avalon_cli import DevServer, build_command, create_project, load_config
153
+
154
+ result = create_project("chat-demo", template="minimal", parent=".", force=False)
155
+ print(result.root, result.template, result.files) # ScaffoldResult
156
+
157
+ config = load_config(result.root) # ProjectConfig(root, name, dev=DevConfig(...))
158
+ argv = build_command(config.dev.command, host=config.dev.host, port=config.dev.port)
159
+
160
+ server = DevServer(
161
+ argv,
162
+ cwd=config.root,
163
+ watch=config.dev.watch,
164
+ extensions=config.dev.extensions,
165
+ ignore=config.dev.ignore,
166
+ interval=config.dev.interval,
167
+ )
168
+ server.run() # blocks; pass a threading.Event to stop it from another thread
169
+ ```
170
+
171
+ | Name | Description |
172
+ | --- | --- |
173
+ | `create_project(name, template="minimal", parent=".", *, force=False) -> ScaffoldResult` | Scaffold a project; raises `ScaffoldError`. |
174
+ | `load_config(start=".") -> ProjectConfig` | Locate and validate `avalon.toml`; raises `ConfigError`. |
175
+ | `find_project_root(start=".") -> Path` | Directory containing the nearest `avalon.toml`; raises `ConfigError`. |
176
+ | `build_command(template, *, host, port, python=None) -> list[str]` | Turn a dev command template into argv; raises `ValueError`. |
177
+ | `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. |
178
+ | `take_snapshot(roots, extensions=(), ignore=()) -> dict[Path, int]` | File-to-mtime map of watched files. |
179
+ | `diff_snapshots(old, new) -> set[Path]` | Paths added, removed or modified between two snapshots. |
180
+ | `TEMPLATES`, `get_template(name)`, `ProjectTemplate` | The built-in template registry. |
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ python3 -m venv .venv && . .venv/bin/activate
186
+ pip install -e .
187
+ python -m unittest discover -s tests -t .
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,167 @@
1
+ # avalon-cli
2
+
3
+ **The command-line companion for the [Avalon](https://pypi.org/project/avalon/) real-time web framework.**
4
+
5
+ `avalon-cli` gets you from zero to a running, auto-reloading app in two commands:
6
+ `avalon new` scaffolds a project and `avalon dev` runs it, restarting the server
7
+ every time you save a file. It uses only the Python standard library and does not
8
+ import `avalon` itself, so it installs in seconds and works with any Avalon version
9
+ (or with no framework at all, via the `static` template).
10
+
11
+ ## Features
12
+
13
+ - **Project scaffolding**: `avalon new` generates a ready-to-run project from a built-in template.
14
+ - **Auto-reloading dev server**: `avalon dev` runs your dev command, watches files by polling
15
+ (no native dependencies), and restarts the process on change. If the process crashes it waits
16
+ for your fix and starts again on the next save, so you never have to restart `avalon dev`.
17
+ - **One config file**: `avalon.toml` holds the dev command, host, port and watch rules, found by
18
+ walking upward from the current directory.
19
+ - **Clean shutdown**: Ctrl-C or SIGTERM always terminates the child process; nothing is orphaned.
20
+ - **Zero dependencies**: standard library only, Python 3.11+.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install avalon-cli
26
+ ```
27
+
28
+ This installs the `avalon` command. `python -m avalon_cli` works too.
29
+
30
+ ## Quickstart
31
+
32
+ ```bash
33
+ avalon new chat-demo # Avalon app (default "minimal" template)
34
+ cd chat-demo
35
+ pip install avalon # the framework the generated app.py uses
36
+ avalon dev # http://127.0.0.1:8000/, reloads on save
37
+ ```
38
+
39
+ No framework yet? The `static` template needs nothing but Python:
40
+
41
+ ```bash
42
+ avalon new mysite --template static
43
+ avalon dev -C mysite --port 9000
44
+ ```
45
+
46
+ ## Commands
47
+
48
+ All commands print errors as `error: <message>` on stderr and exit with status 1;
49
+ usage errors exit with status 2.
50
+
51
+ ### `avalon new NAME [-t TEMPLATE] [-d DIRECTORY] [--force]`
52
+
53
+ Create the directory `DIRECTORY/NAME` and fill it from a template.
54
+
55
+ | Option | Default | Meaning |
56
+ | --- | --- | --- |
57
+ | `NAME` | (required) | Project name and directory name. Must start with a letter and contain only letters, digits, `-` and `_`. |
58
+ | `-t`, `--template` | `minimal` | One of the templates listed by `avalon templates`. |
59
+ | `-d`, `--directory` | `.` | Parent directory to create the project in. |
60
+ | `--force` | off | Write into an existing non-empty directory, overwriting files the template provides (other files are left alone). |
61
+
62
+ An existing *empty* directory is always accepted.
63
+
64
+ ### `avalon templates`
65
+
66
+ List the built-in templates:
67
+
68
+ | Template | Files | Notes |
69
+ | --- | --- | --- |
70
+ | `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`. |
71
+ | `static` | `avalon.toml`, `public/index.html`, `public/style.css`, `public/app.js`, `README.md`, `.gitignore` | Served by `python -m http.server`; no dependencies. |
72
+
73
+ ### `avalon dev [-C PROJECT] [--host HOST] [-p PORT] [--interval SECONDS] [--no-reload]`
74
+
75
+ Find `avalon.toml` (in `PROJECT` or any parent directory), start the configured dev
76
+ command from the project root, and restart it whenever a watched file is added,
77
+ modified or removed.
78
+
79
+ | Option | Default | Meaning |
80
+ | --- | --- | --- |
81
+ | `-C`, `--project` | `.` | Directory to start looking for `avalon.toml`. |
82
+ | `--host` | `[dev].host` | Overrides the host. |
83
+ | `-p`, `--port` | `[dev].port` | Overrides the port (1-65535). |
84
+ | `--interval` | `[dev].interval` | Seconds between file-change polls. |
85
+ | `--no-reload` | off | Run the command once, without watching; `avalon dev` exits with the command's exit code. |
86
+
87
+ The child process receives `AVALON_HOST`, `AVALON_PORT` and `AVALON_ENV=development`
88
+ in its environment. With reloading on, `avalon dev` exits with status 0 on SIGTERM
89
+ and 130 on Ctrl-C.
90
+
91
+ ### `avalon info [-C PROJECT]`
92
+
93
+ Print the `avalon-cli` and Python versions and, if a project is found, its name,
94
+ root, fully-resolved dev command, address, watch paths and extensions.
95
+
96
+ ### `avalon --version`
97
+
98
+ Print `avalon 0.2.0`.
99
+
100
+ ## Configuration: `avalon.toml`
101
+
102
+ ```toml
103
+ [project]
104
+ name = "chat-demo" # default: the directory name
105
+
106
+ [dev]
107
+ command = "{python} app.py" # placeholders: {host}, {port}, {python}
108
+ host = "127.0.0.1"
109
+ port = 8000
110
+ watch = ["."] # files or directories, relative to the project root
111
+ extensions = [".py", ".html", ".css", ".js", ".toml"] # [] watches every file
112
+ ignore = [".venv", ".git", "__pycache__", "node_modules"] # glob patterns matched against names
113
+ interval = 0.5 # seconds between polls
114
+ ```
115
+
116
+ Every key is optional; the values above are the defaults. Extensions without a
117
+ leading dot get one (`"py"` becomes `".py"`). Unknown keys in `[dev]` are rejected
118
+ so typos are caught early. The command is split shell-style *before* placeholders
119
+ are substituted, so a `{python}` path containing spaces stays a single argument.
120
+ `{python}` is the interpreter running `avalon-cli`.
121
+
122
+ ## Python API
123
+
124
+ Everything the CLI does is available from `avalon_cli`:
125
+
126
+ ```python
127
+ from avalon_cli import DevServer, build_command, create_project, load_config
128
+
129
+ result = create_project("chat-demo", template="minimal", parent=".", force=False)
130
+ print(result.root, result.template, result.files) # ScaffoldResult
131
+
132
+ config = load_config(result.root) # ProjectConfig(root, name, dev=DevConfig(...))
133
+ argv = build_command(config.dev.command, host=config.dev.host, port=config.dev.port)
134
+
135
+ server = DevServer(
136
+ argv,
137
+ cwd=config.root,
138
+ watch=config.dev.watch,
139
+ extensions=config.dev.extensions,
140
+ ignore=config.dev.ignore,
141
+ interval=config.dev.interval,
142
+ )
143
+ server.run() # blocks; pass a threading.Event to stop it from another thread
144
+ ```
145
+
146
+ | Name | Description |
147
+ | --- | --- |
148
+ | `create_project(name, template="minimal", parent=".", *, force=False) -> ScaffoldResult` | Scaffold a project; raises `ScaffoldError`. |
149
+ | `load_config(start=".") -> ProjectConfig` | Locate and validate `avalon.toml`; raises `ConfigError`. |
150
+ | `find_project_root(start=".") -> Path` | Directory containing the nearest `avalon.toml`; raises `ConfigError`. |
151
+ | `build_command(template, *, host, port, python=None) -> list[str]` | Turn a dev command template into argv; raises `ValueError`. |
152
+ | `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. |
153
+ | `take_snapshot(roots, extensions=(), ignore=()) -> dict[Path, int]` | File-to-mtime map of watched files. |
154
+ | `diff_snapshots(old, new) -> set[Path]` | Paths added, removed or modified between two snapshots. |
155
+ | `TEMPLATES`, `get_template(name)`, `ProjectTemplate` | The built-in template registry. |
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ python3 -m venv .venv && . .venv/bin/activate
161
+ pip install -e .
162
+ python -m unittest discover -s tests -t .
163
+ ```
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "avalon-cli"
7
+ version = "0.2.0"
8
+ description = "Command-line companion for the Avalon real-time web framework: scaffold projects and run an auto-reloading dev server."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "nehz" }]
14
+ keywords = ["avalon", "cli", "scaffold", "dev-server", "real-time", "web"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Environment :: Web Environment",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
27
+ "Topic :: Software Development :: Code Generators",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.scripts]
33
+ avalon = "avalon_cli.cli:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ avalon_cli = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ """avalon-cli: command-line companion for the Avalon real-time web framework.
2
+
3
+ The package is self-contained (standard library only) and does not import
4
+ ``avalon`` itself. The public API mirrors the CLI commands:
5
+
6
+ * :func:`create_project` - ``avalon new``
7
+ * :class:`DevServer` / :func:`build_command` - ``avalon dev``
8
+ * :func:`load_config` - reads ``avalon.toml``
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ __version__ = "0.2.0"
14
+
15
+ from .config import ConfigError, DevConfig, ProjectConfig, find_project_root, load_config # noqa: E402
16
+ from .reloader import DevServer, build_command, diff_snapshots, take_snapshot # noqa: E402
17
+ from .scaffold import ScaffoldError, ScaffoldResult, create_project # noqa: E402
18
+ from .templates import TEMPLATES, ProjectTemplate, get_template # noqa: E402
19
+
20
+ __all__ = [
21
+ "__version__",
22
+ "ConfigError",
23
+ "DevConfig",
24
+ "DevServer",
25
+ "ProjectConfig",
26
+ "ProjectTemplate",
27
+ "ScaffoldError",
28
+ "ScaffoldResult",
29
+ "TEMPLATES",
30
+ "build_command",
31
+ "create_project",
32
+ "diff_snapshots",
33
+ "find_project_root",
34
+ "get_template",
35
+ "load_config",
36
+ "take_snapshot",
37
+ ]
@@ -0,0 +1,7 @@
1
+ """Allow ``python -m avalon_cli``."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,163 @@
1
+ """The ``avalon`` command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import platform
8
+ import shlex
9
+ import signal
10
+ import sys
11
+ import threading
12
+ from collections.abc import Sequence
13
+ from typing import TextIO
14
+
15
+ from . import __version__
16
+ from .config import ConfigError, load_config
17
+ from .reloader import DevServer, build_command
18
+ from .scaffold import ScaffoldError, create_project
19
+ from .templates import DEFAULT_TEMPLATE, TEMPLATES
20
+
21
+ __all__ = ["build_parser", "main"]
22
+
23
+
24
+ class CLIError(Exception):
25
+ """An error reported to the user as ``error: <message>`` with exit code 1."""
26
+
27
+
28
+ def _cmd_new(args: argparse.Namespace, out: TextIO) -> int:
29
+ result = create_project(args.name, args.template, args.directory, force=args.force)
30
+ out.write(f"Created {result.template!r} project in {result.root}\n")
31
+ for path in result.files:
32
+ out.write(f" {path.relative_to(result.root).as_posix()}\n")
33
+ out.write(f"\nNext steps:\n cd {shlex.quote(str(result.root))}\n avalon dev\n")
34
+ return 0
35
+
36
+
37
+ def _cmd_templates(args: argparse.Namespace, out: TextIO) -> int:
38
+ width = max(len(name) for name in TEMPLATES)
39
+ for name in sorted(TEMPLATES):
40
+ marker = " (default)" if name == DEFAULT_TEMPLATE else ""
41
+ out.write(f"{name:<{width}} {TEMPLATES[name].description}{marker}\n")
42
+ return 0
43
+
44
+
45
+ def _cmd_info(args: argparse.Namespace, out: TextIO) -> int:
46
+ out.write(f"avalon-cli {__version__}\n")
47
+ out.write(f"python {platform.python_version()} ({sys.executable})\n")
48
+ try:
49
+ config = load_config(args.project)
50
+ except ConfigError as exc:
51
+ out.write(f"project (none: {exc})\n")
52
+ return 0
53
+ dev = config.dev
54
+ argv = build_command(dev.command, host=dev.host, port=dev.port)
55
+ out.write(f"project {config.name}\n")
56
+ out.write(f"root {config.root}\n")
57
+ out.write(f"command {shlex.join(argv)}\n")
58
+ out.write(f"address http://{dev.host}:{dev.port}/\n")
59
+ out.write(f"watch {', '.join(dev.watch)}\n")
60
+ out.write(f"extensions {', '.join(dev.extensions) or '(all files)'}\n")
61
+ return 0
62
+
63
+
64
+ def _cmd_dev(args: argparse.Namespace, out: TextIO) -> int:
65
+ config = load_config(args.project)
66
+ dev = config.dev
67
+ host = dev.host if args.host is None else args.host
68
+ port = dev.port if args.port is None else args.port
69
+ interval = dev.interval if args.interval is None else args.interval
70
+ if not 0 < port < 65536:
71
+ raise CLIError(f"port must be between 1 and 65535 (got {port})")
72
+ if interval <= 0:
73
+ raise CLIError("interval must be positive")
74
+ argv = build_command(dev.command, host=host, port=port)
75
+
76
+ env = dict(os.environ)
77
+ env.update(AVALON_HOST=host, AVALON_PORT=str(port), AVALON_ENV="development")
78
+ server = DevServer(
79
+ argv,
80
+ cwd=config.root,
81
+ env=env,
82
+ watch=dev.watch,
83
+ extensions=dev.extensions,
84
+ ignore=dev.ignore,
85
+ interval=interval,
86
+ reload=not args.no_reload,
87
+ )
88
+ out.write(f"Serving {config.name} at http://{host}:{port}/ (Ctrl-C to stop)\n")
89
+ out.flush()
90
+
91
+ # Treat SIGTERM like a clean shutdown so the child is never orphaned.
92
+ stop_event = threading.Event()
93
+ previous = None
94
+ if threading.current_thread() is threading.main_thread():
95
+ previous = signal.signal(signal.SIGTERM, lambda signum, frame: stop_event.set())
96
+ try:
97
+ return server.run(stop_event)
98
+ except KeyboardInterrupt:
99
+ out.write("\nStopped.\n")
100
+ return 130
101
+ finally:
102
+ if previous is not None:
103
+ signal.signal(signal.SIGTERM, previous)
104
+
105
+
106
+ def build_parser() -> argparse.ArgumentParser:
107
+ """Construct the argument parser for the ``avalon`` command."""
108
+ parser = argparse.ArgumentParser(
109
+ prog="avalon",
110
+ description="Command-line companion for the Avalon real-time web framework.",
111
+ )
112
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
113
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND", required=True)
114
+
115
+ new = sub.add_parser("new", help="create a new project from a template")
116
+ new.add_argument("name", help="project name (also the directory name)")
117
+ new.add_argument(
118
+ "-t", "--template", default=DEFAULT_TEMPLATE, choices=sorted(TEMPLATES),
119
+ help=f"template to use (default: {DEFAULT_TEMPLATE})",
120
+ )
121
+ new.add_argument(
122
+ "-d", "--directory", default=".",
123
+ help="parent directory to create the project in (default: current directory)",
124
+ )
125
+ new.add_argument("--force", action="store_true", help="write into a non-empty directory")
126
+ new.set_defaults(handler=_cmd_new)
127
+
128
+ dev = sub.add_parser("dev", help="run the dev command with auto-reload")
129
+ dev.add_argument("-C", "--project", default=".", help="project directory (default: current directory)")
130
+ dev.add_argument("--host", help="host to bind (overrides avalon.toml)")
131
+ dev.add_argument("-p", "--port", type=int, help="port to bind (overrides avalon.toml)")
132
+ dev.add_argument("--interval", type=float, help="seconds between file-change polls")
133
+ dev.add_argument("--no-reload", action="store_true", help="run once without watching files")
134
+ dev.set_defaults(handler=_cmd_dev)
135
+
136
+ templates = sub.add_parser("templates", help="list available project templates")
137
+ templates.set_defaults(handler=_cmd_templates)
138
+
139
+ info = sub.add_parser("info", help="show environment and resolved project settings")
140
+ info.add_argument("-C", "--project", default=".", help="project directory (default: current directory)")
141
+ info.set_defaults(handler=_cmd_info)
142
+
143
+ return parser
144
+
145
+
146
+ def main(argv: Sequence[str] | None = None, out: TextIO | None = None) -> int:
147
+ """Entry point for the ``avalon`` console script.
148
+
149
+ :param argv: arguments excluding the program name (defaults to ``sys.argv[1:]``).
150
+ :param out: stream for normal output (defaults to ``sys.stdout``).
151
+ :returns: process exit code. Errors print ``error: ...`` to stderr and return 1.
152
+ """
153
+ out = out or sys.stdout
154
+ args = build_parser().parse_args(argv)
155
+ try:
156
+ return args.handler(args, out)
157
+ except (CLIError, ConfigError, ScaffoldError, ValueError) as exc:
158
+ print(f"error: {exc}", file=sys.stderr)
159
+ return 1
160
+
161
+
162
+ if __name__ == "__main__": # pragma: no cover
163
+ sys.exit(main())