simple-module-hosting 0.0.1__tar.gz → 0.0.3__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 (81) hide show
  1. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/.gitignore +4 -0
  2. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/PKG-INFO +10 -14
  3. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/README.md +7 -11
  4. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/pyproject.toml +5 -6
  5. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/app_builder.py +2 -2
  6. simple_module_hosting-0.0.3/simple_module_hosting/host_cli.py +117 -0
  7. simple_module_hosting-0.0.3/tests/test_host_cli.py +28 -0
  8. simple_module_hosting-0.0.1/simple_module_hosting/cli.py +0 -292
  9. simple_module_hosting-0.0.1/simple_module_hosting/scaffolding.py +0 -294
  10. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/.env.example +0 -20
  11. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/.gitignore +0 -19
  12. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/Makefile +0 -24
  13. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/README.md.tpl +0 -59
  14. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/alembic.ini +0 -36
  15. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/app.tsx +0 -16
  16. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/main.tsx +0 -2
  17. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/package.json.tpl +0 -23
  18. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/pages/Error.tsx +0 -13
  19. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/pages.ts +0 -47
  20. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/styles.css +0 -7
  21. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/tsconfig.json +0 -16
  22. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/client_app/vite.config.ts +0 -39
  23. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/main.py +0 -27
  24. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/migrations/env.py +0 -80
  25. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/migrations/script.py.mako +0 -26
  26. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/migrations/versions/.gitkeep +0 -1
  27. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/pyproject.toml.tpl +0 -17
  28. simple_module_hosting-0.0.1/simple_module_hosting/templates/host/templates/index.html +0 -12
  29. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/.github/workflows/ci.yml +0 -32
  30. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/.github/workflows/publish.yml.tpl +0 -52
  31. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/.gitignore +0 -14
  32. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/README.md.tpl +0 -82
  33. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/__init__.py +0 -0
  34. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  35. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -11
  36. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/module.py.tpl +0 -46
  37. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/pages/.gitkeep +0 -1
  38. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/__PACKAGE__/services.py.tpl +0 -22
  39. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/package.json.tpl +0 -16
  40. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/pyproject.toml.tpl +0 -39
  41. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/tests/__init__.py +0 -0
  42. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/tests/test_module.py.tpl +0 -27
  43. simple_module_hosting-0.0.1/simple_module_hosting/templates/module/tsconfig.json.tpl +0 -11
  44. simple_module_hosting-0.0.1/tests/test_cli_new.py +0 -78
  45. simple_module_hosting-0.0.1/tests/test_scaffolding_host.py +0 -171
  46. simple_module_hosting-0.0.1/tests/test_scaffolding_module.py +0 -179
  47. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/LICENSE +0 -0
  48. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/__init__.py +0 -0
  49. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_error_handlers.py +0 -0
  50. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_hydrate_step.py +0 -0
  51. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_inertia_setup.py +0 -0
  52. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_inertia_shared.py +0 -0
  53. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_observability.py +0 -0
  54. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/_phase_helpers.py +0 -0
  55. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/bootstrap_settings.py +0 -0
  56. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/health.py +0 -0
  57. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/host_settings.py +0 -0
  58. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/i18n_deps.py +0 -0
  59. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/i18n_manifest.py +0 -0
  60. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/i18n_middleware.py +0 -0
  61. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/inertia_deps.py +0 -0
  62. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/inertia_utils.py +0 -0
  63. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/logging.py +0 -0
  64. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/manifest.py +0 -0
  65. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/middleware.py +0 -0
  66. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/migrations.py +0 -0
  67. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/permissions.py +0 -0
  68. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/py.typed +0 -0
  69. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/redirects.py +0 -0
  70. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/simple_module_hosting/settings.py +0 -0
  71. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_app.py +0 -0
  72. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_health.py +0 -0
  73. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_hosting_permissions.py +0 -0
  74. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_i18n_manifest.py +0 -0
  75. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_inertia_i18n_shared_props.py +0 -0
  76. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_locale_middleware.py +0 -0
  77. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_logging.py +0 -0
  78. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_settings_i18n.py +0 -0
  79. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_settings_secrets.py +0 -0
  80. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_tenant_middleware.py +0 -0
  81. {simple_module_hosting-0.0.1 → simple_module_hosting-0.0.3}/tests/test_translator_dep.py +0 -0
