Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions docs/starter-template.md
Original file line number Diff line number Diff line change
@@ -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 ".[dev]"
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.
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
2 changes: 1 addition & 1 deletion examples/automation_observability_app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
55 changes: 48 additions & 7 deletions examples/minimal_cli/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,56 @@
# 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 ".[dev]"
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

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.3"
python -m pip install .
python -m pip install -e ".[dev]"
base-minimal --help
```

Expand All @@ -29,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
```

Expand Down
5 changes: 4 additions & 1 deletion examples/minimal_cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ 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"]
Comment thread
codeforester marked this conversation as resolved.

[project.optional-dependencies]
dev = ["pytest>=8,<9"]

[project.scripts]
base-minimal = "minimal_cli.cli:main"
Expand Down
7 changes: 7 additions & 0 deletions examples/minimal_cli/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion examples/nested_click_app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
2 changes: 1 addition & 1 deletion examples/typer_app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
1 change: 1 addition & 0 deletions scripts/validate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"security-threat-model.md",
"schemas.md",
"strict-json-consumer.md",
"starter-template.md",
"typer-adapter.md",
"user-config-typing.md",
}
Expand Down
5 changes: 4 additions & 1 deletion scripts/validate_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")
Expand Down
35 changes: 35 additions & 0 deletions tests/test_starter_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 ".[dev]"',
"python -m pytest",
"base_cli.run_app(app)",
"no `base-cli init` command yet",
):
assert required in text
Loading