simple-module-cli 0.0.18__tar.gz → 0.0.20__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.
Files changed (101) hide show
  1. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/.gitignore +1 -0
  2. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/PKG-INFO +2 -2
  3. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/README.md +1 -1
  4. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/pyproject.toml +1 -1
  5. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/app_project.py +27 -55
  6. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/cli.py +40 -3
  7. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/new.py +3 -1
  8. simple_module_cli-0.0.20/simple_module_cli/pins.py +66 -0
  9. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/scaffolding.py +80 -4
  10. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/README.md +1 -1
  11. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-cli/SKILL.md +3 -2
  12. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-doctor/SKILL.md +7 -4
  13. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-registries/SKILL.md +2 -2
  14. simple_module_cli-0.0.20/simple_module_cli/templates/host/Makefile +55 -0
  15. simple_module_cli-0.0.20/simple_module_cli/templates/host/main.py +59 -0
  16. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/migrations/env.py +1 -4
  17. simple_module_cli-0.0.20/simple_module_cli/templates/host/pyproject.toml.tpl +51 -0
  18. simple_module_cli-0.0.20/simple_module_cli/templates/workspace/Makefile +68 -0
  19. simple_module_cli-0.0.20/simple_module_cli/templates/workspace/pyproject.toml.tpl +61 -0
  20. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_new.py +0 -82
  21. simple_module_cli-0.0.20/tests/test_cli_new_scaffold_layout.py +171 -0
  22. simple_module_cli-0.0.20/tests/test_create_module_ci_skip.py +107 -0
  23. simple_module_cli-0.0.20/tests/test_module_pages_manifest.py +70 -0
  24. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_scaffolding_host.py +86 -68
  25. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_scaffolding_module.py +57 -0
  26. simple_module_cli-0.0.18/simple_module_cli/templates/host/Makefile +0 -32
  27. simple_module_cli-0.0.18/simple_module_cli/templates/host/main.py +0 -46
  28. simple_module_cli-0.0.18/simple_module_cli/templates/host/pyproject.toml.tpl +0 -18
  29. simple_module_cli-0.0.18/simple_module_cli/templates/workspace/Makefile +0 -42
  30. simple_module_cli-0.0.18/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -17
  31. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/LICENSE +0 -0
  32. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/__init__.py +0 -0
  33. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/_env.py +0 -0
  34. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/case.py +0 -0
  35. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/catalog.py +0 -0
  36. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/package_update.py +0 -0
  37. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/plugins.py +0 -0
  38. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/recipes.py +0 -0
  39. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
  40. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
  41. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
  42. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
  43. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
  44. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
  45. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
  46. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/skills_cmd.py +0 -0
  47. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/.env.example +0 -0
  48. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/.gitignore +0 -0
  49. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/README.md.tpl +0 -0
  50. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
  51. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
  52. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
  53. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
  54. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
  55. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/alembic.ini +0 -0
  56. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
  57. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
  58. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
  59. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
  60. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
  61. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
  62. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/styles.css +0 -0
  63. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
  64. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
  65. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
  66. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
  67. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/routes.py +0 -0
  68. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/host/templates/index.html +0 -0
  69. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
  70. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
  71. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/.gitignore +0 -0
  72. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/README.md.tpl +0 -0
  73. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  74. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  75. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
  76. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
  77. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
  78. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
  79. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
  80. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/package.json.tpl +0 -0
  81. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
  82. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
  83. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
  84. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/.env.example +0 -0
  85. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/.gitignore +0 -0
  86. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
  87. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
  88. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/simple_module_cli/wizard.py +0 -0
  89. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_build_packaging.py +0 -0
  90. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_case.py +0 -0
  91. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_catalog.py +0 -0
  92. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_new_dest_tolerance.py +0 -0
  93. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_new_regressions.py +0 -0
  94. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_package_update.py +0 -0
  95. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_recipes.py +0 -0
  96. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_cli_wizard.py +0 -0
  97. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_env_helper.py +0 -0
  98. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_no_framework_deps.py +0 -0
  99. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_plugin_discovery.py +0 -0
  100. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_scaffold_rollback.py +0 -0
  101. {simple_module_cli-0.0.18 → simple_module_cli-0.0.20}/tests/test_skills_cmd.py +0 -0