@@ -36,6 +36,10 @@ uploads/
36
36
  # Vite
37
37
  host/static/dist/
38
38
 
39
+ # VitePress (docs)
40
+ docs/.vitepress/cache/
41
+ docs/.vitepress/dist/
42
+
39
43
  # Auto-generated frontend module manifest (regenerated by the host at boot
40
44
  # or via `make gen-pages`).
41
45
  host/client_app/modules.manifest.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple_module_hosting
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -26,8 +26,8 @@ Requires-Dist: fastapi-inertia>=1.0
26
26
  Requires-Dist: fastapi>=0.115
27
27
  Requires-Dist: httpx>=0.27
28
28
  Requires-Dist: jinja2>=3.1
29
- Requires-Dist: simple-module-core==0.0.1
30
- Requires-Dist: simple-module-db==0.0.1
29
+ Requires-Dist: simple-module-core==0.0.3
30
+ Requires-Dist: simple-module-db==0.0.3
31
31
  Requires-Dist: starlette>=0.44
32
32
  Requires-Dist: tomlkit>=0.13
33
33
  Requires-Dist: uvicorn[standard]>=0.34
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  # simple_module_hosting
37
37
 
38
- FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, exposes the `sm` / `simple-module` CLI, and ships the project scaffolder.
38
+ FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, and contributes the `sm host` plugin to the standalone `sm` CLI.
39
39
 
40
40
  ## Install
41
41
 
@@ -43,10 +43,10 @@ FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/ant
43
43
  pip install simple_module_hosting
44
44
  ```
45
45
 
46
- For a new project, most users run the generator instead:
46
+ For a new project, most users run the generator instead (shipped by the standalone `simple_module_cli` distribution):
47
47
 
48
48
  ```bash
49
- uvx simple-module new my-app
49
+ uvx --from simple_module_cli sm new my-app
50
50
  ```
51
51
 
52
52
  ## What it provides
@@ -54,8 +54,7 @@ uvx simple-module new my-app
54
54
  - `create_app(settings)` — returns a fully-wired `FastAPI` instance with all discovered modules registered.
55
55
  - Middleware pipeline (execution order): CorrelationId → RequestLogging → SecurityHeaders → Session → `<module middleware>` → Tenant (opt-in) → Locale → InertiaLayoutData → app.
56
56
  - Inertia wiring — shared props (`auth`, `menus`, `i18n`), `InertiaDep`, page-route lookup.
57
- - CLI entry points: both `sm` and `simple-module` are installed and alias the same Click tree.
58
- - Scaffolders — `sm create-host`, `sm create-module`, `sm new` (greenfield app with users + dashboard + permissions pre-wired), `sm gen-pages`.
57
+ - `sm host` plugin `sm host gen-pages` regenerates the frontend pages manifest; `sm host sync-js-deps` installs JS deps declared by installed modules. The `sm` binary itself comes from `simple_module_cli`.
59
58
 
60
59
  ## Usage
61
60
 
@@ -73,16 +72,13 @@ if __name__ == "__main__":
73
72
  uvicorn.run(app, host="0.0.0.0", port=8000)
74
73
  ```
75
74
 
76
- CLI:
75
+ CLI (after also installing `simple_module_cli`):
77
76
 
78
77
  ```bash
79
- simple-module new my-app # scaffold a new project
80
- simple-module doctor # diagnostic codes (SM001-SM017)
81
- simple-module gen-pages # regenerate client_app/modules.generated.ts
78
+ sm host gen-pages # regenerate client_app/modules.generated.ts
79
+ sm host sync-js-deps # sync module JS deps into client_app/node_modules
82
80
  ```
83
81
 
84
- `sm` works identically to `simple-module`.
85
-
86
82
  ## Depends on
87
83
 
88
84
  - `simple_module_core`, `simple_module_db`
@@ -1,6 +1,6 @@
1
1
  # simple_module_hosting
2
2
 
