From 9ace26b37dbf1646a6c10b1a201b031d4af2b603 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:34:17 +0530 Subject: [PATCH 1/5] docs: add five-minute starter template (#244) --- CHANGELOG.md | 3 ++ docs/index.md | 2 ++ docs/starter-template.md | 47 ++++++++++++++++++++++++ examples/README.md | 2 +- examples/minimal_cli/README.md | 50 +++++++++++++++++++++++--- examples/minimal_cli/pyproject.toml | 2 +- examples/minimal_cli/tests/test_cli.py | 7 ++++ scripts/validate_docs.py | 1 + tests/test_starter_template.py | 36 +++++++++++++++++++ 9 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 docs/starter-template.md create mode 100644 tests/test_starter_template.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d3482a1..d78e012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and versions are tracked in the repo-root `VERSION` file. ### Changed +- Add a maintained, copyable five-minute starter template with explicit safe + customization points and installed-consumer validation (#244). + - Include `uv.lock` freshness in the authoritative local validation aggregate (#424). ## [0.5.1] - 2026-10-10 diff --git a/docs/index.md b/docs/index.md index 35ef02d..90b63d5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -58,6 +58,8 @@ application. with the underlying parser and decide whether its lifecycle boundary fits. - Follow the [five-minute consumer quickstart](consumer-quickstart.md) for a minimal public `App`, `run_app()`, human output, and JSON invocation. +- Use the [starter template](starter-template.md) when you want a complete + copyable project with packaging and tests. - Start with the [adopter readiness guide](adopter-readiness.md) for a production evaluation. - See the [adoption and compatibility evidence guide](adoption-evidence.md) diff --git a/docs/starter-template.md b/docs/starter-template.md new file mode 100644 index 0000000..e1d9aea --- /dev/null +++ b/docs/starter-template.md @@ -0,0 +1,47 @@ +# Starter template + +The maintained starter is [`examples/minimal_cli`](../examples/minimal_cli/). +It is deliberately a copyable project rather than an interactive generator: +the first five minutes should expose the framework's public boundaries instead +of hiding them behind template machinery. + +## Five-minute proof + +Copy the directory into a clean location and run the same commands a new +consumer would use: + +```bash +cp -R examples/minimal_cli my-cli +cd my-cli +python -m venv .venv +source .venv/bin/activate # Windows: .venv\\Scripts\\activate +python -m pip install --upgrade pip +python -m pip install -e . +base-minimal --name Ada +python -m pytest +``` + +The starter has no private-repository dependency and uses only the public +`base_cli` facade. Its tests cover a successful invocation, lifecycle help, +and the missing-option error boundary. The repository's example workflow +installs and tests it as an installed consumer as well. + +## What to customize + +The command implementation belongs in +`src/minimal_cli/cli.py`; the package and console-script names belong in +`pyproject.toml`; product behavior belongs in `tests/` and `README.md`. +Keep `base_cli.run_app(app)` as the process boundary and do not import +framework-private modules. Add JSON, configuration, or optional integrations +only when the product needs them, following the corresponding contract docs. + +The starter's `base-cli>=0.5.1,<1.0` requirement is a release-line guard for +the current pre-1.0 framework. It should be updated deliberately when the +consumer adopts base-cli 1.0. + +## Why there is no `base-cli init` command yet + +The copyable starter is the 1.0 adoption surface. A generator would add a +second templating and upgrade contract before there is enough consumer +feedback to stabilize it. Interactive scaffolding remains a follow-up design +item; it must not become a hidden dependency of the core package. diff --git a/examples/README.md b/examples/README.md index 61ba0db..4172224 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,7 @@ directory. | Example | Demonstrates | Launcher | | --- | --- | --- | -| [`minimal_cli`](minimal_cli/README.md) | A single `base_cli.App` command | `base-minimal` | +| [`minimal_cli`](minimal_cli/README.md) | The five-minute copyable starter and a single `base_cli.App` command | `base-minimal` | | [`nested_click_app`](nested_click_app/README.md) | Native Click groups and plugin discovery | `base-nested` | | [`typer_app`](typer_app/README.md) | Typed Typer commands through the optional adapter | `base-typer` | | [`automation_observability_app`](automation_observability_app/README.md) | JSON output, dry runs, Rich, and telemetry | `base-automation` | diff --git a/examples/minimal_cli/README.md b/examples/minimal_cli/README.md index 2eedd91..22f27d9 100644 --- a/examples/minimal_cli/README.md +++ b/examples/minimal_cli/README.md @@ -1,13 +1,53 @@ -# Minimal CLI +# Minimal CLI starter -This is the smallest complete `base-cli` application: one typed command, -standard lifecycle flags, file logging, cleanup, and a console-script entry -point. It is a good starting point for an internal tool or a new product. +This is the maintained, copyable starter for a new `base-cli` application. It +is intentionally small: one typed command, standard lifecycle flags, file +logging, cleanup, and a console-script entry point. It is a good starting +point for an internal tool or a new product. + +The starter is a reference template, not a code generator. Copy the directory +into your project, run the normal install and test commands below, and then +replace the command implementation at `src/minimal_cli/cli.py`. + +## Start in five minutes + +From the root of a `base-cli` checkout: + +```bash +cp -R examples/minimal_cli my-cli +cd my-cli +python -m venv .venv +source .venv/bin/activate # Windows: .venv\\Scripts\\activate +python -m pip install --upgrade pip +python -m pip install -e . +base-minimal --name Ada +python -m pytest +``` + +The copy is a complete installable project. It exercises the same package, +console-script, and test boundaries that a production consumer uses; it does +not import the repository checkout. + +## Safe customization points + +- `src/minimal_cli/cli.py`: replace `greet()` with the product's commands and + keep `base_cli.run_app(app)` as the process boundary. +- `src/minimal_cli/__init__.py`: update the consumer package version and + metadata when the product is released. +- `pyproject.toml`: rename the distribution and console script, then pin the + supported `base-cli` release line. +- `tests/test_cli.py`: keep a happy-path test and add the product's error and + JSON-contract tests. +- `README.md`: document the product's configuration, output, completion, + release, and troubleshooting behavior. + +Keep framework internals out of the consumer. Click remains the parser and +the public `base_cli` facade is the supported integration boundary. ## Install ```bash -python -m pip install "base-cli>=0.3" +python -m pip install "base-cli>=0.5.1,<1.0" python -m pip install . base-minimal --help ``` diff --git a/examples/minimal_cli/pyproject.toml b/examples/minimal_cli/pyproject.toml index d171f32..62649c5 100644 --- a/examples/minimal_cli/pyproject.toml +++ b/examples/minimal_cli/pyproject.toml @@ -7,7 +7,7 @@ name = "base-cli-reference-minimal" version = "0.1.0" description = "Minimal production-style base-cli reference application" requires-python = ">=3.10" -dependencies = ["base-cli>=0.3"] +dependencies = ["base-cli>=0.5.1,<1.0"] [project.scripts] base-minimal = "minimal_cli.cli:main" diff --git a/examples/minimal_cli/tests/test_cli.py b/examples/minimal_cli/tests/test_cli.py index 540e7a2..72fa756 100644 --- a/examples/minimal_cli/tests/test_cli.py +++ b/examples/minimal_cli/tests/test_cli.py @@ -24,3 +24,10 @@ def test_help_exposes_lifecycle_options() -> None: assert result.exit_code == 0, result.output assert "--name" in result.stdout assert "--environment" in result.stdout + + +def test_missing_name_is_an_actionable_production_error() -> None: + result = base_cli.testing.invoke(app, []) + + assert result.exit_code != 0 + assert "Missing option '--name'" in result.stderr diff --git a/scripts/validate_docs.py b/scripts/validate_docs.py index 708b8a1..07c563e 100644 --- a/scripts/validate_docs.py +++ b/scripts/validate_docs.py @@ -45,6 +45,7 @@ "security-threat-model.md", "schemas.md", "strict-json-consumer.md", + "starter-template.md", "typer-adapter.md", "user-config-typing.md", } diff --git a/tests/test_starter_template.py b/tests/test_starter_template.py new file mode 100644 index 0000000..73872eb --- /dev/null +++ b/tests/test_starter_template.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).parents[1] +STARTER = ROOT / "examples" / "minimal_cli" +STARTER_DOC = ROOT / "docs" / "starter-template.md" + + +def test_starter_has_the_copyable_project_boundaries() -> None: + for relative in ( + "README.md", + "pyproject.toml", + "src/minimal_cli/cli.py", + "src/minimal_cli/__init__.py", + "tests/test_cli.py", + ): + assert (STARTER / relative).is_file(), relative + + +def test_starter_pins_the_current_pre_one_release_line() -> None: + metadata = (STARTER / "pyproject.toml").read_text(encoding="utf-8") + assert 'base-cli>=0.5.1,<1.0' in metadata + + +def test_starter_docs_explain_copying_and_safe_customization() -> None: + text = STARTER_DOC.read_text(encoding="utf-8") + for required in ( + "examples/minimal_cli", + "python -m pip install -e .", + "python -m pytest", + "base_cli.run_app(app)", + "no `base-cli init` command yet", + ): + assert required in text From 5017febe178aae916445573b4dffbf0774f83eaa Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:49:36 +0530 Subject: [PATCH 2/5] fix: align starter and reference installs --- docs/starter-template.md | 4 ++-- examples/automation_observability_app/pyproject.toml | 2 +- examples/minimal_cli/README.md | 11 ++++++----- examples/minimal_cli/pyproject.toml | 3 +++ examples/nested_click_app/pyproject.toml | 2 +- examples/typer_app/pyproject.toml | 2 +- scripts/validate_examples.py | 5 ++++- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/starter-template.md b/docs/starter-template.md index e1d9aea..f0cd167 100644 --- a/docs/starter-template.md +++ b/docs/starter-template.md @@ -14,9 +14,9 @@ consumer would use: cp -R examples/minimal_cli my-cli cd my-cli python -m venv .venv -source .venv/bin/activate # Windows: .venv\\Scripts\\activate +source .venv/bin/activate # Windows: .venv\Scripts\activate python -m pip install --upgrade pip -python -m pip install -e . +python -m pip install -e ".[dev]" base-minimal --name Ada python -m pytest ``` diff --git a/examples/automation_observability_app/pyproject.toml b/examples/automation_observability_app/pyproject.toml index a3c7d40..007f9c3 100644 --- a/examples/automation_observability_app/pyproject.toml +++ b/examples/automation_observability_app/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Automation and observability base-cli reference application" requires-python = ">=3.10" dependencies = [ - "base-cli>=0.3", + "base-cli>=0.5.1,<1.0", "click>=8.1", ] diff --git a/examples/minimal_cli/README.md b/examples/minimal_cli/README.md index 22f27d9..032cfca 100644 --- a/examples/minimal_cli/README.md +++ b/examples/minimal_cli/README.md @@ -17,9 +17,9 @@ From the root of a `base-cli` checkout: cp -R examples/minimal_cli my-cli cd my-cli python -m venv .venv -source .venv/bin/activate # Windows: .venv\\Scripts\\activate +source .venv/bin/activate # Windows: .venv\Scripts\activate python -m pip install --upgrade pip -python -m pip install -e . +python -m pip install -e ".[dev]" base-minimal --name Ada python -m pytest ``` @@ -46,9 +46,11 @@ the public `base_cli` facade is the supported integration boundary. ## Install +From the copied project directory, use the editable install so changes to +`src/minimal_cli/` are immediately reflected when you rerun the command: + ```bash -python -m pip install "base-cli>=0.5.1,<1.0" -python -m pip install . +python -m pip install -e ".[dev]" base-minimal --help ``` @@ -69,7 +71,6 @@ errors follow the versioned JSON contract documented by base-cli. ## Tests ```bash -python -m pip install "base-cli[dev]" python -m pytest tests ``` diff --git a/examples/minimal_cli/pyproject.toml b/examples/minimal_cli/pyproject.toml index 62649c5..dbc7877 100644 --- a/examples/minimal_cli/pyproject.toml +++ b/examples/minimal_cli/pyproject.toml @@ -9,6 +9,9 @@ description = "Minimal production-style base-cli reference application" requires-python = ">=3.10" dependencies = ["base-cli>=0.5.1,<1.0"] +[project.optional-dependencies] +dev = ["pytest>=8,<9"] + [project.scripts] base-minimal = "minimal_cli.cli:main" diff --git a/examples/nested_click_app/pyproject.toml b/examples/nested_click_app/pyproject.toml index 4af6106..8bcbeb6 100644 --- a/examples/nested_click_app/pyproject.toml +++ b/examples/nested_click_app/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Nested Click and plugin-discovery base-cli reference application" requires-python = ">=3.10" dependencies = [ - "base-cli>=0.3", + "base-cli>=0.5.1,<1.0", "click>=8.1", ] diff --git a/examples/typer_app/pyproject.toml b/examples/typer_app/pyproject.toml index a73345f..1aa122f 100644 --- a/examples/typer_app/pyproject.toml +++ b/examples/typer_app/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Typed Typer base-cli reference application" requires-python = ">=3.10" dependencies = [ - "base-cli>=0.3", + "base-cli>=0.5.1,<1.0", "typer>=0.25.1,<0.28", ] diff --git a/scripts/validate_examples.py b/scripts/validate_examples.py index cc5fdbc..25d8b3f 100644 --- a/scripts/validate_examples.py +++ b/scripts/validate_examples.py @@ -24,6 +24,7 @@ "Operational troubleshooting", ) SCRIPT_PATTERN = re.compile(r"^\s*base-[a-z0-9-]+\s*=\s*\"[a-zA-Z0-9_.]+:[a-zA-Z0-9_]+\"\s*$", re.MULTILINE) +BASE_CLI_REQUIREMENT = "base-cli>=0.5.1,<1.0" def fail(message: str) -> None: @@ -50,7 +51,9 @@ def validate_example(root: Path, name: str) -> None: if not required.exists(): fail(f"{required.relative_to(root)} is required") metadata = pyproject.read_text(encoding="utf-8") - if "base-cli" not in metadata or "[project.scripts]" not in metadata: + if BASE_CLI_REQUIREMENT not in metadata: + fail(f"{pyproject.relative_to(root)} must require {BASE_CLI_REQUIREMENT!r}") + if "[project.scripts]" not in metadata: fail(f"{pyproject.relative_to(root)} must declare base-cli and a console script") if not SCRIPT_PATTERN.search(metadata): fail(f"{pyproject.relative_to(root)} has no base-* console script") From 1584a657d621df9933a780559bf3a86050c6b632 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:50:47 +0530 Subject: [PATCH 3/5] test: update starter install assertion --- tests/test_starter_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_starter_template.py b/tests/test_starter_template.py index 73872eb..dc4780d 100644 --- a/tests/test_starter_template.py +++ b/tests/test_starter_template.py @@ -28,7 +28,7 @@ def test_starter_docs_explain_copying_and_safe_customization() -> None: text = STARTER_DOC.read_text(encoding="utf-8") for required in ( "examples/minimal_cli", - "python -m pip install -e .", + 'python -m pip install -e ".[dev]"', "python -m pytest", "base_cli.run_app(app)", "no `base-cli init` command yet", From 836db0b3d155c5280c0048ea87f21134d941d1c2 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:45:01 +0530 Subject: [PATCH 4/5] style: satisfy starter template formatting gate --- tests/test_starter_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_starter_template.py b/tests/test_starter_template.py index dc4780d..2e04c0d 100644 --- a/tests/test_starter_template.py +++ b/tests/test_starter_template.py @@ -21,7 +21,7 @@ def test_starter_has_the_copyable_project_boundaries() -> None: def test_starter_pins_the_current_pre_one_release_line() -> None: metadata = (STARTER / "pyproject.toml").read_text(encoding="utf-8") - assert 'base-cli>=0.5.1,<1.0' in metadata + assert "base-cli>=0.5.1,<1.0" in metadata def test_starter_docs_explain_copying_and_safe_customization() -> None: From 7d0dc0d444af484d9b7197a67b4ef029106ba390 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:48:14 +0530 Subject: [PATCH 5/5] style: normalize starter test imports --- tests/test_starter_template.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_starter_template.py b/tests/test_starter_template.py index 2e04c0d..c47b719 100644 --- a/tests/test_starter_template.py +++ b/tests/test_starter_template.py @@ -2,7 +2,6 @@ from pathlib import Path - ROOT = Path(__file__).parents[1] STARTER = ROOT / "examples" / "minimal_cli" STARTER_DOC = ROOT / "docs" / "starter-template.md"