@@ -61,3 +61,4 @@ Thumbs.db
61
61
  .playwright-mcp/*
62
62
  host/client_app/.playwright-cli/*
63
63
  .superpowers/
64
+ .qa/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple_module_cli
3
- Version: 0.0.18
3
+ Version: 0.0.20
4
4
  Summary: Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host.
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -48,7 +48,7 @@ smpy create-module my_feature # scaffold a publishable module package
48
48
  smpy create-host bare-host # scaffold a bare host (no opinionated wiring)
49
49
  ```
50
50
 
51
- Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`.
51
+ Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`, `smpy package-update`, and `smpy skills {list,add,update}`.
52
52
 
53
53
  When other framework packages are installed, they contribute additional subcommands via the `simple_module_cli.cli_plugins` entry-point group:
54
54
 
@@ -23,7 +23,7 @@ smpy create-module my_feature # scaffold a publishable module package
23
23
  smpy create-host bare-host # scaffold a bare host (no opinionated wiring)
24
24
  ```
25
25
 
26
- Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`.
26
+ Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`, `smpy package-update`, and `smpy skills {list,add,update}`.
27
27
 
28
28
  When other framework packages are installed, they contribute additional subcommands via the `simple_module_cli.cli_plugins` entry-point group:
29
29
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_cli"
3
- version = "0.0.18"
3
+ version = "0.0.20"
4
4
  description = "Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -15,10 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import json as _json
17
17
  import secrets as _secrets
18
- import shutil as _shutil
19
18
  from collections.abc import Sequence
20
- from importlib.metadata import PackageNotFoundError
21
- from importlib.metadata import version as _pkg_version
22
19
  from pathlib import Path
23
20
  from typing import Any
24
21
 
@@ -32,6 +29,7 @@ from simple_module_cli.scaffolding import (
32
29
  create_host,
33
30
  create_module,
34
31
  create_workspace,
32
+ resolve_framework_version,
35
33
  )
36
34
 
37
35
  __all__ = ["create_app_project"]
@@ -40,30 +38,21 @@ _SAMPLE_MODULE_NAME = "hello"
40
38
  _SAMPLE_MODULE_PKG = _module_to_pypi_name(_SAMPLE_MODULE_NAME)
41
39
 
42
40
 
43
- def _resolve_framework_version() -> str:
44
- """Resolve the framework version to pin scaffolded apps against.
45
-
46
- The CLI ships in lockstep with the rest of the framework (one
47
- ``bump_version.py`` rewrites every ``pyproject.toml`` in the repo), so
48
- its own installed version is the source of truth. Falling back to a
49
- placeholder lets editable installs without dist-info still scaffold —
50
- but that path should never be reached in a release wheel.
51
- """
52
- try:
53
- return _pkg_version("simple_module_cli")
54
- except PackageNotFoundError:
55
- return "0.0.0"
56
-
57
-
58
- _FRAMEWORK_VERSION = _resolve_framework_version()
41
+ _FRAMEWORK_VERSION = resolve_framework_version()
59
42
 
60
43
  # Pin ``simple_module_cli`` as a dev dep so ``uv run smpy`` resolves to the
61
44
  # project venv. The global ``uv tool`` install runs in its own isolated venv
62
- # that can't see the project's plugin entry points (issue #134).
45
+ # that can't see the project's plugin entry points (issue #134). The lint /
46
+ # test tooling (ruff, ty, pytest-*) backs the generated `make lint`/`make
47
+ # test` targets so a fresh app can run its own quality gates.
63
48
  _APP_PY_DEV_DEPS = [
64
49
  f"simple_module_test=={_FRAMEWORK_VERSION}",
65
50
  f"simple_module_cli=={_FRAMEWORK_VERSION}",
66
51
  "pytest>=8.0",
52
+ "pytest-asyncio>=0.24",
53
+ "pytest-playwright>=0.7.2",
54
+ "ruff>=0.8",
55
+ "ty>=0.0.29",
67
56
  ]
68
57
 
69
58
  _APP_NPM_DEPS = {
@@ -120,7 +109,12 @@ def create_app_project(
120
109
  preserved: list[Path] = []
121
110
  if not flat:
122
111
  preserved.extend(
123
- create_workspace(target, name=name, preserve_existing=SAFE_PRESERVED_NAMES)
112
+ create_workspace(
113
+ target,
114
+ name=name,
115
+ framework_version=_FRAMEWORK_VERSION,
116
+ preserve_existing=SAFE_PRESERVED_NAMES,
117
+ )
124
118
  )
125
119
  preserved.extend(
126
120
  create_host(
@@ -195,11 +189,18 @@ def _scaffold_sample_module(target: Path) -> None:
195
189
  sample_dest = target / "modules" / _SAMPLE_MODULE_NAME
196
190
  if sample_dest.exists():
197
191
  return
198
- create_module(sample_dest, name=_SAMPLE_MODULE_NAME)
199
- # GitHub only reads workflows from the repo root, so the template's
200
- # .github/ is dead inside a workspace.
201
- _shutil.rmtree(sample_dest / ".github")
202
- _pin_sample_module_deps(sample_dest)
192
+ # Pin the sample's framework deps to the exact framework version so the
193
+ # workspace resolves (the template's >=1.0,<2.0 ranges don't exist on PyPI
194
+ # pre-1.0). See GH #195.
195
+ # The sample lives inside the workspace, so it gets no per-module .github/:
196
+ # GitHub only reads workflows from the repo root, where the template's
197
+ # .github/ would be dead anyway. See GH #210.
198
+ create_module(
199
+ sample_dest,
200
+ name=_SAMPLE_MODULE_NAME,
201
+ framework_version=_FRAMEWORK_VERSION,
202
+ include_ci=False,
203
+ )
203
204
  _seed_static_dist_placeholder(sample_dest / _SAMPLE_MODULE_NAME / "static" / "dist")
204
205
 
205
206
 
@@ -210,35 +211,6 @@ def _seed_static_dist_placeholder(static_dist: Path) -> None:
210
211
  (static_dist / ".gitkeep").touch()
211
212
 
212
213
 
213
- def _pin_sample_module_deps(sample_dest: Path) -> None:
214
- """Replace the module template's future-API range pins with exact pins.
215
-
216
- The shared ``smpy create-module`` template ships ``>=1.0,<2.0`` against the
217
- framework's eventual stable line, but the workspace-bundled sample has to
218
- resolve against whatever the framework version actually is today (``==X``
219
- in pre-1.0). Without rewriting, ``uv sync`` can't satisfy the workspace.
220
- """
221
- import tomlkit
222
-
223
- pyproject = sample_dest / "pyproject.toml"
224
- doc = tomlkit.parse(pyproject.read_text(encoding="utf-8"))
225
- project = doc.setdefault("project", tomlkit.table())
226
- project["dependencies"] = [_pin_or_keep(dep) for dep in project.get("dependencies", [])]
227
- optional = project.get("optional-dependencies")
228
- if optional is not None:
229
- for extra, deps in list(optional.items()):
230
- optional[extra] = [_pin_or_keep(dep) for dep in deps]
231
- pyproject.write_text(tomlkit.dumps(doc), encoding="utf-8")
232
-
233
-
234
- def _pin_or_keep(dep: str) -> str:
235
- """Pin a ``simple_module_*`` requirement to the framework version; pass through otherwise."""
236
- pkg = dep.split(">=", 1)[0].split("==", 1)[0].split("<", 1)[0].strip()
237
- if pkg.startswith(("simple_module_", "simple-module-")):
238
- return f"{pkg}=={_FRAMEWORK_VERSION}"
239
- return dep
240
-
241
-
242
214
  def _db_url(db: str, slug: str, *, flat: bool) -> str:
243
215
  if db == "postgres":
244
216
  return f"postgresql+asyncpg://postgres:postgres@localhost:5432/{slug}"
@@ -23,6 +23,7 @@ from simple_module_cli.package_update import package_update
23
23
  from simple_module_cli.plugins import discover_and_mount
24
24
  from simple_module_cli.scaffolding import create_host as _create_host
25
25
  from simple_module_cli.scaffolding import create_module as _create_module
26
+ from simple_module_cli.scaffolding import is_inside_existing_repo, resolve_framework_version
26
27
  from simple_module_cli.skills_cmd import app as skills_app
27
28
 
28
29
  app = typer.Typer(
@@ -55,7 +56,17 @@ def create_host(
55
56
  target = dest or Path.cwd() / name
56
57
  selected = [m.strip() for m in modules.split(",") if m.strip()]
57
58
  try:
58
- _create_host(target, name=name, modules=selected)
59
+ # Pin the host's framework + module deps to the installed framework
60
+ # version so the generated host's first `uv sync` resolves (the
61
+ # template's >=1.0,<2.0 / >=0.1,<1.0 ranges don't exist pre-1.0). The
62
+ # workspace `smpy new` path rewrites these via _rewrite_pyproject, but
63
+ # standalone create-host never did — see GH #206.
64
+ _create_host(
65
+ target,
66
+ name=name,
67
+ modules=selected,
68
+ framework_version=resolve_framework_version(),
69
+ )
59
70
  except FileExistsError as exc:
60
71
  typer.echo(f"ERROR: {exc}", err=True)
61
72
  raise typer.Exit(code=1) from exc
@@ -68,7 +79,7 @@ def create_host(
68
79
  typer.echo(" uv sync")
69
80
  typer.echo(" cp .env.example .env")
70
81
  typer.echo(' alembic revision --autogenerate -m "initial schema"')
71
- typer.echo(" alembic upgrade head")
82
+ typer.echo(" alembic upgrade heads")
72
83
  typer.echo(" python main.py")
73
84
 
74
85
 
@@ -79,18 +90,44 @@ def create_module(
79
90
  Path | None,
80
91
  typer.Option("--dest", help="Destination dir. Defaults to ./simple_module_<name>."),
81
92
  ] = None,
93
+ standalone: Annotated[
94
+ bool,
95
+ typer.Option(
96
+ "--standalone",
97
+ help="Emit the module's own .github/ CI + PyPI publish workflows. "
98
+ "By default they are omitted when the module lands inside an "
99
+ "existing repo/host (nested workflows never run there).",
100
+ ),
101
+ ] = False,
82
102
  ) -> None:
83
103
  """Scaffold a publishable SimpleModule module package."""
84
104
  slug = to_kebab_case(name)
85
105
  package = slug.replace("-", "_")
86
106
  target = dest or Path.cwd() / f"simple_module_{package}"
107
+ # An in-repo module (the documented modules/* layout) gets no .github/: those
108
+ # nested workflows never run and publish.yml is a PyPI footgun. --standalone
109
+ # forces them for a module that lives in its own repo. See GH #210.
110
+ include_ci = standalone or not is_inside_existing_repo(target)
87
111
  try:
88
- _create_module(target, name=name)
112
+ # Pin framework deps to the installed framework version so the module
113
+ # resolves against the app that created it (the template's >=1.0,<2.0
114
+ # ranges don't exist on PyPI pre-1.0). See GH #195.
115
+ _create_module(
116
+ target,
117
+ name=name,
118
+ framework_version=resolve_framework_version(),
119
+ include_ci=include_ci,
120
+ )
89
121
  except FileExistsError as exc:
90
122
  typer.echo(f"ERROR: {exc}", err=True)
91
123
  raise typer.Exit(code=1) from exc
92
124
 
93
125
  typer.echo(f"Created module 'simple_module_{package}' at {target}")
126
+ if not include_ci:
127
+ typer.echo(
128
+ "Skipped .github/ workflows: this module is inside an existing repo, "
129
+ "where nested workflows never run. Use --standalone to emit them."
130
+ )
94
131
  typer.echo("\nNext steps:")
95
132
  typer.echo(f" cd {target}")
96
133
  typer.echo(" uv sync --extra dev")
@@ -164,7 +164,9 @@ def new_project(
164
164
  return
165
165
 
166
166
  _bootstrap_initial_migration(host_dir)
167
- subprocess.run([*_ALEMBIC, "upgrade", "head"], cwd=host_dir, check=False)
167
+ # `heads` (plural) applies every per-module branch head; `head` (singular)
168
+ # errors once a second module ships its own migration branch label.
169
+ subprocess.run([*_ALEMBIC, "upgrade", "heads"], cwd=host_dir, check=False)
168
170
  typer.echo("\nSetup complete. Run `make dev` in the new directory.")
169
171
  if "background_tasks" in resolved:
170
172
  typer.echo("For background jobs, also run: docker compose up -d redis worker beat")
@@ -0,0 +1,66 @@
1
+ """Resolve and pin the framework version that scaffolded apps depend on.
2
+
3
+ Kept separate from :mod:`simple_module_cli.scaffolding` (template
4
+ materialization) because version resolution + dependency pinning is its own
5
+ concern: the scaffolders, ``cli`` commands, and ``app_project`` all reach for
6
+ it independently. The module templates ship forward-looking ``simple_module_*``
7
+ ranges (``>=1.0,<2.0`` / ``>=0.1,<1.0``) against the framework's eventual stable
8
+ line, but the published distributions are pre-1.0 (``0.0.x``), so those ranges
9
+ resolve to nothing on PyPI. Rewriting them to an exact ``==`` pin lets a freshly
10
+ scaffolded app/module resolve against the framework version that created it.
11
+ See GH #195 / #206.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+
18
+
19
+ def resolve_framework_version() -> str:
20
+ """Resolve the framework version that scaffolded apps should pin against.
21
+
22
+ The CLI ships in lockstep with the rest of the framework (one
23
+ ``bump_version.py`` rewrites every ``pyproject.toml``), so its own
24
+ installed distribution version is the source of truth. Falls back to a
25
+ placeholder for editable installs lacking dist-info — never reached from a
26
+ release wheel.
27
+ """
28
+ from importlib.metadata import PackageNotFoundError
29
+ from importlib.metadata import version as pkg_version
30
+
31
+ try:
32
+ return pkg_version("simple_module_cli")
33
+ except PackageNotFoundError:
34
+ return "0.0.0"
35
+
36
+
37
+ def _pin_one(dep: str, version: str) -> str:
38
+ """Pin a single ``simple_module_*`` requirement to ``==version``; else pass through."""
39
+ pkg = dep.split(">=", 1)[0].split("==", 1)[0].split("<", 1)[0].strip()
40
+ if pkg.startswith(("simple_module_", "simple-module-")):
41
+ return f"{pkg}=={version}"
42
+ return dep
43
+
44
+
45
+ def pin_framework_deps(pyproject_path: Path, version: str) -> None:
46
+ """Pin every ``simple_module_*`` requirement in a pyproject to ``==version``.
47
+
48
+ Rewrites both ``dependencies`` and every ``optional-dependencies`` extra
49
+ (the module template's ``dev`` extra pins ``simple_module_test``). Used by
50
+ ``create_module`` and ``create_host`` so a freshly scaffolded package
51
+ resolves against the framework version that created it. See GH #195 / #206.
52
+ """
53
+ import tomlkit
54
+
55
+ doc = tomlkit.parse(pyproject_path.read_text(encoding="utf-8"))
56
+ project = doc.get("project")
57
+ if project is None:
58
+ return
59
+ deps = project.get("dependencies")
60
+ if deps is not None:
61
+ project["dependencies"] = [_pin_one(dep, version) for dep in deps]
62
+ optional = project.get("optional-dependencies")
63
+ if optional is not None:
64
+ for extra, items in list(optional.items()):
65
+ optional[extra] = [_pin_one(dep, version) for dep in items]
66
+ pyproject_path.write_text(tomlkit.dumps(doc), encoding="utf-8")
@@ -28,11 +28,18 @@ from simple_module_cli.case import (
28
28
  validate_scaffold_name,
29
29
  )
30
30
 
31
+ # Re-exported so the scaffolders and their long-standing callers keep importing
32
+ # version pinning from one place; the implementations live in pins.py.
33
+ from simple_module_cli.pins import pin_framework_deps, resolve_framework_version
34
+
31
35
  __all__ = [
32
36
  "SAFE_PRESERVED_NAMES",
33
37
  "create_host",
34
38
  "create_module",
35
39
  "create_workspace",
40
+ "is_inside_existing_repo",
41
+ "pin_framework_deps",
42
+ "resolve_framework_version",
36
43
  ]
37
44
 
38
45
  logger = logging.getLogger(__name__)
@@ -55,6 +62,42 @@ def _module_to_pypi_name(name: str) -> str:
55
62
  return f"simple_module_{name.lower()}"
56
63
 
57
64
 
65
+ def is_inside_existing_repo(dest: Path) -> bool:
66
+ """Return True when ``dest`` lands inside an existing repo / host project.
67
+
68
+ A module scaffolded under an existing host application (the documented
69
+ monorepo ``modules/*`` layout) is an *in-repo* module: GitHub only runs
70
+ workflows from the repository-root ``.github/workflows/``, so a per-module
71
+ ``.github/`` is dead weight there — and the bundled ``publish.yml`` (which
72
+ publishes ``simple_module_<name>`` to PyPI on any ``v*`` tag) is a footgun if
73
+ it ever surfaces at the repo root. We detect this by walking up from
74
+ ``dest``'s parent for a ``.git`` directory or a ``pyproject.toml`` (an
75
+ existing repo / host / workspace member).
76
+
77
+ ``dest`` itself is *excluded* from the walk — the module's own scaffolded
78
+ ``pyproject.toml`` must not count as "an existing host". A truly standalone
79
+ target (no repo/pyproject above it) returns False. See GH #210.
80
+ """
81
+ # ``resolve()`` allows ``dest`` to not exist yet; the walk is over its
82
+ # absolute parents so a relative ``--dest`` is handled the same way.
83
+ start = Path(dest).resolve().parent
84
+ for parent in (start, *start.parents):
85
+ if (parent / ".git").exists() or (parent / "pyproject.toml").is_file():
86
+ return True
87
+ return False
88
+
89
+
90
+ def _should_pin_framework_version(version: str | None) -> bool:
91
+ """Whether ``version`` is a concrete pin rather than a skip sentinel.
92
+
93
+ Both scaffolders skip pinning for ``None`` (a library caller that wants the
94
+ template's ranges kept verbatim) and ``"*"`` (the npm-wildcard default,
95
+ which would otherwise render the invalid PEP 508 specifier ``==*``). One
96
+ rule, so the two paths can't drift. See GH #195 / #206.
97
+ """
98
+ return bool(version) and version != "*"
99
+
100
+
58
101
  def _iter_template_files(template_root: Path):
59
102
  """Yield every file under ``template_root``. Skips ``_optional/`` paths."""
60
103
  for path in template_root.rglob("*"):
@@ -124,15 +167,20 @@ def create_workspace(
124
167
  dest: Path,
125
168
  name: str,
126
169
  template_root: Path | None = None,
170
+ framework_version: str = "*",
127
171
  *,
128
172
  preserve_existing: frozenset[str] = frozenset(),
129
173
  ) -> list[Path]:
130
174
  """Materialize the workspace-root shell at ``dest``; return preserved paths.
131
175
 
132
- Lays down the top-level ``pyproject.toml`` (uv workspace), ``package.json``
133
- (npm workspace), ``Makefile`` (delegates to host), ``.env.example``,
134
- ``.gitignore``, and ``README.md``. Does NOT create the host or any
135
- modules — those go under ``dest/host`` and ``dest/modules/`` afterwards.
176
+ Lays down the top-level ``pyproject.toml`` (uv workspace + dev tooling for
177
+ ``make test``/``lint``), ``package.json`` (npm workspace), ``Makefile``
178
+ (delegates to host), ``.env.example``, ``.gitignore``, and ``README.md``.
179
+ Does NOT create the host or any modules — those go under ``dest/host`` and
180
+ ``dest/modules/`` afterwards.
181
+
182
+ ``framework_version`` pins ``simple_module_test`` in the root dev group;
183
+ defaults to ``"*"`` for callers that don't need an exact pin.
136
184
 
137
185
  ``preserve_existing`` lists top-level entry names that may already exist
138
186
  in ``dest``; the scaffold's copy is skipped and the preserved path is
@@ -147,6 +195,7 @@ def create_workspace(
147
195
  {
148
196
  "{{HOST_NAME}}": validate_scaffold_name(name),
149
197
  "{{HOST_PYPI_NAME}}": to_kebab_case(name),
198
+ "{{FRAMEWORK_VERSION}}": framework_version,
150
199
  },
151
200
  preserve_existing=preserve_existing,
152
201
  )
@@ -181,6 +230,12 @@ def create_host(
181
230
  },
182
231
  preserve_existing=preserve_existing,
183
232
  )
233
+ # Pin every simple_module_* host dep (framework packages *and* selected
234
+ # bundled modules) to the lockstep version so the host's first `uv sync`
235
+ # resolves — the template's >=1.0,<2.0 / >=0.1,<1.0 ranges match nothing
236
+ # against pre-1.0 dists. See GH #206.
237
+ if _should_pin_framework_version(framework_version):
238
+ pin_framework_deps(dest / "pyproject.toml", framework_version)
184
239
  logger.info(
185
240
  "Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
186
241
  )
@@ -191,7 +246,24 @@ def create_module(
191
246
  dest: Path,
192
247
  name: str,
193
248
  template_root: Path | None = None,
249
+ *,
250
+ framework_version: str | None = None,
251
+ include_ci: bool = True,
194
252
  ) -> Path:
253
+ """Scaffold a module package at ``dest``.
254
+
255
+ When ``framework_version`` is a concrete version, the template's
256
+ forward-looking ``simple_module_*`` ranges are rewritten to an exact pin so
257
+ the module resolves against that framework version (e.g. ``uv add`` into the
258
+ workspace that created it). Left as ``None`` (or the ``"*"`` sentinel), the
259
+ template's ranges are kept verbatim. See GH #195.
260
+
261
+ When ``include_ci`` is False, the scaffolded ``.github/`` (CI + PyPI publish
262
+ workflows) is omitted. Those nested workflows never run inside an existing
263
+ host repo (GitHub only reads the repo-root ``.github/``) and ``publish.yml``
264
+ is a footgun there, so callers creating an *in-repo* module pass
265
+ ``include_ci=False``. See GH #210.
266
+ """
195
267
  dest = Path(dest)
196
268
  existed_before = dest.exists()
197
269
  _require_empty_dest(dest)
@@ -210,6 +282,10 @@ def create_module(
210
282
  },
211
283
  path_rewrites={_PACKAGE_PATH_TOKEN: package_name},
212
284
  )
285
+ if not include_ci:
286
+ shutil.rmtree(dest / ".github", ignore_errors=True)
287
+ if _should_pin_framework_version(framework_version):
288
+ pin_framework_deps(dest / "pyproject.toml", framework_version)
213
289
  except Exception:
214
290
  # Rollback so a half-scaffolded directory doesn't leave the user
215
291
  # with an unparseable Python package and the impression that a
@@ -47,7 +47,7 @@ The CLI is [vercel-labs/skills](https://github.com/vercel-labs/skills); see its
47
47
  | [simple-module-locales](./simple-module-locales/SKILL.md) | Adding or debugging i18n in a module — `locale_dirs()`, namespaces, CLDR plurals, the Zod-in-hook rule |
48
48
  | [simple-module-registries](./simple-module-registries/SKILL.md) | Contributing menu items, permissions, feature flags, or events from a module |
49
49
  | [simple-module-testing](./simple-module-testing/SKILL.md) | Writing pytest tests — picking the right fixture (`db_session` / `app` / `authenticated_client`), single-test runs, e2e |
50
- | [simple-module-doctor](./simple-module-doctor/SKILL.md) | Interpreting a diagnostic code (`SM001`–`SM018`) printed at boot |
50
+ | [simple-module-doctor](./simple-module-doctor/SKILL.md) | Interpreting a diagnostic code (`SM001`–`SM021`) printed at boot |
51
51
 
52
52
  The skills are designed to stand alone — install them into any host or module-package project and they'll work without needing access to the framework's source repo.
53
53
 
@@ -5,7 +5,7 @@ description: Use when invoking the `smpy` CLI for a simple_module_python project
5
5
 
6
6
  # simple_module_python: the `smpy` CLI
7
7
 
8
- The `smpy` command is provided by `simple_module_cli` (installed as a dep of `simple_module_hosting`). It groups four kinds of operations: scaffolding new things, project-time helpers for the host, admin shortcuts for the bundled modules, and installing the bundled agent skills.
8
+ The `smpy` command is provided by `simple_module_cli` (installed as a dep of `simple_module_hosting`). It groups several kinds of operations: scaffolding new things, project-time helpers for the host, admin shortcuts for the bundled modules, installing the bundled agent skills, and bumping `simple_module_*` deps.
9
9
 
10
10
  ## Top-level commands
11
11
 
@@ -14,6 +14,7 @@ The `smpy` command is provided by `simple_module_cli` (installed as a dep of `si
14
14
  | `smpy new <name>` | Greenfield: scaffold a complete app (host + selected modules) in one shot, with an interactive wizard for DB / tenancy / module preset |
15
15
  | `smpy create-host <name>` | You want just a bare host project; you'll add modules later by `pip install`-ing them |
16
16
  | `smpy create-module <name>` | You're authoring a publishable module package (separate repo, distributed via PyPI) |
17
+ | `smpy package-update` | Bump every `simple_module_*` / `simple-module-*` dependency in `pyproject.toml` to its latest non-yanked PyPI release (workspace/path/git/URL sources are left untouched) |
17
18
  | `smpy skills …` | Install / update the bundled agent-skill packs into a project (`add`, `list`, `update`) |
18
19
  | `smpy host …` | Project-time helpers run from inside a host directory (page manifest, JS dep sync) |
19
20
  | `smpy settings …` | Settings-module admin — currently `import-from-env` |
@@ -47,7 +48,7 @@ smpy new MyApp --no-install
47
48
  | `full` | every module in the catalog |
48
49
  | `custom` | interactive — pick each module yes/no |
49
50
 
50
- `--with` accepts a comma-separated list of catalog keys (`auth, users, permissions, products, dashboard, settings, feature_flags, file_storage, background_tasks, …`). Transitive `requires` are auto-added; the wizard prints `Added X (required by Y)` so you can see what got pulled in.
51
+ `--with` accepts a comma-separated list of catalog keys (`auth, users, permissions, dashboard, settings, feature_flags, file_storage, background_tasks`). Transitive `requires` are auto-added; the wizard prints `Added X (required by Y)` so you can see what got pulled in.
51
52
 
52
53
  **Options summary:**
53
54
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: simple-module-doctor
3
- description: Use when interpreting a simple_module_python diagnostic code (SM001–SM018) printed at boot or by the diagnostics runner — what the code means, whether it's blocking, and the concrete fix. Triggers on any "SMnnn" code in logs, "InvalidModuleError", "module silently doesn't load", or "production failed boot".
3
+ description: Use when interpreting a simple_module_python diagnostic code (SM001–SM021) printed at boot or by the diagnostics runner — what the code means, whether it's blocking, and the concrete fix. Triggers on any "SMnnn" code in logs, "InvalidModuleError", "module silently doesn't load", or "production failed boot".
4
4
  ---
5
5
 
6
6
  # simple_module_python: diagnostics
@@ -17,7 +17,7 @@ You can also call `run_diagnostics(...)` from `simple_module_core` programmatica
17
17
  | **SM003** | WARNING | A `pages/<Name>.tsx` exists but no `inertia.render("<Module>/<Name>", ...)` references it | Either delete the orphan file or wire up the render call |
18
18
  | **SM004** | WARNING | `inertia.render("<Module>/<Name>", ...)` is called but no matching `.tsx` exists | Fix the render-key typo or create the page file |
19
19
  | **SM007** | INFO | Module overrides no `register_*` hooks at all — likely scaffolded but empty | Either implement at least one hook or delete the module if unused |
20
- | **SM008** | ERROR | Two modules declare the same `ModuleMeta.name` (Postgres-schema / SQLite-prefix collision) | Rename one module's `meta.name` |
20
+ | **SM008** | ERROR | Two modules declare the same `ModuleMeta.name` (their lowercased names would collide as table prefixes in the shared schema) | Rename one module's `meta.name` |
21
21
  | **SM009** | ERROR | A `framework/*` package directly imports from a plugin module (`modules/*`) | Move the symbol *up* into `simple_module_core` / `simple_module_hosting`, or invert the dependency via the event bus / a registry |
22
22
  | **SM010** | ERROR | Live DB revision is behind the migration head | Run `alembic upgrade head` before booting; in CI, ensure migrations are part of the deploy step |
23
23
  | **SM011** | WARNING | A module declares a SQLModel table that has no entry in migration history | Run `alembic revision --autogenerate -m "..."` and apply |
@@ -27,9 +27,12 @@ You can also call `run_diagnostics(...)` from `simple_module_core` programmatica
27
27
  | **SM015** | WARNING | A non-default locale has keys *not* present in the default | Remove dead keys from the non-default file or add them to default |
28
28
  | **SM016** | ERROR | A locale JSON file is invalid or contains non-string leaves | Fix the JSON; only string leaves are allowed (interpolation is `{name}` placeholders) |
29
29
  | **SM017** | WARNING | A module ships `.tsx` pages but is missing `package.json` / `tsconfig.json` | Add the JS workspace files so the host's frontend toolchain can resolve module imports |
30
- | **SM018** | WARNING | An Inertia page calls `router.{post,patch,put,delete}()` targeting `/api/*` | Use plain `fetch()` for JSON endpoints; Inertia's client expects an Inertia response, not JSON |
30
+ | **SM018** | WARNING | An Inertia page calls `router.{post,patch,put,delete}()` targeting `/api/*` | Point the call at a view endpoint that returns `RedirectResponse(..., status_code=303)`, or use plain `fetch()` for the JSON endpoint; Inertia's client expects an Inertia response, not JSON |
31
+ | **SM019** | WARNING | A module registers view routes (non-empty `view_prefix` + overrides `register_routes`) but overrides neither `register_menu_items` nor `register_permissions` — its pages exist with no sidebar entry and no role-editor visibility | Add `register_menu_items()` (sidebar) or `register_permissions()` (role editor), or clear `view_prefix` if the module is API-only |
32
+ | **SM020** | ERROR | More than one auth provider module is installed | Install exactly one auth provider (e.g. `users` OR `keycloak`, not both) |
33
+ | **SM021** | WARNING | No auth provider module is installed | Install an auth provider module (e.g. `simple-module-users` or `simple-module-keycloak`) |
31
34
 
32
- Codes `SM002`, `SM005`, `SM006` are reserved/retired don't try to look them up. Output format is one line per finding, e.g. `[SM009] ERROR: <subject>`.
35
+ Codes `SM002`, `SM005`, `SM006`, `SM012` are not part of this table — `SM002`/`SM005`/`SM006` are reserved/retired, and `SM012` (`register_settings` overridden but nothing on `app.state.<module_lower>`, WARNING, dev boot only) is raised from the hosting layer, not the core diagnostics runner. Output format is one line per finding, e.g. `[SM009] ERROR: <subject>`.
33
36
 
34
37
  Warnings are load-bearing: the framework only emits one when something concrete *will* break under a specific condition (locale switch, schema downgrade, deploy ordering). Ignored long enough they become the next on-call page. If you're suppressing warnings in CI to make it green, you're trading the CI signal for a production-boot failure later.
35
38
 
@@ -47,7 +47,7 @@ class OrdersModule(ModuleBase):
47
47
 
48
48
  The runtime expansion (role → permissions) is cached. `register_permissions` is called once at boot; mutating the registry afterwards bypasses the cache and invalidates user sessions until the cache TTL elapses. Don't mutate at request time.
49
49
 
50
- To check inside an endpoint, depend on the `RequireAnyPermissionDep` / `RequireAllPermissionsDep` dependencies (see auth/users), not by reading the registry by hand.
50
+ To check inside an endpoint, depend on `RequiresPermission("<module>.<action>")` (from `simple_module_hosting.permissions`), not by reading the registry by hand. The dependency handles wildcard expansion and the 401-vs-403 distinction.
51
51
 
52
52
  ## Feature flags — `register_feature_flags(registry: FeatureFlagRegistry)`
53
53
 
@@ -132,7 +132,7 @@ Module A consuming Module B's events should import only from `b.contracts.events
132
132
  ## Pitfalls
133
133
 
134
134
  - **Mutated a registry after boot.** Boot-phase only. Cached views (menus, role→permission map) aren't invalidated for live requests; mutations look fine in dev with auto-reload and silently rot in prod.
135
- - **Raw permission strings in endpoints (`request.state.user.permissions`).** Use `RequireAnyPermissionDep` / `RequireAllPermissionsDep`. The dependency handles wildcard expansion and 401 vs 403 distinction.
135
+ - **Raw permission strings in endpoints (`request.state.user.permissions`).** Use `RequiresPermission(...)`. The dependency handles wildcard expansion and the 401-vs-403 distinction.
136
136
  - **Forgot a feature flag's `default_enabled=False`.** A flag added with `default_enabled=True` is on for every tenant on first deploy — defeats the point of gating a rollout. Default to `False`; flip via override after the rollout window.
137
137
  - **Subscribed to an event in `register_settings` instead of `register_event_handlers`.** `register_settings` runs **before** the event bus is constructed; the subscription silently no-ops.
138
138
  - **Used `publish_nowait` inside a request handler that needs the listener to commit a DB row in the same transaction.** It returns immediately — the handler runs after the request has already committed/rolled back. For "in this request, do X then Y", just call Y directly.
@@ -0,0 +1,55 @@
1
+ .PHONY: install dev dev-api dev-ui build test test-py test-js lint doctor migrate migration gen-pages sync-js-deps
2
+
3
+ install:
4
+ uv sync
5
+ cd client_app && npm install
6
+ $(MAKE) sync-js-deps
7
+
8
+ dev: gen-pages
9
+ @echo "Starting API and UI dev servers..."
10
+ $(MAKE) -j2 dev-api dev-ui
11
+
12
+ dev-api:
13
+ uv run uvicorn main:app --reload --port 8000
14
+
15
+ dev-ui:
16
+ cd client_app && npm run dev
17
+
18
+ build:
19
+ cd client_app && npm run build
20
+
21
+ # Testing
22
+ test: test-py test-js
23
+
24
+ test-py:
25
+ uv run pytest
26
+
27
+ # --if-present is a no-op until you add a "test" script (e.g. vitest) to package.json.
28
+ test-js:
29
+ cd client_app && npm run test --if-present
30
+
31
+ # Lint + typecheck (Python). Mirrors the framework's own quality gate.
32
+ lint:
33
+ uv run ruff format --check .
34
+ uv run ruff check .
35
+ uv run ty check
36
+
37
+ # Module diagnostics — the same checks that run at prod boot (orphan pages,
38
+ # coupling violations, migration drift, locale issues).
39
+ doctor:
40
+ uv run python -m simple_module_core
41
+
42
+ gen-pages:
43
+ uv run python -m simple_module_hosting gen-pages --host-dir=client_app
44
+
45
+ sync-js-deps:
46
+ uv run python -m simple_module_hosting sync-js-deps --host-client-app=client_app
47
+
48
+ # `upgrade heads` (plural) applies every per-module branch head; `upgrade head`
49
+ # (singular) errors once a second module adds its own migration branch label.
50
+ migrate:
51
+ uv run alembic upgrade heads
52
+
53
+ migration:
54
+ @test -n "$(msg)" || (echo 'Usage: make migration msg="describe the change"' && exit 1)
55
+ uv run alembic revision --autogenerate -m "$(msg)"