milo-cli 0.2.1__tar.gz → 0.3.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.
- {milo_cli-0.2.1 → milo_cli-0.3.0}/PKG-INFO +88 -34
- {milo_cli-0.2.1 → milo_cli-0.3.0}/README.md +85 -31
- {milo_cli-0.2.1 → milo_cli-0.3.0}/pyproject.toml +16 -5
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/__init__.py +15 -1
- milo_cli-0.3.0/src/milo/_cells.py +166 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_child.py +31 -12
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_command_defs.py +29 -6
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_compat.py +1 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_errors.py +14 -2
- milo_cli-0.3.0/src/milo/_jsonrpc.py +73 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_mcp_router.py +5 -1
- milo_cli-0.3.0/src/milo/_scaffold/__init__.py +55 -0
- milo_cli-0.3.0/src/milo/_scaffold/default/README.md +115 -0
- milo_cli-0.3.0/src/milo/_scaffold/default/app.py +29 -0
- milo_cli-0.3.0/src/milo/_scaffold/default/conftest.py +8 -0
- milo_cli-0.3.0/src/milo/_scaffold/default/tests/test_app.py +76 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_types.py +10 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/app.py +54 -11
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/cli.py +100 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/commands.py +349 -180
- milo_cli-0.3.0/src/milo/components_cli.py +111 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/config.py +30 -5
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/context.py +37 -3
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/dev.py +1 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/flow.py +16 -4
- milo_cli-0.3.0/src/milo/form.py +423 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/gateway.py +71 -19
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/groups.py +31 -3
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/help.py +18 -2
- milo_cli-0.3.0/src/milo/live.py +15 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/llms.py +8 -3
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/mcp.py +185 -20
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/middleware.py +2 -2
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/output.py +19 -5
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/pipeline.py +458 -11
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/plugins.py +22 -5
- milo_cli-0.3.0/src/milo/py.typed +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/registry.py +19 -5
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/schema.py +105 -16
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/state.py +127 -24
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/__init__.py +51 -3
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/components/_defs.kida +93 -3
- milo_cli-0.3.0/src/milo/templates/progress.kida +3 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/testing/_record.py +6 -1
- milo_cli-0.3.0/src/milo/verify.py +465 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/version_check.py +20 -13
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo_cli.egg-info/PKG-INFO +88 -34
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo_cli.egg-info/SOURCES.txt +23 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo_cli.egg-info/requires.txt +2 -2
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_ai_native.py +150 -5
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_app.py +17 -3
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_child.py +17 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_cli.py +63 -0
- milo_cli-0.3.0/tests/test_command_contract.py +173 -0
- milo_cli-0.3.0/tests/test_command_defs.py +294 -0
- milo_cli-0.3.0/tests/test_commands_core.py +470 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_components.py +181 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_config.py +82 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_context.py +32 -0
- milo_cli-0.3.0/tests/test_docs_information_architecture.py +64 -0
- milo_cli-0.3.0/tests/test_docs_snippets.py +90 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_effects.py +16 -7
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_effects_stress.py +1 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_flow.py +24 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_form.py +186 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_gateway.py +78 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_help.py +17 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_input.py +14 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_lazy.py +73 -7
- milo_cli-0.3.0/tests/test_mcp_handler.py +432 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_mcp_resources.py +62 -2
- milo_cli-0.3.0/tests/test_mcp_transport.py +37 -0
- milo_cli-0.3.0/tests/test_migration_docs.py +50 -0
- milo_cli-0.3.0/tests/test_outputgallery_example.py +258 -0
- milo_cli-0.3.0/tests/test_pipeline.py +1318 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_plugins.py +47 -0
- milo_cli-0.3.0/tests/test_readme_example_index.py +41 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_reducers.py +2 -2
- milo_cli-0.3.0/tests/test_registry_crud.py +122 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_registry_v2.py +29 -1
- milo_cli-0.3.0/tests/test_scaffold.py +144 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_schema_v2.py +134 -9
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_state.py +149 -1
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_templates.py +77 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_testing.py +14 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_types.py +2 -1
- milo_cli-0.3.0/tests/test_verify.py +241 -0
- milo_cli-0.2.1/src/milo/_jsonrpc.py +0 -37
- milo_cli-0.2.1/src/milo/form.py +0 -278
- milo_cli-0.2.1/src/milo/templates/progress.kida +0 -1
- milo_cli-0.2.1/tests/test_pipeline.py +0 -614
- {milo_cli-0.2.1 → milo_cli-0.3.0}/setup.cfg +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_cli_help.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/_protocols.py +0 -0
- /milo_cli-0.2.1/src/milo/py.typed → /milo_cli-0.3.0/src/milo/_scaffold/default/tests/__init__.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/completions.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/doctor.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/input/__init__.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/input/_platform.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/input/_reader.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/input/_sequences.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/observability.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/reducers.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/streaming.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/components/command_list.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/components/help_page.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/error.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/field_confirm.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/field_select.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/field_text.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/form.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/templates/help.kida +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/testing/__init__.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/testing/_mcp.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/testing/_replay.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/testing/_snapshot.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo/theme.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo_cli.egg-info/dependency_links.txt +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo_cli.egg-info/entry_points.txt +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/src/milo_cli.egg-info/top_level.txt +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_bubbletea_patterns.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_commands_middleware.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_compat.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_completions.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_dev.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_doctor.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_errors.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_groups.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_mcp_prompts.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_mcp_router.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_middleware.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_milo_init.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_mount.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_observability.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_protocols.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_streaming.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_testing_mcp.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_theme.py +0 -0
- {milo_cli-0.2.1 → milo_cli-0.3.0}/tests/test_version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: milo-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Template-driven CLI applications for free-threaded Python
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://lbliii.github.io/milo-cli/
|
|
@@ -18,9 +18,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: kida-templates
|
|
21
|
+
Requires-Dist: kida-templates<0.8.0,>=0.7.0
|
|
22
22
|
Provides-Extra: docs
|
|
23
|
-
Requires-Dist: bengal>=0.2
|
|
23
|
+
Requires-Dist: bengal>=0.3.2; extra == "docs"
|
|
24
24
|
Provides-Extra: yaml
|
|
25
25
|
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
26
26
|
Provides-Extra: watch
|
|
@@ -92,18 +92,22 @@ Milo is a Python framework where every CLI is simultaneously a terminal app, a c
|
|
|
92
92
|
|
|
93
93
|
## Installation
|
|
94
94
|
|
|
95
|
+
**Requires Python 3.14+.** If you don't have it: `uv python install 3.14`.
|
|
96
|
+
|
|
95
97
|
```bash
|
|
96
98
|
pip install milo-cli
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
The PyPI package is **milo-cli**; import the **`milo`** namespace in Python. The `milo` console command is installed with the package.
|
|
100
102
|
|
|
101
|
-
Requires Python 3.14+
|
|
102
|
-
|
|
103
103
|
---
|
|
104
104
|
|
|
105
105
|
## Quick Start
|
|
106
106
|
|
|
107
|
+
> **Coding agents**: jump to [`docs/agent-quickstart.md`](./docs/agent-quickstart.md)
|
|
108
|
+
> for a 5-minute walkthrough from `@cli.command` to a verified Claude MCP tool call.
|
|
109
|
+
> See also [`docs/testing.md`](./docs/testing.md) for the test template.
|
|
110
|
+
|
|
107
111
|
### AI-Native CLI
|
|
108
112
|
|
|
109
113
|
| Function | Description |
|
|
@@ -140,30 +144,79 @@ Requires Python 3.14+
|
|
|
140
144
|
|
|
141
145
|
| Feature | Description | Docs |
|
|
142
146
|
|---------|-------------|------|
|
|
143
|
-
| **MCP Server** | Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
144
|
-
| **MCP Gateway** | Single gateway aggregates all registered Milo CLIs for unified AI agent access | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
145
|
-
| **Tool Annotations** | Declare `readOnlyHint`, `destructiveHint`, `idempotentHint` per MCP spec | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
146
|
-
| **Streaming Progress** | Commands yield `Progress` objects; MCP clients receive real-time notifications | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
147
|
-
| **Schema Constraints** | `Annotated[str, MinLen(1), MaxLen(100)]` generates rich JSON Schema | [CLI →](https://lbliii.github.io/milo-cli/docs/
|
|
148
|
-
| **llms.txt** | Generate AI-readable discovery documents from CLI command definitions | [llms.txt →](https://lbliii.github.io/milo-cli/docs/
|
|
149
|
-
| **Middleware** | Intercept MCP calls and CLI commands for logging, auth, and transformation | [CLI →](https://lbliii.github.io/milo-cli/docs/
|
|
150
|
-
| **Observability** | Built-in request logging with latency stats (`milo://stats` resource) | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
151
|
-
| **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo-cli/docs/
|
|
152
|
-
| **Commands** | Lightweight `Cmd` thunks, `Batch`, `Sequence`, `TickCmd` for one-shot effects | [Commands →](https://lbliii.github.io/milo-cli/docs/
|
|
153
|
-
| **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry` | [Sagas →](https://lbliii.github.io/milo-cli/docs/
|
|
154
|
-
| **ViewState** | Declarative terminal state (`cursor_visible`, `alt_screen`, `window_title`, `mouse_mode`) | [Commands →](https://lbliii.github.io/milo-cli/docs/
|
|
155
|
-
| **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo-cli/docs/
|
|
156
|
-
| **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo-cli/docs/
|
|
157
|
-
| **Input Handling** | Cross-platform key reader with
|
|
158
|
-
| **Templates** | Kida-powered terminal rendering with built-in form, field, help, and progress templates | [Templates →](https://lbliii.github.io/milo-cli/docs/
|
|
159
|
-
| **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo-cli/docs/
|
|
160
|
-
| **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo-cli/docs/
|
|
161
|
-
| **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo-cli/docs/
|
|
162
|
-
| **
|
|
163
|
-
| **
|
|
164
|
-
| **
|
|
165
|
-
| **
|
|
166
|
-
| **
|
|
147
|
+
| **MCP Server** | Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
148
|
+
| **MCP Gateway** | Single gateway aggregates all registered Milo CLIs for unified AI agent access | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
149
|
+
| **Tool Annotations** | Declare `readOnlyHint`, `destructiveHint`, `idempotentHint` per MCP spec | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
150
|
+
| **Streaming Progress** | Commands yield `Progress` objects; MCP clients receive real-time notifications | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
151
|
+
| **Schema Constraints** | `Annotated[str, MinLen(1), MaxLen(100)]` generates rich JSON Schema | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
152
|
+
| **llms.txt** | Generate AI-readable discovery documents from CLI command definitions | [llms.txt →](https://lbliii.github.io/milo-cli/docs/build-clis/llms/) |
|
|
153
|
+
| **Middleware** | Intercept MCP calls and CLI commands for logging, auth, and transformation | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
154
|
+
| **Observability** | Built-in request logging with latency stats (`milo://stats` resource) | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
155
|
+
| **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo-cli/docs/build-apps/state/) |
|
|
156
|
+
| **Commands** | Lightweight `Cmd` thunks, `Batch`, `Sequence`, `TickCmd` for one-shot effects | [Commands →](https://lbliii.github.io/milo-cli/docs/build-apps/commands-effects/) |
|
|
157
|
+
| **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Race`, `All`, `Take`, and more | [Sagas →](https://lbliii.github.io/milo-cli/docs/build-apps/sagas/) |
|
|
158
|
+
| **ViewState** | Declarative terminal state (`cursor_visible`, `alt_screen`, `window_title`, `mouse_mode`) | [Commands →](https://lbliii.github.io/milo-cli/docs/build-apps/commands-effects/) |
|
|
159
|
+
| **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo-cli/docs/build-apps/flows/) |
|
|
160
|
+
| **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo-cli/docs/build-apps/forms/) |
|
|
161
|
+
| **Input Handling** | Cross-platform key reader with VT100/xterm escape sequence support (arrows, F-keys, modifiers) | [Input →](https://lbliii.github.io/milo-cli/docs/build-apps/input/) |
|
|
162
|
+
| **Templates** | Kida-powered terminal rendering with built-in form, field, help, and progress templates | [Templates →](https://lbliii.github.io/milo-cli/docs/build-apps/templates/) |
|
|
163
|
+
| **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo-cli/docs/build-apps/dev/) |
|
|
164
|
+
| **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo-cli/docs/quality/testing/) |
|
|
165
|
+
| **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo-cli/docs/quality/testing/) |
|
|
166
|
+
| **Pipeline** | Declarative multi-phase workflows with dependency graphs, retry policies, and output capture | [Pipeline →](https://lbliii.github.io/milo-cli/docs/quality/pipeline/) |
|
|
167
|
+
| **Help Rendering** | `HelpRenderer` — drop-in `argparse.HelpFormatter` using Kida templates | [Help →](https://lbliii.github.io/milo-cli/docs/build-clis/help/) |
|
|
168
|
+
| **Context** | Execution context with verbosity, output format, global options, and `run_app()` bridge | [Context →](https://lbliii.github.io/milo-cli/docs/build-clis/context/) |
|
|
169
|
+
| **Configuration** | `Config` with validation, init scaffolding, and profile support | [Config →](https://lbliii.github.io/milo-cli/docs/about/concepts/configuration/) |
|
|
170
|
+
| **Shell Completions** | Generate bash/zsh/fish completions from CLI definitions | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
171
|
+
| **Doctor Diagnostics** | `run_doctor()` validates environment, dependencies, and config health | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Examples Index
|
|
176
|
+
|
|
177
|
+
Pick the example closest to your use case, copy its `app.py`, and adapt. See [examples/README.md](examples/README.md) for run commands, copy paths, and tested starting points.
|
|
178
|
+
|
|
179
|
+
**CLIs (typed function → CLI + MCP + llms.txt)**
|
|
180
|
+
|
|
181
|
+
| What you want to build | Example | Key APIs |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| The simplest possible CLI | [examples/greet](examples/greet) | `CLI`, `@cli.command` |
|
|
184
|
+
| Dual-mode CLI ↔ MCP server (flagship) | [examples/deploy](examples/deploy) | `Annotated`, `MinLen`, `Context`, `Progress`, `--mcp` |
|
|
185
|
+
| Context injection, logging, progress, confirms | [examples/ctxdemo](examples/ctxdemo) | `Context`, `ctx.info`, `ctx.progress`, `ctx.confirm` |
|
|
186
|
+
| Nested command groups (`app repo list`) | [examples/groups](examples/groups) | `cli.group()`, `walk_commands` |
|
|
187
|
+
| Fast startup via deferred imports | [examples/lazyapp](examples/lazyapp) | `cli.lazy_command()` |
|
|
188
|
+
| Production CLI with hooks, completions, doctor | [examples/devtool](examples/devtool) | `run_doctor`, `before_run`/`after_run`, did-you-mean, completions |
|
|
189
|
+
| AI-native CLI surfacing tools + resources | [examples/taskman](examples/taskman) | `@command`, `@resource`, `--format`, `--llms-txt`, `--mcp` |
|
|
190
|
+
| Advanced terminal reports and diagnostics | [examples/outputgallery](examples/outputgallery) | `Context.render`, Kida templates, character maps, JSON output |
|
|
191
|
+
|
|
192
|
+
**Configuration, plugins, pipelines**
|
|
193
|
+
|
|
194
|
+
| What you want to build | Example | Key APIs |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| TOML config with profiles + overlays | [examples/configapp](examples/configapp) | `Config`, `ConfigSpec`, `Config.load`, `Config.validate` |
|
|
197
|
+
| Plugin system with hooks + listeners | [examples/pluggable](examples/pluggable) | `HookRegistry`, `define`, `on`, `invoke` |
|
|
198
|
+
| Multi-phase pipeline with deps + retries | [examples/buildpipe](examples/buildpipe) | `Pipeline`, `Phase`, `PhasePolicy`, `>>` |
|
|
199
|
+
|
|
200
|
+
**Interactive TUIs (`App` + reducer)**
|
|
201
|
+
|
|
202
|
+
| What you want to build | Example | Key APIs |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| The simplest TUI | [examples/counter](examples/counter) | `App.from_dir`, reducer combinators |
|
|
205
|
+
| Modal input with derived filtering | [examples/todo](examples/todo) | tuple state, `quit_on`, derived views |
|
|
206
|
+
| Tick-driven animation | [examples/stopwatch](examples/stopwatch) | `tick_rate`, `@@TICK`, `quit_on` |
|
|
207
|
+
| Scrollable viewport with saga I/O | [examples/filepicker](examples/filepicker) | viewport, sagas, frozen tuples |
|
|
208
|
+
| Multi-screen flow with forms | [examples/wizard](examples/wizard) | `Flow`, `FlowScreen`, `make_form_reducer`, `FieldSpec` |
|
|
209
|
+
|
|
210
|
+
**Async work (sagas + Cmd pattern)**
|
|
211
|
+
|
|
212
|
+
| What you want to build | Example | Key APIs |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| Sagas for async side effects | [examples/fetcher](examples/fetcher) | `Call`, `Put`, `Select`, `Retry` |
|
|
215
|
+
| Parallel concurrent work | [examples/downloader](examples/downloader) | `Fork`, `Call`, `Delay`, `Timeout` |
|
|
216
|
+
| Bubbletea-style Cmd thunks | [examples/spinner](examples/spinner) | `Cmd`, `Batch`, `TickCmd`, `ViewState` |
|
|
217
|
+
| Live rendering outside an App | [examples/liverender](examples/liverender) | `milo.live.LiveRenderer`, `Spinner`, `terminal_env` |
|
|
218
|
+
|
|
219
|
+
> Don't see your use case? Run `milo new <name>` to scaffold a fresh CLI with tests, then `milo verify app.py` to confirm it works.
|
|
167
220
|
|
|
168
221
|
---
|
|
169
222
|
|
|
@@ -343,7 +396,7 @@ def reducer(state, action):
|
|
|
343
396
|
return state
|
|
344
397
|
```
|
|
345
398
|
|
|
346
|
-
Saga effects: `Call
|
|
399
|
+
Saga effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Timeout`, `TryCall`, `Race`, `All`, `Take`, `Debounce`, `TakeEvery`, `TakeLatest`.
|
|
347
400
|
|
|
348
401
|
For one-shot effects, use `Cmd` instead — no generator needed:
|
|
349
402
|
|
|
@@ -455,11 +508,12 @@ App.run()
|
|
|
455
508
|
|
|
456
509
|
| Section | Description |
|
|
457
510
|
|---------|-------------|
|
|
511
|
+
| [About](https://lbliii.github.io/milo-cli/docs/about/) | Philosophy, architecture, concepts, and lifecycle |
|
|
458
512
|
| [Get Started](https://lbliii.github.io/milo-cli/docs/get-started/) | Installation and quickstart |
|
|
459
|
-
| [
|
|
460
|
-
| [
|
|
461
|
-
| [
|
|
462
|
-
| [Reference](https://lbliii.github.io/milo-cli/docs/reference/) |
|
|
513
|
+
| [Build CLIs](https://lbliii.github.io/milo-cli/docs/build-clis/) | Commands, groups, MCP, llms.txt, context, output, and help |
|
|
514
|
+
| [Build Apps](https://lbliii.github.io/milo-cli/docs/build-apps/) | State, reducers, templates, input, forms, flows, sagas, and live rendering |
|
|
515
|
+
| [Quality](https://lbliii.github.io/milo-cli/docs/quality/) | Testing, verification, debugging, and pipelines |
|
|
516
|
+
| [Reference](https://lbliii.github.io/milo-cli/docs/reference/) | Schema, dispatch, error codes, actions, and types |
|
|
463
517
|
|
|
464
518
|
---
|
|
465
519
|
|
|
@@ -64,18 +64,22 @@ Milo is a Python framework where every CLI is simultaneously a terminal app, a c
|
|
|
64
64
|
|
|
65
65
|
## Installation
|
|
66
66
|
|
|
67
|
+
**Requires Python 3.14+.** If you don't have it: `uv python install 3.14`.
|
|
68
|
+
|
|
67
69
|
```bash
|
|
68
70
|
pip install milo-cli
|
|
69
71
|
```
|
|
70
72
|
|
|
71
73
|
The PyPI package is **milo-cli**; import the **`milo`** namespace in Python. The `milo` console command is installed with the package.
|
|
72
74
|
|
|
73
|
-
Requires Python 3.14+
|
|
74
|
-
|
|
75
75
|
---
|
|
76
76
|
|
|
77
77
|
## Quick Start
|
|
78
78
|
|
|
79
|
+
> **Coding agents**: jump to [`docs/agent-quickstart.md`](./docs/agent-quickstart.md)
|
|
80
|
+
> for a 5-minute walkthrough from `@cli.command` to a verified Claude MCP tool call.
|
|
81
|
+
> See also [`docs/testing.md`](./docs/testing.md) for the test template.
|
|
82
|
+
|
|
79
83
|
### AI-Native CLI
|
|
80
84
|
|
|
81
85
|
| Function | Description |
|
|
@@ -112,30 +116,79 @@ Requires Python 3.14+
|
|
|
112
116
|
|
|
113
117
|
| Feature | Description | Docs |
|
|
114
118
|
|---------|-------------|------|
|
|
115
|
-
| **MCP Server** | Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
116
|
-
| **MCP Gateway** | Single gateway aggregates all registered Milo CLIs for unified AI agent access | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
117
|
-
| **Tool Annotations** | Declare `readOnlyHint`, `destructiveHint`, `idempotentHint` per MCP spec | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
118
|
-
| **Streaming Progress** | Commands yield `Progress` objects; MCP clients receive real-time notifications | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
119
|
-
| **Schema Constraints** | `Annotated[str, MinLen(1), MaxLen(100)]` generates rich JSON Schema | [CLI →](https://lbliii.github.io/milo-cli/docs/
|
|
120
|
-
| **llms.txt** | Generate AI-readable discovery documents from CLI command definitions | [llms.txt →](https://lbliii.github.io/milo-cli/docs/
|
|
121
|
-
| **Middleware** | Intercept MCP calls and CLI commands for logging, auth, and transformation | [CLI →](https://lbliii.github.io/milo-cli/docs/
|
|
122
|
-
| **Observability** | Built-in request logging with latency stats (`milo://stats` resource) | [MCP →](https://lbliii.github.io/milo-cli/docs/
|
|
123
|
-
| **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo-cli/docs/
|
|
124
|
-
| **Commands** | Lightweight `Cmd` thunks, `Batch`, `Sequence`, `TickCmd` for one-shot effects | [Commands →](https://lbliii.github.io/milo-cli/docs/
|
|
125
|
-
| **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry` | [Sagas →](https://lbliii.github.io/milo-cli/docs/
|
|
126
|
-
| **ViewState** | Declarative terminal state (`cursor_visible`, `alt_screen`, `window_title`, `mouse_mode`) | [Commands →](https://lbliii.github.io/milo-cli/docs/
|
|
127
|
-
| **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo-cli/docs/
|
|
128
|
-
| **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo-cli/docs/
|
|
129
|
-
| **Input Handling** | Cross-platform key reader with
|
|
130
|
-
| **Templates** | Kida-powered terminal rendering with built-in form, field, help, and progress templates | [Templates →](https://lbliii.github.io/milo-cli/docs/
|
|
131
|
-
| **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo-cli/docs/
|
|
132
|
-
| **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo-cli/docs/
|
|
133
|
-
| **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo-cli/docs/
|
|
134
|
-
| **
|
|
135
|
-
| **
|
|
136
|
-
| **
|
|
137
|
-
| **
|
|
138
|
-
| **
|
|
119
|
+
| **MCP Server** | Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
120
|
+
| **MCP Gateway** | Single gateway aggregates all registered Milo CLIs for unified AI agent access | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
121
|
+
| **Tool Annotations** | Declare `readOnlyHint`, `destructiveHint`, `idempotentHint` per MCP spec | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
122
|
+
| **Streaming Progress** | Commands yield `Progress` objects; MCP clients receive real-time notifications | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
123
|
+
| **Schema Constraints** | `Annotated[str, MinLen(1), MaxLen(100)]` generates rich JSON Schema | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
124
|
+
| **llms.txt** | Generate AI-readable discovery documents from CLI command definitions | [llms.txt →](https://lbliii.github.io/milo-cli/docs/build-clis/llms/) |
|
|
125
|
+
| **Middleware** | Intercept MCP calls and CLI commands for logging, auth, and transformation | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
126
|
+
| **Observability** | Built-in request logging with latency stats (`milo://stats` resource) | [MCP →](https://lbliii.github.io/milo-cli/docs/build-clis/mcp/) |
|
|
127
|
+
| **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo-cli/docs/build-apps/state/) |
|
|
128
|
+
| **Commands** | Lightweight `Cmd` thunks, `Batch`, `Sequence`, `TickCmd` for one-shot effects | [Commands →](https://lbliii.github.io/milo-cli/docs/build-apps/commands-effects/) |
|
|
129
|
+
| **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Race`, `All`, `Take`, and more | [Sagas →](https://lbliii.github.io/milo-cli/docs/build-apps/sagas/) |
|
|
130
|
+
| **ViewState** | Declarative terminal state (`cursor_visible`, `alt_screen`, `window_title`, `mouse_mode`) | [Commands →](https://lbliii.github.io/milo-cli/docs/build-apps/commands-effects/) |
|
|
131
|
+
| **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo-cli/docs/build-apps/flows/) |
|
|
132
|
+
| **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo-cli/docs/build-apps/forms/) |
|
|
133
|
+
| **Input Handling** | Cross-platform key reader with VT100/xterm escape sequence support (arrows, F-keys, modifiers) | [Input →](https://lbliii.github.io/milo-cli/docs/build-apps/input/) |
|
|
134
|
+
| **Templates** | Kida-powered terminal rendering with built-in form, field, help, and progress templates | [Templates →](https://lbliii.github.io/milo-cli/docs/build-apps/templates/) |
|
|
135
|
+
| **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo-cli/docs/build-apps/dev/) |
|
|
136
|
+
| **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo-cli/docs/quality/testing/) |
|
|
137
|
+
| **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo-cli/docs/quality/testing/) |
|
|
138
|
+
| **Pipeline** | Declarative multi-phase workflows with dependency graphs, retry policies, and output capture | [Pipeline →](https://lbliii.github.io/milo-cli/docs/quality/pipeline/) |
|
|
139
|
+
| **Help Rendering** | `HelpRenderer` — drop-in `argparse.HelpFormatter` using Kida templates | [Help →](https://lbliii.github.io/milo-cli/docs/build-clis/help/) |
|
|
140
|
+
| **Context** | Execution context with verbosity, output format, global options, and `run_app()` bridge | [Context →](https://lbliii.github.io/milo-cli/docs/build-clis/context/) |
|
|
141
|
+
| **Configuration** | `Config` with validation, init scaffolding, and profile support | [Config →](https://lbliii.github.io/milo-cli/docs/about/concepts/configuration/) |
|
|
142
|
+
| **Shell Completions** | Generate bash/zsh/fish completions from CLI definitions | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
143
|
+
| **Doctor Diagnostics** | `run_doctor()` validates environment, dependencies, and config health | [CLI →](https://lbliii.github.io/milo-cli/docs/build-clis/commands/) |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Examples Index
|
|
148
|
+
|
|
149
|
+
Pick the example closest to your use case, copy its `app.py`, and adapt. See [examples/README.md](examples/README.md) for run commands, copy paths, and tested starting points.
|
|
150
|
+
|
|
151
|
+
**CLIs (typed function → CLI + MCP + llms.txt)**
|
|
152
|
+
|
|
153
|
+
| What you want to build | Example | Key APIs |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| The simplest possible CLI | [examples/greet](examples/greet) | `CLI`, `@cli.command` |
|
|
156
|
+
| Dual-mode CLI ↔ MCP server (flagship) | [examples/deploy](examples/deploy) | `Annotated`, `MinLen`, `Context`, `Progress`, `--mcp` |
|
|
157
|
+
| Context injection, logging, progress, confirms | [examples/ctxdemo](examples/ctxdemo) | `Context`, `ctx.info`, `ctx.progress`, `ctx.confirm` |
|
|
158
|
+
| Nested command groups (`app repo list`) | [examples/groups](examples/groups) | `cli.group()`, `walk_commands` |
|
|
159
|
+
| Fast startup via deferred imports | [examples/lazyapp](examples/lazyapp) | `cli.lazy_command()` |
|
|
160
|
+
| Production CLI with hooks, completions, doctor | [examples/devtool](examples/devtool) | `run_doctor`, `before_run`/`after_run`, did-you-mean, completions |
|
|
161
|
+
| AI-native CLI surfacing tools + resources | [examples/taskman](examples/taskman) | `@command`, `@resource`, `--format`, `--llms-txt`, `--mcp` |
|
|
162
|
+
| Advanced terminal reports and diagnostics | [examples/outputgallery](examples/outputgallery) | `Context.render`, Kida templates, character maps, JSON output |
|
|
163
|
+
|
|
164
|
+
**Configuration, plugins, pipelines**
|
|
165
|
+
|
|
166
|
+
| What you want to build | Example | Key APIs |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| TOML config with profiles + overlays | [examples/configapp](examples/configapp) | `Config`, `ConfigSpec`, `Config.load`, `Config.validate` |
|
|
169
|
+
| Plugin system with hooks + listeners | [examples/pluggable](examples/pluggable) | `HookRegistry`, `define`, `on`, `invoke` |
|
|
170
|
+
| Multi-phase pipeline with deps + retries | [examples/buildpipe](examples/buildpipe) | `Pipeline`, `Phase`, `PhasePolicy`, `>>` |
|
|
171
|
+
|
|
172
|
+
**Interactive TUIs (`App` + reducer)**
|
|
173
|
+
|
|
174
|
+
| What you want to build | Example | Key APIs |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| The simplest TUI | [examples/counter](examples/counter) | `App.from_dir`, reducer combinators |
|
|
177
|
+
| Modal input with derived filtering | [examples/todo](examples/todo) | tuple state, `quit_on`, derived views |
|
|
178
|
+
| Tick-driven animation | [examples/stopwatch](examples/stopwatch) | `tick_rate`, `@@TICK`, `quit_on` |
|
|
179
|
+
| Scrollable viewport with saga I/O | [examples/filepicker](examples/filepicker) | viewport, sagas, frozen tuples |
|
|
180
|
+
| Multi-screen flow with forms | [examples/wizard](examples/wizard) | `Flow`, `FlowScreen`, `make_form_reducer`, `FieldSpec` |
|
|
181
|
+
|
|
182
|
+
**Async work (sagas + Cmd pattern)**
|
|
183
|
+
|
|
184
|
+
| What you want to build | Example | Key APIs |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Sagas for async side effects | [examples/fetcher](examples/fetcher) | `Call`, `Put`, `Select`, `Retry` |
|
|
187
|
+
| Parallel concurrent work | [examples/downloader](examples/downloader) | `Fork`, `Call`, `Delay`, `Timeout` |
|
|
188
|
+
| Bubbletea-style Cmd thunks | [examples/spinner](examples/spinner) | `Cmd`, `Batch`, `TickCmd`, `ViewState` |
|
|
189
|
+
| Live rendering outside an App | [examples/liverender](examples/liverender) | `milo.live.LiveRenderer`, `Spinner`, `terminal_env` |
|
|
190
|
+
|
|
191
|
+
> Don't see your use case? Run `milo new <name>` to scaffold a fresh CLI with tests, then `milo verify app.py` to confirm it works.
|
|
139
192
|
|
|
140
193
|
---
|
|
141
194
|
|
|
@@ -315,7 +368,7 @@ def reducer(state, action):
|
|
|
315
368
|
return state
|
|
316
369
|
```
|
|
317
370
|
|
|
318
|
-
Saga effects: `Call
|
|
371
|
+
Saga effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Timeout`, `TryCall`, `Race`, `All`, `Take`, `Debounce`, `TakeEvery`, `TakeLatest`.
|
|
319
372
|
|
|
320
373
|
For one-shot effects, use `Cmd` instead — no generator needed:
|
|
321
374
|
|
|
@@ -427,11 +480,12 @@ App.run()
|
|
|
427
480
|
|
|
428
481
|
| Section | Description |
|
|
429
482
|
|---------|-------------|
|
|
483
|
+
| [About](https://lbliii.github.io/milo-cli/docs/about/) | Philosophy, architecture, concepts, and lifecycle |
|
|
430
484
|
| [Get Started](https://lbliii.github.io/milo-cli/docs/get-started/) | Installation and quickstart |
|
|
431
|
-
| [
|
|
432
|
-
| [
|
|
433
|
-
| [
|
|
434
|
-
| [Reference](https://lbliii.github.io/milo-cli/docs/reference/) |
|
|
485
|
+
| [Build CLIs](https://lbliii.github.io/milo-cli/docs/build-clis/) | Commands, groups, MCP, llms.txt, context, output, and help |
|
|
486
|
+
| [Build Apps](https://lbliii.github.io/milo-cli/docs/build-apps/) | State, reducers, templates, input, forms, flows, sagas, and live rendering |
|
|
487
|
+
| [Quality](https://lbliii.github.io/milo-cli/docs/quality/) | Testing, verification, debugging, and pipelines |
|
|
488
|
+
| [Reference](https://lbliii.github.io/milo-cli/docs/reference/) | Schema, dispatch, error codes, actions, and types |
|
|
435
489
|
|
|
436
490
|
---
|
|
437
491
|
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "milo-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Template-driven CLI applications for free-threaded Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
dependencies = ["kida-templates>=0.
|
|
12
|
+
dependencies = ["kida-templates>=0.7.0,<0.8.0"]
|
|
13
13
|
keywords = ["cli", "terminal", "forms", "free-threading", "template", "elm"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
milo = "milo.cli:main"
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
|
-
docs = ["bengal>=0.2
|
|
28
|
+
docs = ["bengal>=0.3.2"]
|
|
29
29
|
yaml = ["pyyaml>=6.0"]
|
|
30
30
|
watch = ["watchfiles>=1.0"]
|
|
31
31
|
|
|
@@ -40,7 +40,13 @@ Issues = "https://github.com/lbliii/milo-cli/issues"
|
|
|
40
40
|
where = ["src"]
|
|
41
41
|
|
|
42
42
|
[tool.setuptools.package-data]
|
|
43
|
-
milo = [
|
|
43
|
+
milo = [
|
|
44
|
+
"templates/*.kida",
|
|
45
|
+
"templates/components/*.kida",
|
|
46
|
+
"_scaffold/default/*",
|
|
47
|
+
"_scaffold/default/**/*",
|
|
48
|
+
"py.typed",
|
|
49
|
+
]
|
|
44
50
|
|
|
45
51
|
[tool.pytest.ini_options]
|
|
46
52
|
testpaths = ["tests"]
|
|
@@ -123,7 +129,9 @@ ignore = [
|
|
|
123
129
|
|
|
124
130
|
[tool.ruff.lint.per-file-ignores]
|
|
125
131
|
"__init__.py" = ["F401"]
|
|
126
|
-
"tests/**/*.py" = ["S101", "S108", "S110", "S604", "SIM117", "N806", "ARG"]
|
|
132
|
+
"tests/**/*.py" = ["S101", "S108", "S110", "S604", "SIM117", "N806", "ARG", "T201"]
|
|
133
|
+
"examples/**/*.py" = ["S101", "S108", "S110", "S604", "SIM117", "N806", "ARG", "T201", "E402"]
|
|
134
|
+
"src/milo/_scaffold/default/**/*.py" = ["S101", "I001", "F401", "ARG", "T201", "E402"]
|
|
127
135
|
"src/milo/_child.py" = ["S101"] # asserts for type narrowing on subprocess pipes
|
|
128
136
|
"src/milo/testing/*.py" = ["S101", "ARG"] # test helpers use assert and may have unused args
|
|
129
137
|
"src/milo/version_check.py" = ["S110", "S310"] # best-effort cache reads + known PyPI URL
|
|
@@ -187,3 +195,6 @@ dev = [
|
|
|
187
195
|
# Changelog
|
|
188
196
|
"towncrier>=24.0",
|
|
189
197
|
]
|
|
198
|
+
docs = [
|
|
199
|
+
"bengal>=0.3.2",
|
|
200
|
+
]
|
|
@@ -68,6 +68,7 @@ def __getattr__(name: str):
|
|
|
68
68
|
# Form
|
|
69
69
|
"form": "form",
|
|
70
70
|
"form_reducer": "form",
|
|
71
|
+
"form_schema": "form",
|
|
71
72
|
"make_form_reducer": "form",
|
|
72
73
|
# Help
|
|
73
74
|
"HelpRenderer": "help",
|
|
@@ -93,8 +94,14 @@ def __getattr__(name: str):
|
|
|
93
94
|
"Pipeline": "pipeline",
|
|
94
95
|
"Phase": "pipeline",
|
|
95
96
|
"PhasePolicy": "pipeline",
|
|
97
|
+
"PhaseLog": "pipeline",
|
|
96
98
|
"PipelineState": "pipeline",
|
|
97
99
|
"PhaseStatus": "pipeline",
|
|
100
|
+
"PipelineViewState": "pipeline",
|
|
101
|
+
"make_detail_reducer": "pipeline",
|
|
102
|
+
"pipeline_to_timeline": "pipeline",
|
|
103
|
+
"set_active_pipeline": "pipeline",
|
|
104
|
+
"get_active_pipeline": "pipeline",
|
|
98
105
|
"CycleError": "pipeline",
|
|
99
106
|
# Plugins
|
|
100
107
|
"HookRegistry": "plugins",
|
|
@@ -154,7 +161,7 @@ def _Py_mod_gil() -> int: # noqa: N802
|
|
|
154
161
|
return 0
|
|
155
162
|
|
|
156
163
|
|
|
157
|
-
__version__ = "0.
|
|
164
|
+
__version__ = "0.3.0"
|
|
158
165
|
__all__ = [
|
|
159
166
|
"BUILTIN_ACTIONS",
|
|
160
167
|
"CLI",
|
|
@@ -211,11 +218,13 @@ __all__ = [
|
|
|
211
218
|
"MinLen",
|
|
212
219
|
"Pattern",
|
|
213
220
|
"Phase",
|
|
221
|
+
"PhaseLog",
|
|
214
222
|
"PhasePolicy",
|
|
215
223
|
"PhaseStatus",
|
|
216
224
|
"Pipeline",
|
|
217
225
|
"PipelineError",
|
|
218
226
|
"PipelineState",
|
|
227
|
+
"PipelineViewState",
|
|
219
228
|
"PluginError",
|
|
220
229
|
"Progress",
|
|
221
230
|
"PromptDef",
|
|
@@ -248,20 +257,25 @@ __all__ = [
|
|
|
248
257
|
"compact_cmds",
|
|
249
258
|
"form",
|
|
250
259
|
"form_reducer",
|
|
260
|
+
"form_schema",
|
|
251
261
|
"format_doctor_report",
|
|
252
262
|
"format_error",
|
|
253
263
|
"format_output",
|
|
254
264
|
"format_render_error",
|
|
255
265
|
"function_to_schema",
|
|
256
266
|
"generate_llms_txt",
|
|
267
|
+
"get_active_pipeline",
|
|
257
268
|
"get_context",
|
|
258
269
|
"install_completions",
|
|
270
|
+
"make_detail_reducer",
|
|
259
271
|
"make_form_reducer",
|
|
260
272
|
"make_style_filter",
|
|
273
|
+
"pipeline_to_timeline",
|
|
261
274
|
"quit_on",
|
|
262
275
|
"render_html",
|
|
263
276
|
"run",
|
|
264
277
|
"run_doctor",
|
|
278
|
+
"set_active_pipeline",
|
|
265
279
|
"with_confirm",
|
|
266
280
|
"with_cursor",
|
|
267
281
|
"write_output",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Terminal display-cell width helpers.
|
|
2
|
+
|
|
3
|
+
Python string length counts code points, but terminals lay text out in display
|
|
4
|
+
cells. Box drawing, CJK characters, combining marks, and ANSI escapes all make
|
|
5
|
+
``len(text)`` the wrong primitive for fixed-width terminal UI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import unicodedata
|
|
12
|
+
|
|
13
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def strip_ansi(value: object) -> str:
|
|
17
|
+
"""Return *value* as text with ANSI SGR/control sequences removed."""
|
|
18
|
+
return _ANSI_RE.sub("", str(value))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cell_width(value: object) -> int:
|
|
22
|
+
"""Return the terminal display-cell width of *value*.
|
|
23
|
+
|
|
24
|
+
This intentionally implements the common wcwidth behavior we need without
|
|
25
|
+
adding a runtime dependency: combining/control characters are zero-width,
|
|
26
|
+
fullwidth/wide East Asian characters are two cells, and ambiguous-width
|
|
27
|
+
symbols are treated as one cell for Western terminal defaults.
|
|
28
|
+
"""
|
|
29
|
+
width = 0
|
|
30
|
+
for ch in strip_ansi(value):
|
|
31
|
+
category = unicodedata.category(ch)
|
|
32
|
+
if category.startswith("C") or unicodedata.combining(ch):
|
|
33
|
+
continue
|
|
34
|
+
width += 2 if unicodedata.east_asian_width(ch) in {"F", "W"} else 1
|
|
35
|
+
return width
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cell_ljust(value: object, width: int, fill: str = " ") -> str:
|
|
39
|
+
"""Left-justify *value* to display-cell *width*."""
|
|
40
|
+
text = str(value)
|
|
41
|
+
return text + fill * max(0, width - cell_width(text))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cell_rjust(value: object, width: int, fill: str = " ") -> str:
|
|
45
|
+
"""Right-justify *value* to display-cell *width*."""
|
|
46
|
+
text = str(value)
|
|
47
|
+
return fill * max(0, width - cell_width(text)) + text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cell_truncate(value: object, width: int, marker: str = "…") -> str:
|
|
51
|
+
"""Truncate *value* to display-cell *width*, preserving ANSI-free text."""
|
|
52
|
+
text = strip_ansi(value)
|
|
53
|
+
if cell_width(text) <= width:
|
|
54
|
+
return text
|
|
55
|
+
if width <= 0:
|
|
56
|
+
return ""
|
|
57
|
+
marker_width = cell_width(marker)
|
|
58
|
+
if width <= marker_width:
|
|
59
|
+
return marker[:width]
|
|
60
|
+
|
|
61
|
+
budget = width - marker_width
|
|
62
|
+
out: list[str] = []
|
|
63
|
+
used = 0
|
|
64
|
+
for ch in text:
|
|
65
|
+
ch_width = cell_width(ch)
|
|
66
|
+
if used + ch_width > budget:
|
|
67
|
+
break
|
|
68
|
+
out.append(ch)
|
|
69
|
+
used += ch_width
|
|
70
|
+
return "".join(out) + marker
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cell_fit(value: object, width: int, fill: str = " ", marker: str = "…") -> str:
|
|
74
|
+
"""Truncate then left-pad *value* so it occupies exactly *width* cells."""
|
|
75
|
+
return cell_ljust(cell_truncate(value, width, marker=marker), width, fill=fill)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cell_fill(width: int, fill: str = " ") -> str:
|
|
79
|
+
"""Return *fill* repeated/truncated to exactly *width* display cells."""
|
|
80
|
+
if width <= 0:
|
|
81
|
+
return ""
|
|
82
|
+
fill_width = cell_width(fill)
|
|
83
|
+
if fill_width <= 0:
|
|
84
|
+
return " " * width
|
|
85
|
+
repeated = str(fill) * ((width // fill_width) + 1)
|
|
86
|
+
return cell_truncate(repeated, width, marker="")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _rule_tail(width: int, fill: str = "─", tail: str = "╌┄") -> str:
|
|
90
|
+
"""Return a fixed-width rule that fades instead of ending abruptly."""
|
|
91
|
+
tail_width = cell_width(tail)
|
|
92
|
+
if width > tail_width + 8:
|
|
93
|
+
return cell_fill(width - tail_width, fill) + tail
|
|
94
|
+
return cell_fill(width, fill)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rule_line(
|
|
98
|
+
value: object = "",
|
|
99
|
+
width: int = 78,
|
|
100
|
+
left: str = "╭",
|
|
101
|
+
right: str = "╮",
|
|
102
|
+
fill: str = "─",
|
|
103
|
+
tail: str = "",
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Return a display-cell exact horizontal rule."""
|
|
106
|
+
label = str(value)
|
|
107
|
+
prefix = f"{left}─ {label} " if label else left
|
|
108
|
+
suffix_width = cell_width(right)
|
|
109
|
+
body_width = width - suffix_width
|
|
110
|
+
prefix_width = cell_width(prefix)
|
|
111
|
+
if body_width <= 0:
|
|
112
|
+
return cell_truncate(right, width, marker="")
|
|
113
|
+
if prefix_width > body_width:
|
|
114
|
+
return cell_fit(cell_truncate(prefix, body_width, marker="…"), body_width) + right
|
|
115
|
+
if tail:
|
|
116
|
+
return prefix + _rule_tail(body_width - prefix_width, fill=fill, tail=tail) + right
|
|
117
|
+
return prefix + cell_fill(body_width - prefix_width, fill) + right
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def divider_line(value: object = "", width: int = 78) -> str:
|
|
121
|
+
"""Return a display-cell exact closed-box divider."""
|
|
122
|
+
return rule_line(value, width=width, left="├", right="┤")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def bottom_rule(value: object = "", width: int = 78) -> str:
|
|
126
|
+
"""Return a display-cell exact closed-box bottom rule."""
|
|
127
|
+
return rule_line(value, width=width, left="╰", right="╯")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def frame_line(value: object = "", width: int = 78, left: str = "│", right: str = "│") -> str:
|
|
131
|
+
"""Return a display-cell exact framed content row."""
|
|
132
|
+
content_width = width - cell_width(left) - cell_width(right) - 2
|
|
133
|
+
if content_width <= 0:
|
|
134
|
+
return cell_truncate(left + right, width, marker="")
|
|
135
|
+
return f"{left} {cell_fit(value, content_width)} {right}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def rail_line(value: object = "", width: int = 78, rail: str = "│") -> str:
|
|
139
|
+
"""Return a display-cell exact open-card row with one left rail."""
|
|
140
|
+
content_width = width - cell_width(rail) - 1
|
|
141
|
+
if content_width <= 0:
|
|
142
|
+
return cell_truncate(rail, width, marker="")
|
|
143
|
+
return f"{rail} {cell_fit(value, content_width)}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def open_rule(value: object = "", width: int = 78, corner: str = "╭") -> str:
|
|
147
|
+
"""Return a cell-width exact open-card rule with a fading right edge."""
|
|
148
|
+
return rule_line(value, width=width, left=corner, right="", tail="╌┄")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def open_rule_divider(value: object = "", width: int = 78) -> str:
|
|
152
|
+
"""Return a cell-width exact open-card section divider."""
|
|
153
|
+
return open_rule(value, width=width, corner="├")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def open_rule_end(value: object = "", width: int = 78) -> str:
|
|
157
|
+
"""Return a cell-width exact open-card bottom rule."""
|
|
158
|
+
return open_rule(value, width=width, corner="╰")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cell_meter(value: int, total: int, width: int = 10, filled: str = "█", empty: str = "░") -> str:
|
|
162
|
+
"""Return a display-cell exact meter."""
|
|
163
|
+
if width <= 0:
|
|
164
|
+
return ""
|
|
165
|
+
fill_cells = 0 if total <= 0 else max(0, min(width, round((value / total) * width)))
|
|
166
|
+
return cell_fill(fill_cells, filled) + cell_fill(width - fill_cells, empty)
|