3
- FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, exposes the `sm` / `simple-module` CLI, and ships the project scaffolder.
3
+ FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/antosubash/simple_module_python) framework — builds the app, wires the middleware pipeline, and contributes the `sm host` plugin to the standalone `sm` CLI.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,10 +8,10 @@ FastAPI + Inertia.js host runtime for the [simple_module](https://github.com/ant
8
8
  pip install simple_module_hosting
9
9
  ```
10
10
 
11
- For a new project, most users run the generator instead:
11
+ For a new project, most users run the generator instead (shipped by the standalone `simple_module_cli` distribution):
12
12
 
13
13
  ```bash
14
- uvx simple-module new my-app
14
+ uvx --from simple_module_cli sm new my-app
15
15
  ```
16
16
 
17
17
  ## What it provides
@@ -19,8 +19,7 @@ uvx simple-module new my-app
19
19
  - `create_app(settings)` — returns a fully-wired `FastAPI` instance with all discovered modules registered.
20
20
  - Middleware pipeline (execution order): CorrelationId → RequestLogging → SecurityHeaders → Session → `<module middleware>` → Tenant (opt-in) → Locale → InertiaLayoutData → app.
21
21
  - Inertia wiring — shared props (`auth`, `menus`, `i18n`), `InertiaDep`, page-route lookup.
22
- - CLI entry points: both `sm` and `simple-module` are installed and alias the same Click tree.
23
- - Scaffolders — `sm create-host`, `sm create-module`, `sm new` (greenfield app with users + dashboard + permissions pre-wired), `sm gen-pages`.
22
+ - `sm host` plugin `sm host gen-pages` regenerates the frontend pages manifest; `sm host sync-js-deps` installs JS deps declared by installed modules. The `sm` binary itself comes from `simple_module_cli`.
24
23
 
25
24
  ## Usage
26
25
 
@@ -38,16 +37,13 @@ if __name__ == "__main__":
38
37
  uvicorn.run(app, host="0.0.0.0", port=8000)
39
38
  ```
40
39
 
41
- CLI:
40
+ CLI (after also installing `simple_module_cli`):
42
41
 
43
42
  ```bash
44
- simple-module new my-app # scaffold a new project
45
- simple-module doctor # diagnostic codes (SM001-SM017)
46
- simple-module gen-pages # regenerate client_app/modules.generated.ts
43
+ sm host gen-pages # regenerate client_app/modules.generated.ts
44
+ sm host sync-js-deps # sync module JS deps into client_app/node_modules
47
45
  ```
48
46
 
49
- `sm` works identically to `simple-module`.
50
-
51
47
  ## Depends on
52
48
 
53
49
  - `simple_module_core`, `simple_module_db`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_hosting"
3
- version = "0.0.1"
3
+ version = "0.0.3"
4
4
  description = "FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,16 +26,15 @@ dependencies = [
26
26
  "fastapi-inertia>=1.0",
27
27
  "httpx>=0.27",
28
28
  "jinja2>=3.1",
29
- "simple_module_core==0.0.1",
30
- "simple_module_db==0.0.1",
29
+ "simple_module_core==0.0.3",
30
+ "simple_module_db==0.0.3",
31
31
  "starlette>=0.44",
32
32
  "tomlkit>=0.13",
33
33
  "uvicorn[standard]>=0.34",
34
34
  ]
35
35
 
36
- [project.scripts]
37
- sm = "simple_module_hosting.cli:main"
38
- simple-module = "simple_module_hosting.cli:main"
36
+ [project.entry-points."simple_module_cli.cli_plugins"]
37
+ host = "simple_module_hosting.host_cli:app"
39
38
 
40
39
  [project.urls]
41
40
  Homepage = "https://github.com/antosubash/simple_module_python"
@@ -69,7 +69,7 @@ def wire_module_routes(app: FastAPI, module) -> None:
69
69
  """Attach a module's API + view routers to ``app`` using its Meta prefixes.
70
70
 
71
71
  The single canonical implementation so ``create_app`` and the test harness
72
- in ``simple_module_testing`` stay in lockstep if ``ModuleBase`` ever gains
72
+ in ``simple_module_test`` stay in lockstep if ``ModuleBase`` ever gains
73
73
  a new router type.
74
74
  """
