From 8fe65344f7dffc0f4b9c6d635ee1d6ac453d1e58 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 11 Nov 2026 00:55:21 +0100 Subject: [PATCH 1/4] fix(cli): smpy new warns when a newer simple_module_cli is on PyPI (#899) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 1 + README.md | 4 +- docs/guide/installation.md | 2 + docs/guide/quickstart.md | 2 +- .../2026-11-11-stale-cli-warning-design.md | 37 +++++++++++++ framework/cli/README.md | 2 +- .../cli/simple_module_cli/_version_check.py | 54 +++++++++++++++++++ framework/cli/simple_module_cli/new.py | 3 ++ framework/cli/tests/conftest.py | 10 ++++ framework/cli/tests/test_cli_version_check.py | 52 ++++++++++++++++++ 10 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md create mode 100644 framework/cli/simple_module_cli/_version_check.py create mode 100644 framework/cli/tests/test_cli_version_check.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 50914dcc2..df455c33d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed +- `smpy new` warns when the CLI running it is older than the latest `simple_module_cli` on PyPI. A new app pins the framework to the CLI's own version, and `uvx --from simple_module_cli` quietly reuses a copy installed with `uv tool install`, so an old tool used to scaffold an outdated app without saying so. The warning names both fixes: `uv tool upgrade simple_module_cli`, or `uvx --from simple_module_cli@latest`. The check is best effort, with a 3-second timeout; offline or on any PyPI error it is skipped, and it never blocks the scaffold. The README and the CLI README now use `uvx --from simple_module_cli@latest`, and the installation and quickstart guides say how to upgrade an installed CLI (#899). - The first-module guidance now agrees with itself. The guides (`docs/index.md`, quickstart, installation, Your first module) wire a new module with `smpy add ../modules/orders --path host`, the command `smpy create-module` prints, which adds it to `host/pyproject.toml` as a workspace source. They used to say `uv add ./modules/orders`, which in a `smpy new` workspace edits the root `pyproject.toml` instead. A flat app uses `smpy add ./modules/orders`. The landing page `smpy new` generates drops the `create-admin` step (the `/setup` wizard already creates the first administrator), lists the wiring and migration steps after `create-module`, and its "Read the docs" button opens the first-module guide on docs.py.simplemodule.dev instead of the GitHub repo. The first-module guide and the `simple-module-conventions` skill note that SQLModel's `Field(regex=...)` is accepted but not enforced on a DTO, and show a `field_validator` instead (#898). - A fresh app's `make dev` no longer prints Vite errors or warnings, and `npm audit` reports nothing. The `simple_module_settings` wheel no longer ships `ui/components/validation-errors.test.ts`: that test made the host's Vite dependency scan fail with "Failed to run dependency scan … vitest". It now lives in `modules/settings/tests-js/`, and `make ci-build-packages` fails if any wheel contains a JS or TS `*.test.*` or `*.spec.*` file, a `test-dom.tsx` or a `page-tests/` directory. The scaffold's `client_app/vite.config.ts` loads under Vite 8's native config loader: it uses `import.meta.dirname` and `.ts` import extensions, and the template `tsconfig.json` sets `allowImportingTsExtensions`. It also uses `optimizeDeps.rolldownOptions` instead of the deprecated `esbuildOptions`. New apps' root `package.json` overrides `postcss-selector-parser` to `^6.1.5`, which is what `@tailwindcss/typography` pulls in, as this repo does. An existing app can copy `host/client_app/vite.config.ts` and `tsconfig.json` from a new app, and add the override (#900). - The `Browse`, `Create` and `Edit` pages that `smpy create-module` (and `make new-module`) generates now render inside the app shell: each sets `.layout = [AuthenticatedLayout]`. Before, they had no sidebar, header or footer, so the module's sidebar entry led to a bare page with no way back. To fix a module generated earlier, add `import { AuthenticatedLayout } from '@simple-module-py/ui/layouts/AuthenticatedLayout';` and that assignment to each of its pages, or use `AdminLayout` for pages you moved under `/admin` and `ADMIN_SIDEBAR` (#897). diff --git a/README.md b/README.md index c48aa8b86..26ab644eb 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ A modular-monolith framework for Python. Each feature lives in its own self-cont If you want to **build an app on simple_module**, not hack on the framework itself: ```bash -uvx --from simple_module_cli smpy new my-app +uvx --from simple_module_cli@latest smpy new my-app cd my-app make dev ``` +`@latest` matters: without it `uvx` reuses an `smpy` you installed earlier with `uv tool install`, and an older CLI scaffolds an app pinned to its own, older framework version. `smpy new` warns when PyPI has a newer CLI. + That scaffolds a working FastAPI + Inertia + React app with `users`, `dashboard`, and `permissions` pre-wired. You land on `/users/login`, sign in with the admin account you bootstrap, and go from there. See [CHANGELOG.md](CHANGELOG.md) for the list of published PyPI / npm packages at each release. diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 5f18b4d09..63f01a8ad 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -32,6 +32,8 @@ That puts `smpy` on your PATH globally. Confirm with: smpy --help ``` +Already installed? `uv tool upgrade simple_module_cli` (or `pipx upgrade simple_module_cli`). A new app pins the framework to the CLI's own version, so an old CLI scaffolds an old app; `smpy new` warns when PyPI has a newer one. + ## Scaffold a new app ```bash diff --git a/docs/guide/quickstart.md b/docs/guide/quickstart.md index 40b29ffdf..74b87a070 100644 --- a/docs/guide/quickstart.md +++ b/docs/guide/quickstart.md @@ -8,7 +8,7 @@ Five minutes from `smpy new` to a running app with a freshly scaffolded module. uv tool install simple_module_cli ``` -(Or `pipx install simple_module_cli`.) That puts `smpy` on your PATH globally. +(Or `pipx install simple_module_cli`.) That puts `smpy` on your PATH globally. Already installed? `uv tool upgrade simple_module_cli` (or `pipx upgrade simple_module_cli`). A new app pins the framework to the CLI's own version, so an old CLI scaffolds an old app; `smpy new` warns when PyPI has a newer one. ## 2. Scaffold an app diff --git a/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md b/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md new file mode 100644 index 000000000..9685980d0 --- /dev/null +++ b/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md @@ -0,0 +1,37 @@ +# Warn when `smpy new` runs from an outdated CLI (#899) + +## Problem + +A new app pins every `simple_module_*` package to the running CLI's own version +(`pins.resolve_framework_version`). `uvx --from simple_module_cli` reuses a tool that was +installed earlier with `uv tool install`, and `--refresh` doesn't change that. So an old +tool scaffolds an old app, and nothing says so. + +## Design + +- `simple_module_cli/_version_check.py`: + - `newer_release(current)` reuses `pypi.fetch_latest` with its own 3-second fetcher. It + returns the newer stable version, or `None`. It also returns `None` for the + `0.0.0` editable placeholder and on any `OSError`/`ValueError`: offline, timeouts, + bad JSON. + - `warn_if_outdated` prints a warning to stderr that names `uv tool upgrade + simple_module_cli` and `uvx --from simple_module_cli@latest`. It never blocks the + scaffold. +- `smpy new` calls it right after validating the name. +- `framework/cli/tests/conftest.py` gets an autouse fixture that makes the fetcher raise, + so no CLI test touches the network. +- Docs: + - `README.md` and `framework/cli/README.md` use `@latest`; + - the installation and quickstart guides say how to upgrade. + +Rejected: an env var to opt out. The check is quiet whenever it can't run, and repo rules +discourage new env-var configuration. + +## Testing + +- Unit tests: + - a newer version is reported; + - the same version, a newer local one and the placeholder are not; + - every failure type is skipped. +- CLI tests: `smpy new` warns and still scaffolds, and is silent when PyPI is unreachable. +- Against the real PyPI: `0.0.36` reports 0.0.44, and `0.0.44` reports nothing (0.16 s). diff --git a/framework/cli/README.md b/framework/cli/README.md index c7a0ef31a..a45ea4f98 100644 --- a/framework/cli/README.md +++ b/framework/cli/README.md @@ -9,7 +9,7 @@ pip install simple_module_cli # or, to keep the CLI in its own venv: pipx install simple_module_cli # or, to run it without installing: -uvx --from simple_module_cli smpy new my-app +uvx --from simple_module_cli@latest smpy new my-app ``` The package depends only on `typer`, `tomlkit` and `packaging` — installing it does **not** pull in FastAPI, SQLModel, or any other framework runtime. diff --git a/framework/cli/simple_module_cli/_version_check.py b/framework/cli/simple_module_cli/_version_check.py new file mode 100644 index 000000000..af5e7057d --- /dev/null +++ b/framework/cli/simple_module_cli/_version_check.py @@ -0,0 +1,54 @@ +"""Warn when ``smpy new`` runs from an older CLI than PyPI's latest (GH #899). + +A new app pins every framework package to the running CLI's own version, and +``uvx --from simple_module_cli`` quietly reuses a ``uv tool install``-ed copy, so +an old tool scaffolds an old app without saying so. The check is best effort: a +short timeout, and any failure (offline, PyPI down, bad JSON) skips it. +""" + +from __future__ import annotations + +import json +import urllib.request +from typing import Any + +import typer +from packaging.version import Version + +from simple_module_cli.pypi import PYPI_URL, Fetcher, fetch_latest + +CLI_DIST = "simple_module_cli" +_TIMEOUT_SECONDS = 3 + + +def _quick_fetcher(url: str) -> dict[str, Any]: + with urllib.request.urlopen(url, timeout=_TIMEOUT_SECONDS) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def newer_release(current: str, fetcher: Fetcher | None = None) -> str | None: + """The latest stable ``simple_module_cli`` on PyPI if it is newer than ``current``.""" + if current == "0.0.0": # resolve_framework_version()'s placeholder: no dist-info + return None + try: + latest = fetch_latest(CLI_DIST, include_pre=False, fetcher=fetcher or _quick_fetcher) + if latest is not None and Version(latest) > Version(current): + return latest + except (OSError, ValueError): + # Timeouts and socket errors are OSErrors; bad JSON or versions are ValueErrors. + return None + return None + + +def warn_if_outdated(current: str, fetcher: Fetcher | None = None) -> None: + latest = newer_release(current, fetcher) + if latest is None: + return + typer.echo( + f"WARNING: this smpy is {current}, but {CLI_DIST} {latest} is on PyPI " + f"({PYPI_URL.format(name=CLI_DIST)}). The new app will pin the framework to " + f"{current}.\n" + f" Upgrade the installed tool: uv tool upgrade {CLI_DIST}\n" + f" or run the latest once: uvx --from {CLI_DIST}@latest smpy new ...", + err=True, + ) diff --git a/framework/cli/simple_module_cli/new.py b/framework/cli/simple_module_cli/new.py index 8df2b3841..233a50fb6 100644 --- a/framework/cli/simple_module_cli/new.py +++ b/framework/cli/simple_module_cli/new.py @@ -10,10 +10,12 @@ import typer +from simple_module_cli._version_check import warn_if_outdated from simple_module_cli.add_cmd import run_add from simple_module_cli.app_project import create_app_project from simple_module_cli.case import InvalidScaffoldNameError, to_kebab_case, validate_scaffold_name from simple_module_cli.catalog import PRESETS, expand_deps +from simple_module_cli.pins import resolve_framework_version from simple_module_cli.wizard import run_wizard __all__ = ["new_project"] @@ -110,6 +112,7 @@ def new_project( except InvalidScaffoldNameError as exc: typer.echo(f"ERROR: {exc}", err=True) raise typer.Exit(code=1) from exc + warn_if_outdated(resolve_framework_version()) pypi_name = to_kebab_case(name) if pypi_name != name: typer.echo(f"Normalizing PyPI name to {pypi_name!r}.") diff --git a/framework/cli/tests/conftest.py b/framework/cli/tests/conftest.py index 9283d24e8..188c44045 100644 --- a/framework/cli/tests/conftest.py +++ b/framework/cli/tests/conftest.py @@ -16,6 +16,16 @@ import pytest +@pytest.fixture(autouse=True) +def _no_pypi_version_check(monkeypatch): + """`smpy new` asks PyPI for a newer CLI (GH #899); keep CLI tests off the network.""" + + def offline(url: str) -> dict: + raise OSError(f"network disabled in tests: {url}") + + monkeypatch.setattr("simple_module_cli._version_check._quick_fetcher", offline) + + @pytest.fixture def fake_pypi(): """Return a factory building a `package-update` fetcher stub. diff --git a/framework/cli/tests/test_cli_version_check.py b/framework/cli/tests/test_cli_version_check.py new file mode 100644 index 000000000..a0a57352a --- /dev/null +++ b/framework/cli/tests/test_cli_version_check.py @@ -0,0 +1,52 @@ +"""`smpy new` warns when the running CLI is older than PyPI's latest (GH #899).""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from simple_module_cli import _version_check +from simple_module_cli.cli import app +from typer.testing import CliRunner + + +def test_newer_release_reports_a_newer_stable_version(fake_pypi) -> None: + fetcher = fake_pypi({"simple_module_cli": "0.0.44"}) + assert _version_check.newer_release("0.0.36", fetcher) == "0.0.44" + + +@pytest.mark.parametrize("current", ["0.0.44", "0.0.45", "0.0.0"]) +def test_no_newer_release_when_current_or_placeholder(fake_pypi, current: str) -> None: + fetcher = fake_pypi({"simple_module_cli": "0.0.44"}) + assert _version_check.newer_release(current, fetcher) is None + + +@pytest.mark.parametrize("error", [OSError("offline"), TimeoutError(), ValueError("bad json")]) +def test_any_lookup_failure_skips_the_check(error: Exception) -> None: + def failing(url: str) -> dict: + raise error + + assert _version_check.newer_release("0.0.36", failing) is None + + +def test_smpy_new_warns_and_still_scaffolds(tmp_path: Path, fake_pypi, monkeypatch) -> None: + monkeypatch.setattr( + _version_check, "_quick_fetcher", fake_pypi({"simple_module_cli": "99.0.0"}) + ) + target = tmp_path / "demo" + result = CliRunner().invoke( + app, ["new", "demo", "--yes", "--db", "sqlite", "--no-install", "--dest", str(target)] + ) + assert result.exit_code == 0, result.output + assert "simple_module_cli 99.0.0 is on PyPI" in result.output + assert "uv tool upgrade simple_module_cli" in result.output + assert (target / "host" / "pyproject.toml").is_file() + + +def test_smpy_new_is_silent_when_pypi_is_unreachable(tmp_path: Path) -> None: + target = tmp_path / "demo" + result = CliRunner().invoke( + app, ["new", "demo", "--yes", "--db", "sqlite", "--no-install", "--dest", str(target)] + ) + assert result.exit_code == 0, result.output + assert "WARNING: this smpy" not in result.output From d4e19ac77908ab5984a659b5d36af50582438b14 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 11 Nov 2026 00:58:04 +0100 Subject: [PATCH 2/4] fix: address code review findings (round 1, pass 1) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- docs/index.md | 2 +- .../cli/simple_module_cli/_version_check.py | 22 ++++++++----------- framework/cli/simple_module_cli/pins.py | 5 ++++- framework/cli/simple_module_cli/pypi.py | 4 ++-- framework/hosting/README.md | 2 +- .../components/CopyCommand.test.tsx | 2 +- host/client_app/pages/Landing.tsx | 2 +- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/index.md b/docs/index.md index 4dc46c25a..8a79913c8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -62,7 +62,7 @@ features: ## Try it in 60 seconds ```bash -uv tool install simple_module_cli +uv tool install simple_module_cli # already installed? uv tool upgrade simple_module_cli smpy new myapp --yes cd myapp make dev # API on :8000, Vite on :5050 diff --git a/framework/cli/simple_module_cli/_version_check.py b/framework/cli/simple_module_cli/_version_check.py index af5e7057d..2071730c7 100644 --- a/framework/cli/simple_module_cli/_version_check.py +++ b/framework/cli/simple_module_cli/_version_check.py @@ -8,34 +8,30 @@ from __future__ import annotations -import json -import urllib.request -from typing import Any +from functools import partial import typer from packaging.version import Version -from simple_module_cli.pypi import PYPI_URL, Fetcher, fetch_latest +from simple_module_cli.pins import PLACEHOLDER_VERSION +from simple_module_cli.pypi import PYPI_URL, Fetcher, default_fetcher, fetch_latest CLI_DIST = "simple_module_cli" _TIMEOUT_SECONDS = 3 - - -def _quick_fetcher(url: str) -> dict[str, Any]: - with urllib.request.urlopen(url, timeout=_TIMEOUT_SECONDS) as resp: - return json.loads(resp.read().decode("utf-8")) +_quick_fetcher: Fetcher = partial(default_fetcher, timeout=_TIMEOUT_SECONDS) def newer_release(current: str, fetcher: Fetcher | None = None) -> str | None: """The latest stable ``simple_module_cli`` on PyPI if it is newer than ``current``.""" - if current == "0.0.0": # resolve_framework_version()'s placeholder: no dist-info + if current == PLACEHOLDER_VERSION: return None try: latest = fetch_latest(CLI_DIST, include_pre=False, fetcher=fetcher or _quick_fetcher) if latest is not None and Version(latest) > Version(current): return latest - except (OSError, ValueError): - # Timeouts and socket errors are OSErrors; bad JSON or versions are ValueErrors. + except Exception: + # OSError (timeouts, sockets), ValueError (bad JSON/versions), but also + # http.client.IncompleteRead and AttributeError on an unexpected payload shape. return None return None @@ -48,7 +44,7 @@ def warn_if_outdated(current: str, fetcher: Fetcher | None = None) -> None: f"WARNING: this smpy is {current}, but {CLI_DIST} {latest} is on PyPI " f"({PYPI_URL.format(name=CLI_DIST)}). The new app will pin the framework to " f"{current}.\n" - f" Upgrade the installed tool: uv tool upgrade {CLI_DIST}\n" + f" Upgrade the installed tool: uv tool upgrade {CLI_DIST} (pipx upgrade {CLI_DIST})\n" f" or run the latest once: uvx --from {CLI_DIST}@latest smpy new ...", err=True, ) diff --git a/framework/cli/simple_module_cli/pins.py b/framework/cli/simple_module_cli/pins.py index f711b9a72..9ab9b9bae 100644 --- a/framework/cli/simple_module_cli/pins.py +++ b/framework/cli/simple_module_cli/pins.py @@ -15,6 +15,9 @@ from pathlib import Path +# What resolve_framework_version() returns for an install without dist-info. +PLACEHOLDER_VERSION = "0.0.0" + def resolve_framework_version() -> str: """Resolve the framework version that scaffolded apps should pin against. @@ -31,7 +34,7 @@ def resolve_framework_version() -> str: try: return pkg_version("simple_module_cli") except PackageNotFoundError: - return "0.0.0" + return PLACEHOLDER_VERSION def _pin_one(dep: str, version: str) -> str: diff --git a/framework/cli/simple_module_cli/pypi.py b/framework/cli/simple_module_cli/pypi.py index 8b7a2f3ea..9a0d62b2e 100644 --- a/framework/cli/simple_module_cli/pypi.py +++ b/framework/cli/simple_module_cli/pypi.py @@ -57,6 +57,6 @@ def fetch_latest(name: str, *, include_pre: bool, fetcher: Fetcher) -> str | Non return fallback if is_valid_version(fallback) else None -def default_fetcher(url: str) -> dict[str, Any]: - with urllib.request.urlopen(url, timeout=10) as resp: +def default_fetcher(url: str, timeout: float = 10) -> dict[str, Any]: + with urllib.request.urlopen(url, timeout=timeout) as resp: return json.loads(resp.read().decode("utf-8")) diff --git a/framework/hosting/README.md b/framework/hosting/README.md index 61e3a906c..c1f7bebe9 100644 --- a/framework/hosting/README.md +++ b/framework/hosting/README.md @@ -11,7 +11,7 @@ pip install simple_module_hosting For a new project, most users run the generator instead (shipped by the standalone `simple_module_cli` distribution): ```bash -uvx --from simple_module_cli smpy new my-app +uvx --from simple_module_cli@latest smpy new my-app ``` ## What it provides diff --git a/host/client_app/components/CopyCommand.test.tsx b/host/client_app/components/CopyCommand.test.tsx index 6ef7dbad3..f63aa4eae 100644 --- a/host/client_app/components/CopyCommand.test.tsx +++ b/host/client_app/components/CopyCommand.test.tsx @@ -27,7 +27,7 @@ vi.mock('@simple-module-py/i18n', () => ({ import { CopyCommand } from './CopyCommand'; -const COMMAND = 'uvx --from simple_module_cli smpy new my-app'; +const COMMAND = 'uvx --from simple_module_cli@latest smpy new my-app'; function stubClipboard(impl: () => Promise) { const writeText = vi.fn(impl); diff --git a/host/client_app/pages/Landing.tsx b/host/client_app/pages/Landing.tsx index dd2aebef4..1e2518d4c 100644 --- a/host/client_app/pages/Landing.tsx +++ b/host/client_app/pages/Landing.tsx @@ -10,7 +10,7 @@ import { Database, LayoutTemplate, Package, Route, ShieldCheck, Stethoscope } fr import { CopyCommand } from '../components/CopyCommand'; /** The one command the hero exists to hand over. */ -const INSTALL_COMMAND = 'uvx --from simple_module_cli smpy new my-app'; +const INSTALL_COMMAND = 'uvx --from simple_module_cli@latest smpy new my-app'; /** * The terminal transcript, one entry per numbered step. From ef890013db317792044a7f389c8dcdd378d70c7f Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 11 Nov 2026 00:59:10 +0100 Subject: [PATCH 3/4] fix: address code review findings (round 1, pass 2) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- docs/reference/make-commands.md | 2 +- framework/cli/simple_module_cli/_version_check.py | 5 ++++- framework/cli/tests/test_cli_version_check.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/reference/make-commands.md b/docs/reference/make-commands.md index b7374f53a..22e185172 100644 --- a/docs/reference/make-commands.md +++ b/docs/reference/make-commands.md @@ -2,7 +2,7 @@ Two surfaces drive day-to-day work: -1. **The `smpy` CLI** — installed globally with `uv tool install simple_module_cli`. Used to scaffold apps and modules and to bump dependency versions. +1. **The `smpy` CLI** — installed globally with `uv tool install simple_module_cli` (upgrade with `uv tool upgrade simple_module_cli`). Used to scaffold apps and modules and to bump dependency versions. 2. **The Makefile in your scaffolded app** — a thin wrapper around `uv` and `npm` for the inner dev loop (`dev`, `migrate`, `build`, `gen-pages`). Past those, there's no hidden tooling — read the `Makefile` and pyproject.toml in your app directly when something surprises you. diff --git a/framework/cli/simple_module_cli/_version_check.py b/framework/cli/simple_module_cli/_version_check.py index 2071730c7..80c26bd61 100644 --- a/framework/cli/simple_module_cli/_version_check.py +++ b/framework/cli/simple_module_cli/_version_check.py @@ -26,8 +26,11 @@ def newer_release(current: str, fetcher: Fetcher | None = None) -> str | None: if current == PLACEHOLDER_VERSION: return None try: + running = Version(current) + if running.is_prerelease or running.is_devrelease or running.local: + return None # a checkout or local build is not what PyPI ships latest = fetch_latest(CLI_DIST, include_pre=False, fetcher=fetcher or _quick_fetcher) - if latest is not None and Version(latest) > Version(current): + if latest is not None and Version(latest) > running: return latest except Exception: # OSError (timeouts, sockets), ValueError (bad JSON/versions), but also diff --git a/framework/cli/tests/test_cli_version_check.py b/framework/cli/tests/test_cli_version_check.py index a0a57352a..f0cd5a4b4 100644 --- a/framework/cli/tests/test_cli_version_check.py +++ b/framework/cli/tests/test_cli_version_check.py @@ -15,7 +15,9 @@ def test_newer_release_reports_a_newer_stable_version(fake_pypi) -> None: assert _version_check.newer_release("0.0.36", fetcher) == "0.0.44" -@pytest.mark.parametrize("current", ["0.0.44", "0.0.45", "0.0.0"]) +@pytest.mark.parametrize( + "current", ["0.0.44", "0.0.45", "0.0.0", "0.0.40.dev0", "0.0.40rc1", "0.0.40+local"] +) def test_no_newer_release_when_current_or_placeholder(fake_pypi, current: str) -> None: fetcher = fake_pypi({"simple_module_cli": "0.0.44"}) assert _version_check.newer_release(current, fetcher) is None From 8bcf6e68da0c81f7a13166530f1d9673bc9fb9f2 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 11 Nov 2026 00:59:59 +0100 Subject: [PATCH 4/4] docs: design, CHANGELOG and comments match the stale-CLI check (review) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 2 +- .../2026-11-11-stale-cli-warning-design.md | 17 +++++++++++------ .../cli/simple_module_cli/_version_check.py | 2 +- framework/cli/tests/test_cli_version_check.py | 4 +++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df455c33d..3b4914663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed -- `smpy new` warns when the CLI running it is older than the latest `simple_module_cli` on PyPI. A new app pins the framework to the CLI's own version, and `uvx --from simple_module_cli` quietly reuses a copy installed with `uv tool install`, so an old tool used to scaffold an outdated app without saying so. The warning names both fixes: `uv tool upgrade simple_module_cli`, or `uvx --from simple_module_cli@latest`. The check is best effort, with a 3-second timeout; offline or on any PyPI error it is skipped, and it never blocks the scaffold. The README and the CLI README now use `uvx --from simple_module_cli@latest`, and the installation and quickstart guides say how to upgrade an installed CLI (#899). +- `smpy new` warns when the CLI running it is older than the latest `simple_module_cli` on PyPI. A new app pins the framework to the CLI's own version, and `uvx --from simple_module_cli` quietly reuses a copy installed with `uv tool install`, so an old tool used to scaffold an outdated app without saying so. The warning names both fixes: `uv tool upgrade simple_module_cli`, or `uvx --from simple_module_cli@latest`. The check is best effort, with a 3-second timeout; offline or on any PyPI error it is skipped, and it never blocks the scaffold. A CLI that is itself a pre-release, dev or local build is never warned. The README, the CLI and hosting READMEs and the landing page's copy command now use `uvx --from simple_module_cli@latest`, and the installation and quickstart guides, the docs home and the make-commands reference say how to upgrade an installed CLI, with `pipx upgrade` as the alternative (#899). - The first-module guidance now agrees with itself. The guides (`docs/index.md`, quickstart, installation, Your first module) wire a new module with `smpy add ../modules/orders --path host`, the command `smpy create-module` prints, which adds it to `host/pyproject.toml` as a workspace source. They used to say `uv add ./modules/orders`, which in a `smpy new` workspace edits the root `pyproject.toml` instead. A flat app uses `smpy add ./modules/orders`. The landing page `smpy new` generates drops the `create-admin` step (the `/setup` wizard already creates the first administrator), lists the wiring and migration steps after `create-module`, and its "Read the docs" button opens the first-module guide on docs.py.simplemodule.dev instead of the GitHub repo. The first-module guide and the `simple-module-conventions` skill note that SQLModel's `Field(regex=...)` is accepted but not enforced on a DTO, and show a `field_validator` instead (#898). - A fresh app's `make dev` no longer prints Vite errors or warnings, and `npm audit` reports nothing. The `simple_module_settings` wheel no longer ships `ui/components/validation-errors.test.ts`: that test made the host's Vite dependency scan fail with "Failed to run dependency scan … vitest". It now lives in `modules/settings/tests-js/`, and `make ci-build-packages` fails if any wheel contains a JS or TS `*.test.*` or `*.spec.*` file, a `test-dom.tsx` or a `page-tests/` directory. The scaffold's `client_app/vite.config.ts` loads under Vite 8's native config loader: it uses `import.meta.dirname` and `.ts` import extensions, and the template `tsconfig.json` sets `allowImportingTsExtensions`. It also uses `optimizeDeps.rolldownOptions` instead of the deprecated `esbuildOptions`. New apps' root `package.json` overrides `postcss-selector-parser` to `^6.1.5`, which is what `@tailwindcss/typography` pulls in, as this repo does. An existing app can copy `host/client_app/vite.config.ts` and `tsconfig.json` from a new app, and add the override (#900). - The `Browse`, `Create` and `Edit` pages that `smpy create-module` (and `make new-module`) generates now render inside the app shell: each sets `.layout = [AuthenticatedLayout]`. Before, they had no sidebar, header or footer, so the module's sidebar entry led to a bare page with no way back. To fix a module generated earlier, add `import { AuthenticatedLayout } from '@simple-module-py/ui/layouts/AuthenticatedLayout';` and that assignment to each of its pages, or use `AdminLayout` for pages you moved under `/admin` and `ADMIN_SIDEBAR` (#897). diff --git a/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md b/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md index 9685980d0..08e024511 100644 --- a/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md +++ b/docs/superpowers/specs/2026-11-11-stale-cli-warning-design.md @@ -12,17 +12,21 @@ tool scaffolds an old app, and nothing says so. - `simple_module_cli/_version_check.py`: - `newer_release(current)` reuses `pypi.fetch_latest` with its own 3-second fetcher. It returns the newer stable version, or `None`. It also returns `None` for the - `0.0.0` editable placeholder and on any `OSError`/`ValueError`: offline, timeouts, - bad JSON. + `0.0.0` editable placeholder (`pins.PLACEHOLDER_VERSION`), for a running version that + is a pre-release, dev or local build (not what PyPI ships), and on any exception: + offline, timeouts, bad JSON or an unexpected payload shape. The 3-second fetcher is + `partial(pypi.default_fetcher, timeout=3)`. - `warn_if_outdated` prints a warning to stderr that names `uv tool upgrade - simple_module_cli` and `uvx --from simple_module_cli@latest`. It never blocks the + simple_module_cli` (or `pipx upgrade`) and `uvx --from simple_module_cli@latest`. It never blocks the scaffold. - `smpy new` calls it right after validating the name. - `framework/cli/tests/conftest.py` gets an autouse fixture that makes the fetcher raise, so no CLI test touches the network. - Docs: - - `README.md` and `framework/cli/README.md` use `@latest`; - - the installation and quickstart guides say how to upgrade. + - `README.md`, `framework/cli/README.md`, `framework/hosting/README.md` and the host + landing page copy command (`Landing.tsx`) use `@latest`; + - the installation and quickstart guides, `docs/index.md` and + `docs/reference/make-commands.md` say how to upgrade. Rejected: an env var to opt out. The check is quiet whenever it can't run, and repo rules discourage new env-var configuration. @@ -31,7 +35,8 @@ discourage new env-var configuration. - Unit tests: - a newer version is reported; - - the same version, a newer local one and the placeholder are not; + - the same version, a newer local one, the placeholder, and a pre-release, dev or local + running version are not; - every failure type is skipped. - CLI tests: `smpy new` warns and still scaffolds, and is silent when PyPI is unreachable. - Against the real PyPI: `0.0.36` reports 0.0.44, and `0.0.44` reports nothing (0.16 s). diff --git a/framework/cli/simple_module_cli/_version_check.py b/framework/cli/simple_module_cli/_version_check.py index 80c26bd61..cb3f0ae62 100644 --- a/framework/cli/simple_module_cli/_version_check.py +++ b/framework/cli/simple_module_cli/_version_check.py @@ -28,7 +28,7 @@ def newer_release(current: str, fetcher: Fetcher | None = None) -> str | None: try: running = Version(current) if running.is_prerelease or running.is_devrelease or running.local: - return None # a checkout or local build is not what PyPI ships + return None # pre-release, dev or local build: not what PyPI ships latest = fetch_latest(CLI_DIST, include_pre=False, fetcher=fetcher or _quick_fetcher) if latest is not None and Version(latest) > running: return latest diff --git a/framework/cli/tests/test_cli_version_check.py b/framework/cli/tests/test_cli_version_check.py index f0cd5a4b4..4d3acc53d 100644 --- a/framework/cli/tests/test_cli_version_check.py +++ b/framework/cli/tests/test_cli_version_check.py @@ -23,7 +23,9 @@ def test_no_newer_release_when_current_or_placeholder(fake_pypi, current: str) - assert _version_check.newer_release(current, fetcher) is None -@pytest.mark.parametrize("error", [OSError("offline"), TimeoutError(), ValueError("bad json")]) +@pytest.mark.parametrize( + "error", [OSError("offline"), TimeoutError(), ValueError("bad json"), AttributeError("shape")] +) def test_any_lookup_failure_skips_the_check(error: Exception) -> None: def failing(url: str) -> dict: raise error