75
75
  api_router = APIRouter(prefix=module.meta.route_prefix, tags=[module.meta.name])
@@ -131,7 +131,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
131
131
  # Emit frontend module-pages manifest so Vite can find pages shipped
132
132
  # inside pip-installed module wheels. See scaffolding.py.
133
133
  try:
134
- from simple_module_hosting.scaffolding import write_module_pages_manifest
134
+ from simple_module_hosting.manifest import write_module_pages_manifest
135
135
 
136
136
  client_app = _PROJECT_ROOT / "host" / "client_app"
137
137
  if client_app.is_dir():
@@ -0,0 +1,117 @@
1
+ """``sm host`` plugin — project-time helpers exposed through the simple-module CLI.
2
+
3
+ Commands here need module discovery (``simple_module_core.discover_modules``)
4
+ and the manifest helpers; they're not part of the standalone scaffolder.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import shutil
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Annotated
14
+
15
+ import typer
16
+ from simple_module_core import discover_modules
17
+
18
+ from simple_module_hosting.manifest import (
19
+ collect_module_js_deps,
20
+ repo_root_from_client_app,
21
+ write_module_pages_manifest,
22
+ )
23
+
24
+ app = typer.Typer(
25
+ help="Project-time helpers (frontend pages manifest, module JS dep sync).",
26
+ no_args_is_help=True,
27
+ )
28
+
29
+
30
+ @app.command("gen-pages")
31
+ def gen_pages(
32
+ host_dir: Annotated[
33
+ Path,
34
+ typer.Option(
35
+ "--host-dir",
36
+ help="Path to the host's client_app directory. Defaults to ./client_app.",
37
+ ),
38
+ ] = Path("client_app"),
39
+ ) -> None:
40
+ """Regenerate client_app/modules.{manifest.json,generated.ts,generated.css}."""
41
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
42
+ if not host_dir.is_dir():
43
+ typer.echo(f"ERROR: client_app directory not found at {host_dir}", err=True)
44
+ raise typer.Exit(code=1)
45
+ modules = discover_modules()
46
+ written = write_module_pages_manifest(modules, host_dir)
47
+ typer.echo(
48
+ f"Wrote {written['manifest'].name}, {written['generated'].name}, "
49
+ f"{written['css'].name} to {host_dir}"
50
+ )
51
+
52
+
53
+ @app.command("sync-js-deps")
54
+ def sync_js_deps(
55
+ host_client_app: Annotated[
56
+ Path,
57
+ typer.Option(
58
+ "--host-client-app",
59
+ help="Path to host/client_app. Defaults to ./client_app.",
60
+ ),
61
+ ] = Path("client_app"),
62
+ dry_run: Annotated[
63
+ bool, typer.Option("--dry-run", help="Print the npm install command only.")
64
+ ] = False,
65
+ ) -> None:
66
+ """Install JS deps declared by installed modules into host's node_modules."""
67
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
68
+ if not host_client_app.is_dir():
69
+ typer.echo(f"ERROR: client_app directory not found at {host_client_app}", err=True)
70
+ raise typer.Exit(code=1)
71
+
72
+ modules = discover_modules()
73
+ by_module = collect_module_js_deps(modules)
74
+ if not by_module:
75
+ typer.echo("No module JS dependencies declared.")
76
+ return
77
+
78
+ specs: list[str] = []
79
+ for mod_name in sorted(by_module):
80
+ for dep, rng in sorted(by_module[mod_name].items()):
81
+ specs.append(f"{dep}@{rng}")
82
+ deduped: list[str] = []
83
+ seen: set[str] = set()
84
+ for spec in specs:
85
+ if spec not in seen:
86
+ seen.add(spec)
87
+ deduped.append(spec)
88
+
89
+ npm = shutil.which("npm")
90
+ if npm is None:
91
+ typer.echo("ERROR: npm not found on PATH.", err=True)
92
+ raise typer.Exit(code=1)
93
+
94
+ repo_root = repo_root_from_client_app(host_client_app)
95
+ try:
96
+ workspace = str(host_client_app.resolve().relative_to(repo_root))
97
+ except ValueError:
98
+ workspace = str(host_client_app.resolve())
99
+
100
+ cmd = [
101
+ npm,
102
+ "install",
103
+ "--workspace",
104
+ workspace,
105
+ "--save=false",
106
+ "--no-audit",
107
+ "--no-fund",
108
+ *deduped,
109
+ ]
110
+ typer.echo("Installing module JS deps:")
111
+ for spec in deduped:
112
+ typer.echo(f" {spec}")
113
+ if dry_run:
114
+ typer.echo("(dry-run) " + " ".join(cmd))
115
+ return
116
+ result = subprocess.run(cmd, cwd=repo_root, check=False)
117
+ raise typer.Exit(code=result.returncode)
@@ -0,0 +1,28 @@
1
+ """Smoke tests for the simple_module_hosting host_cli Typer plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from simple_module_hosting.host_cli import app
9
+ from typer.testing import CliRunner
10
+
11
+
12
+ def test_app_is_typer_instance() -> None:
13
+ assert isinstance(app, typer.Typer)
14
+
15
+
16
+ def test_help_lists_gen_pages_and_sync_js_deps() -> None:
17
+ runner = CliRunner()
18
+ result = runner.invoke(app, ["--help"])
19
+ assert result.exit_code == 0
20
+ assert "gen-pages" in result.output
21
+ assert "sync-js-deps" in result.output
22
+
23
+
24
+ def test_gen_pages_errors_on_missing_client_app(tmp_path: Path) -> None:
25
+ runner = CliRunner()
26
+ result = runner.invoke(app, ["gen-pages", "--host-dir", str(tmp_path / "does-not-exist")])
27
+ assert result.exit_code != 0
28
+ assert "not found" in result.output.lower() or "not found" in (result.stderr or "").lower()
@@ -1,292 +0,0 @@
1
- """SimpleModule CLI — `sm` console script.
2
-
3
- Currently exposes:
4
-
5
- * ``sm create-host <name>`` — scaffold a new host directory.
6
- * ``sm create-module <name>`` — scaffold a new module package.
7
- * ``sm gen-pages`` — regenerate the frontend pages manifest + Tailwind CSS.
8
- * ``sm sync-js-deps`` — install JS deps declared by installed modules.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import logging
14
- import shutil
15
- import subprocess
16
- import sys
17
- from pathlib import Path
18
-
19
- import click
20
- from simple_module_core import discover_modules
21
-
22
- from simple_module_hosting.scaffolding import (
23
- _to_kebab_case,
24
- collect_module_js_deps,
25
- create_module,
26
- repo_root_from_client_app,
27
- write_module_pages_manifest,
28
- )
29
- from simple_module_hosting.scaffolding import (
30
- create_host as _create_host,
31
- )
32
-
33
-
34
- @click.group()
35
- def main() -> None:
36
- """SimpleModule developer CLI."""
37
-
38
-
39
- @main.command("new")
40
- @click.argument("name")
41
- @click.option(
42
- "--dest",
43
- type=click.Path(file_okay=False, path_type=Path),
44
- default=None,
45
- help="Destination directory. Defaults to ./<name>.",
46
- )
47
- @click.option(
48
- "--db",
49
- type=click.Choice(["sqlite", "postgres"]),
50
- default="sqlite",
51
- show_default=True,
52
- help="Database backend to configure in .env.example.",
53
- )
54
- @click.option(
55
- "--tenancy/--no-tenancy",
56
- default=False,
57
- show_default=True,
58
- help="Enable the multi-tenant middleware by default.",
59
- )
60
- @click.option(
61
- "--yes",
62
- "-y",
63
- is_flag=True,
64
- default=False,
65
- help="Skip interactive prompts; accept all defaults.",
66
- )
67
- @click.option(
68
- "--no-install",
69
- is_flag=True,
70
- default=False,
71
- help="Skip 'uv sync' / 'npm install' / 'alembic upgrade head' after scaffolding.",
72
- )
73
- def new_project(
74
- name: str,
75
- dest: Path | None,
76
- db: str,
77
- tenancy: bool,
78
- yes: bool,
79
- no_install: bool,
80
- ) -> None:
81
- """Scaffold a new SimpleModule app — pre-wired with users, dashboard, permissions."""
82
- target = dest or Path.cwd() / name
83
- if not yes:
84
- db = click.prompt(
85
- "Database backend",
86
- default=db,
87
- type=click.Choice(["sqlite", "postgres"]),
88
- )
89
- tenancy = click.confirm("Enable multi-tenancy?", default=tenancy)
90
-
91
- from simple_module_hosting.scaffolding import create_app_project
92
-
93
- try:
94
- create_app_project(target, name=name, db=db, tenancy=tenancy)
95
- except FileExistsError as exc:
96
- click.echo(f"ERROR: {exc}", err=True)
97
- sys.exit(1)
98
-
99
- click.echo(f"Created app '{name}' at {target}")
100
- click.echo("\nPre-wired modules: users, dashboard, permissions")
101
- click.echo("\nNext steps:")
102
- click.echo(f" cd {target}")
103
- if no_install:
104
- click.echo(" uv sync")
105
- click.echo(" npm install")
106
- click.echo(" alembic upgrade head")
107
- click.echo(" make dev")
108
- return
109
-
110
- click.echo("Installing dependencies...")
111
- for cmd in (["uv", "sync"], ["npm", "install"]):
112
- result = subprocess.run(cmd, cwd=target, check=False)
113
- if result.returncode != 0:
114
- click.echo(
115
- f"WARNING: {' '.join(cmd)} failed (exit {result.returncode}); "
116
- f"finish setup manually.",
117
- err=True,
118
- )
119
- return
120
-
121
- subprocess.run(["uv", "run", "alembic", "upgrade", "head"], cwd=target, check=False)
122
- click.echo("\nSetup complete. Run `make dev` in the new directory.")
123
-
124
-
125
- @main.command("create-host")
126
- @click.argument("name")
127
- @click.option(
128
- "--dest",
129
- type=click.Path(file_okay=False, path_type=Path),
130
- default=None,
131
- help="Destination directory. Defaults to ./<name>.",
132
- )
133
- @click.option(
134
- "--with",
135
- "modules",
136
- default="",
137
- help="Comma-separated module names to declare as deps (e.g. --with=Auth,Products).",
138
- )
139
- def create_host(name: str, dest: Path | None, modules: str) -> None:
140
- """Scaffold a new SimpleModule host project at ./<NAME>."""
141
- target = dest or Path.cwd() / name
142
- selected = [m.strip() for m in modules.split(",") if m.strip()]
143
-
144
- try:
145
- _create_host(target, name=name, modules=selected)
146
- except FileExistsError as exc:
147
- click.echo(f"ERROR: {exc}", err=True)
148
- sys.exit(1)
149
-
150
- click.echo(f"Created host '{name}' at {target}")
151
- if selected:
152
- click.echo(f"Declared modules: {', '.join(selected)}")
153
- click.echo("\nNext steps:")
154
- click.echo(f" cd {target}")
155
- click.echo(" uv sync")
156
- click.echo(" cp .env.example .env")
157
- click.echo(' alembic revision --autogenerate -m "initial schema"')
158
- click.echo(" alembic upgrade head")
159
- click.echo(" python main.py")
160
-
161
-
162
- @main.command("create-module")
163
- @click.argument("name")
164
- @click.option(
165
- "--dest",
166
- type=click.Path(file_okay=False, path_type=Path),
167
- default=None,
168
- help="Destination directory. Defaults to ./simple_module_<package>.",
169
- )
170
- def create_module_cmd(name: str, dest: Path | None) -> None:
171
- """Scaffold a publishable SimpleModule module package."""
172
- slug = _to_kebab_case(name)
173
- package = slug.replace("-", "_")
174
- target = dest or Path.cwd() / f"simple_module_{package}"
175
-
176
- try:
177
- create_module(target, name=name)
178
- except FileExistsError as exc:
179
- click.echo(f"ERROR: {exc}", err=True)
180
- sys.exit(1)
181
-
182
- click.echo(f"Created module 'simple_module_{package}' at {target}")
183
- click.echo("\nNext steps:")
184
- click.echo(f" cd {target}")
185
- click.echo(" uv sync --extra dev")
186
- click.echo(" uv run pytest")
187
-
188
-
189
- @main.command("gen-pages")
190
- @click.option(
191
- "--host-dir",
192
- type=click.Path(file_okay=False, exists=True, path_type=Path),
193
- default=None,
194
- help="Path to the host's client_app directory. Defaults to ./client_app.",
195
- )
196
- def gen_pages(host_dir: Path | None) -> None:
197
- """Regenerate client_app/modules.{manifest.json,generated.ts,generated.css}."""
198
- logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
199
- output = host_dir or Path.cwd() / "client_app"
200
- if not output.is_dir():
201
- click.echo(f"ERROR: client_app directory not found at {output}", err=True)
202
- sys.exit(1)
203
-
204
- modules = discover_modules()
205
- written = write_module_pages_manifest(modules, output)
206
- click.echo(
207
- f"Wrote {written['manifest'].name}, {written['generated'].name}, "
208
- f"{written['css'].name} to {output}"
209
- )
210
-
211
-
212
- @main.command("sync-js-deps")
213
- @click.option(
214
- "--host-client-app",
215
- type=click.Path(file_okay=False, exists=True, path_type=Path),
216
- default=None,
217
- help="Path to host/client_app. Defaults to ./client_app.",
218
- )
219
- @click.option(
220
- "--dry-run",
221
- is_flag=True,
222
- default=False,
223
- help="Print the npm install command without running it.",
224
- )
225
- def sync_js_deps(host_client_app: Path | None, dry_run: bool) -> None:
226
- """Install JS deps declared by installed modules into host's node_modules.
227
-
228
- Walks every discovered module, reads its package.json, and runs a single
229
- ``npm install --workspace host/client_app --save=false <specs>``. Use
230
- this after ``pip install``-ing a module wheel that declares JS deps;
231
- in-repo modules already flow through npm workspaces and need nothing.
232
- """
233
- logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
234
-
235
- output = host_client_app or Path.cwd() / "client_app"
236
- if not output.is_dir():
237
- click.echo(f"ERROR: client_app directory not found at {output}", err=True)
238
- sys.exit(1)
239
-
240
- modules = discover_modules()
241
- by_module = collect_module_js_deps(modules)
242
- if not by_module:
243
- click.echo("No module JS dependencies declared.")
244
- return
245
-
246
- # Flatten into a single spec list. npm's own resolver handles conflicts.
247
- specs: list[str] = []
248
- for mod_name in sorted(by_module):
249
- for dep, rng in sorted(by_module[mod_name].items()):
250
- specs.append(f"{dep}@{rng}")
251
- # Dedupe while preserving first-seen order.
252
- deduped: list[str] = []
253
- seen: set[str] = set()
254
- for spec in specs:
255
- if spec not in seen:
256
- seen.add(spec)
257
- deduped.append(spec)
258
-
259
- npm = shutil.which("npm")
260
- if npm is None:
261
- click.echo("ERROR: npm not found on PATH.", err=True)
262
- sys.exit(1)
263
-
264
- # Workspace path is relative to the repo root — derive it from output.
265
- repo_root = repo_root_from_client_app(output)
266
- try:
267
- workspace = str(output.resolve().relative_to(repo_root))
268
- except ValueError:
269
- workspace = str(output.resolve())
270
-
271
- cmd = [
272
- npm,
273
- "install",
274
- "--workspace",
275
- workspace,
276
- "--save=false",
277
- "--no-audit",
278
- "--no-fund",
279
- *deduped,
280
- ]
281
- click.echo("Installing module JS deps:")
282
- for spec in deduped:
283
- click.echo(f" {spec}")
284
- if dry_run:
285
- click.echo("(dry-run) " + " ".join(cmd))
286
- return
287
- result = subprocess.run(cmd, cwd=repo_root, check=False)
288
- sys.exit(result.returncode)
289
-
290
-
291
- if __name__ == "__main__":
292
- main()