simple-module-cli 0.0.11__tar.gz → 0.0.13__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.
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/PKG-INFO +11 -11
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/README.md +9 -9
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/pyproject.toml +3 -3
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/app_project.py +39 -21
- simple_module_cli-0.0.13/simple_module_cli/case.py +79 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/cli.py +7 -7
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/new.py +42 -5
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/package_update.py +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/plugins.py +3 -3
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/recipes.py +2 -2
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/scaffolding.py +77 -21
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/README.md +12 -12
- simple_module_cli-0.0.13/simple_module_cli/skills/simple-module-cli/SKILL.md +170 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-creating/SKILL.md +2 -2
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +2 -2
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-migrations/SKILL.md +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-testing/SKILL.md +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills_cmd.py +4 -4
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/package.json.tpl +1 -1
- simple_module_cli-0.0.13/simple_module_cli/templates/host/client_app/pages/Landing.tsx +78 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/pages.ts +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/vite.config.ts +85 -24
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/main.py +4 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/migrations/env.py +11 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/pyproject.toml.tpl +1 -1
- simple_module_cli-0.0.13/simple_module_cli/templates/host/routes.py +28 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/workspace/README.md.tpl +2 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/workspace/package.json.tpl +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/workspace/pyproject.toml.tpl +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/wizard.py +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_build_packaging.py +2 -2
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_cli_new.py +2 -1
- simple_module_cli-0.0.13/tests/test_cli_new_dest_tolerance.py +96 -0
- simple_module_cli-0.0.13/tests/test_cli_new_regressions.py +272 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_cli_package_update.py +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_cli_wizard.py +1 -1
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_scaffolding_host.py +4 -2
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_scaffolding_module.py +2 -2
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_skills_cmd.py +1 -1
- simple_module_cli-0.0.11/simple_module_cli/case.py +0 -38
- simple_module_cli-0.0.11/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -170
- simple_module_cli-0.0.11/tests/test_cli_new_regressions.py +0 -61
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/.gitignore +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/LICENSE +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.11 → simple_module_cli-0.0.13}/tests/test_plugin_discovery.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: Standalone scaffolder for the SimpleModule framework — `
|
|
3
|
+
Version: 0.0.13
|
|
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
|
|
7
7
|
Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
|
|
@@ -34,7 +34,7 @@ pip install simple_module_cli
|
|
|
34
34
|
# or, to keep the CLI in its own venv:
|
|
35
35
|
pipx install simple_module_cli
|
|
36
36
|
# or, to run it without installing:
|
|
37
|
-
uvx --from simple_module_cli
|
|
37
|
+
uvx --from simple_module_cli smpy new my-app
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
The package depends only on `typer` and `tomlkit` — installing it does **not** pull in FastAPI, SQLModel, or any other framework runtime.
|
|
@@ -42,21 +42,21 @@ The package depends only on `typer` and `tomlkit` — installing it does **not**
|
|
|
42
42
|
## Usage
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
smpy new my-app # interactive wizard
|
|
46
|
+
smpy new my-app --yes --preset full # all built-in modules + background jobs
|
|
47
|
+
smpy create-module my_feature # scaffold a publishable module package
|
|
48
|
+
smpy create-host bare-host # scaffold a bare host (no opinionated wiring)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Built-in commands: `
|
|
51
|
+
Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`.
|
|
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
|
|
|
55
55
|
| Package | Commands |
|
|
56
56
|
|---|---|
|
|
57
|
-
| `simple_module_hosting` | `
|
|
58
|
-
| `simple_module_users` | `
|
|
59
|
-
| `simple_module_settings` | `
|
|
57
|
+
| `simple_module_hosting` | `smpy host gen-pages`, `smpy host sync-js-deps` |
|
|
58
|
+
| `simple_module_users` | `smpy users create-admin` |
|
|
59
|
+
| `simple_module_settings` | `smpy settings import-from-env` |
|
|
60
60
|
|
|
61
61
|
## License
|
|
62
62
|
|
|
@@ -9,7 +9,7 @@ pip install simple_module_cli
|
|
|
9
9
|
# or, to keep the CLI in its own venv:
|
|
10
10
|
pipx install simple_module_cli
|
|
11
11
|
# or, to run it without installing:
|
|
12
|
-
uvx --from simple_module_cli
|
|
12
|
+
uvx --from simple_module_cli smpy new my-app
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The package depends only on `typer` and `tomlkit` — installing it does **not** pull in FastAPI, SQLModel, or any other framework runtime.
|
|
@@ -17,21 +17,21 @@ The package depends only on `typer` and `tomlkit` — installing it does **not**
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
smpy new my-app # interactive wizard
|
|
21
|
+
smpy new my-app --yes --preset full # all built-in modules + background jobs
|
|
22
|
+
smpy create-module my_feature # scaffold a publishable module package
|
|
23
|
+
smpy create-host bare-host # scaffold a bare host (no opinionated wiring)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Built-in commands: `
|
|
26
|
+
Built-in commands: `smpy new`, `smpy create-host`, `smpy create-module`.
|
|
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
|
|
|
30
30
|
| Package | Commands |
|
|
31
31
|
|---|---|
|
|
32
|
-
| `simple_module_hosting` | `
|
|
33
|
-
| `simple_module_users` | `
|
|
34
|
-
| `simple_module_settings` | `
|
|
32
|
+
| `simple_module_hosting` | `smpy host gen-pages`, `smpy host sync-js-deps` |
|
|
33
|
+
| `simple_module_users` | `smpy users create-admin` |
|
|
34
|
+
| `simple_module_settings` | `smpy settings import-from-env` |
|
|
35
35
|
|
|
36
36
|
## License
|
|
37
37
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_cli"
|
|
3
|
-
version = "0.0.
|
|
4
|
-
description = "Standalone scaffolder for the SimpleModule framework — `
|
|
3
|
+
version = "0.0.13"
|
|
4
|
+
description = "Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.scripts]
|
|
28
|
-
|
|
28
|
+
smpy = "simple_module_cli.cli:main"
|
|
29
29
|
simple-module = "simple_module_cli.cli:main"
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
@@ -15,6 +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
|
|
18
19
|
from collections.abc import Sequence
|
|
19
20
|
from importlib.metadata import PackageNotFoundError
|
|
20
21
|
from importlib.metadata import version as _pkg_version
|
|
@@ -26,6 +27,7 @@ from simple_module_cli.case import to_kebab_case, to_pascal_case
|
|
|
26
27
|
from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps
|
|
27
28
|
from simple_module_cli.recipes import RECIPES, ScaffoldCtx
|
|
28
29
|
from simple_module_cli.scaffolding import (
|
|
30
|
+
SAFE_PRESERVED_NAMES,
|
|
29
31
|
_module_to_pypi_name,
|
|
30
32
|
create_host,
|
|
31
33
|
create_module,
|
|
@@ -55,7 +57,14 @@ def _resolve_framework_version() -> str:
|
|
|
55
57
|
|
|
56
58
|
_FRAMEWORK_VERSION = _resolve_framework_version()
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
# Pin ``simple_module_cli`` as a dev dep so ``uv run smpy`` resolves to the
|
|
61
|
+
# 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).
|
|
63
|
+
_APP_PY_DEV_DEPS = [
|
|
64
|
+
f"simple_module_test=={_FRAMEWORK_VERSION}",
|
|
65
|
+
f"simple_module_cli=={_FRAMEWORK_VERSION}",
|
|
66
|
+
"pytest>=8.0",
|
|
67
|
+
]
|
|
59
68
|
|
|
60
69
|
_APP_NPM_DEPS = {
|
|
61
70
|
"@simple-module-py/ui": _FRAMEWORK_VERSION,
|
|
@@ -89,35 +98,39 @@ def create_app_project(
|
|
|
89
98
|
tenancy: bool = False,
|
|
90
99
|
selected: Sequence[str] | None = None,
|
|
91
100
|
flat: bool = False,
|
|
92
|
-
) ->
|
|
101
|
+
) -> tuple[Path, list[Path]]:
|
|
93
102
|
"""Greenfield ``simple-module new`` scaffold.
|
|
94
103
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
``
|
|
104
|
+
Workspace mode (default) lays down a uv + npm workspace at ``target/``
|
|
105
|
+
with the host under ``target/host/`` and a sample module under
|
|
106
|
+
``target/modules/hello/``. Flat mode keeps the legacy single-host layout.
|
|
107
|
+
Tolerates ``SAFE_PRESERVED_NAMES`` at ``target`` (leftovers from
|
|
108
|
+
``git init`` / ``gh repo create`` / IDE setup); other pre-existing
|
|
109
|
+
entries raise ``FileExistsError``.
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
``
|
|
103
|
-
|
|
104
|
-
(e.g. the ``background_tasks`` recipe drops a Celery worker stack).
|
|
111
|
+
Returns ``(host_dir, preserved)`` — the host directory (``target`` in
|
|
112
|
+
flat mode, ``target/host`` in workspace mode) and the paths whose
|
|
113
|
+
scaffold copy was skipped because the user already had one.
|
|
105
114
|
"""
|
|
106
|
-
if target.exists() and any(target.iterdir()):
|
|
107
|
-
raise FileExistsError(
|
|
108
|
-
f"Destination {target} already exists and is non-empty; "
|
|
109
|
-
"choose a new path or remove its contents first."
|
|
110
|
-
)
|
|
111
|
-
|
|
112
115
|
chosen = list(selected) if selected is not None else list(PRESETS["standard"])
|
|
113
116
|
resolved, _added = expand_deps(chosen)
|
|
114
117
|
|
|
115
118
|
display_names = [to_pascal_case(CATALOG[m].display) for m in resolved]
|
|
116
119
|
host_dir = target if flat else target / "host"
|
|
120
|
+
preserved: list[Path] = []
|
|
117
121
|
if not flat:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
preserved.extend(
|
|
123
|
+
create_workspace(target, name=name, preserve_existing=SAFE_PRESERVED_NAMES)
|
|
124
|
+
)
|
|
125
|
+
preserved.extend(
|
|
126
|
+
create_host(
|
|
127
|
+
host_dir,
|
|
128
|
+
name=name,
|
|
129
|
+
modules=display_names,
|
|
130
|
+
framework_version=_FRAMEWORK_VERSION,
|
|
131
|
+
preserve_existing=SAFE_PRESERVED_NAMES if flat else frozenset(),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
121
134
|
if not flat:
|
|
122
135
|
_strip_workspace_owned_files(host_dir)
|
|
123
136
|
|
|
@@ -169,6 +182,8 @@ def create_app_project(
|
|
|
169
182
|
if recipe_key is not None and recipe_key in RECIPES:
|
|
170
183
|
RECIPES[recipe_key].apply(target, ctx)
|
|
171
184
|
|
|
185
|
+
return host_dir, preserved
|
|
186
|
+
|
|
172
187
|
|
|
173
188
|
def _strip_workspace_owned_files(host_dir: Path) -> None:
|
|
174
189
|
"""Drop host copies of files the workspace root owns in workspace mode."""
|
|
@@ -181,6 +196,9 @@ def _scaffold_sample_module(target: Path) -> None:
|
|
|
181
196
|
if sample_dest.exists():
|
|
182
197
|
return
|
|
183
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")
|
|
184
202
|
_pin_sample_module_deps(sample_dest)
|
|
185
203
|
_seed_static_dist_placeholder(sample_dest / _SAMPLE_MODULE_NAME / "static" / "dist")
|
|
186
204
|
|
|
@@ -195,7 +213,7 @@ def _seed_static_dist_placeholder(static_dist: Path) -> None:
|
|
|
195
213
|
def _pin_sample_module_deps(sample_dest: Path) -> None:
|
|
196
214
|
"""Replace the module template's future-API range pins with exact pins.
|
|
197
215
|
|
|
198
|
-
The shared ``
|
|
216
|
+
The shared ``smpy create-module`` template ships ``>=1.0,<2.0`` against the
|
|
199
217
|
framework's eventual stable line, but the workspace-bundled sample has to
|
|
200
218
|
resolve against whatever the framework version actually is today (``==X``
|
|
201
219
|
in pre-1.0). Without rewriting, ``uv sync`` can't satisfy the workspace.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Identifier case-conversion helpers used by every scaffolder.
|
|
2
|
+
|
|
3
|
+
Module/host names are accepted in any case style and normalized to the
|
|
4
|
+
three forms the templates need: snake_case (Python package + entry-point
|
|
5
|
+
key), kebab-case (PyPI slug), and PascalCase (display name in Meta).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"InvalidScaffoldNameError",
|
|
14
|
+
"to_kebab_case",
|
|
15
|
+
"to_pascal_case",
|
|
16
|
+
"to_snake_case",
|
|
17
|
+
"validate_scaffold_name",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InvalidScaffoldNameError(ValueError):
|
|
22
|
+
"""Raised when a user-supplied scaffold name can't be canonicalized.
|
|
23
|
+
|
|
24
|
+
Names that mix case, lead with a digit, contain spaces or non
|
|
25
|
+
``[a-z0-9_-]`` characters, or mix ``_`` and ``-`` separators within
|
|
26
|
+
the same identifier are ambiguous: the scaffolder would have to guess
|
|
27
|
+
a canonical form, and the directory name would diverge from the
|
|
28
|
+
READMEs that reference it. Reject up front instead.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_snake_case(name: str) -> str:
|
|
33
|
+
"""'MyFeature' / 'my-feature' / 'My Feature' / 'URLPath' -> 'my_feature' / 'url_path'.
|
|
34
|
+
|
|
35
|
+
Handles acronyms by treating ``Acronym|Word`` and ``word|Capital`` as
|
|
36
|
+
boundaries: ``URLPath`` -> ``url_path``, ``APIClient`` -> ``api_client``,
|
|
37
|
+
``HTTPServer2`` -> ``http_server2``. The single-pass ``(?=[A-Z])`` form
|
|
38
|
+
that preceded this would emit ``u_r_l_path`` and propagate the typo
|
|
39
|
+
into the PyPI slug + display name.
|
|
40
|
+
"""
|
|
41
|
+
s = re.sub(r"[\s\-]+", "_", name)
|
|
42
|
+
s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", s)
|
|
43
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
|
|
44
|
+
return s.lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_kebab_case(name: str) -> str:
|
|
48
|
+
"""'MyFeature' / 'my_feature' -> 'my-feature' (used as the PyPI slug)."""
|
|
49
|
+
return to_snake_case(name).replace("_", "-")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def to_pascal_case(name: str) -> str:
|
|
53
|
+
"""'my-feature' / 'my_feature' -> 'MyFeature' (the display name in Meta)."""
|
|
54
|
+
snake = to_snake_case(name)
|
|
55
|
+
return "".join(part.capitalize() for part in snake.split("_") if part)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_VALID_NAME_RE = re.compile(r"^[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*$")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_scaffold_name(name: str) -> str:
|
|
62
|
+
"""Reject ambiguous host/module names; return the canonical display form.
|
|
63
|
+
|
|
64
|
+
A valid name is lowercase alphanumerics with at most one separator
|
|
65
|
+
style — either ``_`` or ``-``, not both. Examples:
|
|
66
|
+
|
|
67
|
+
* ``simple_module_chat`` -> ``simple_module_chat``
|
|
68
|
+
* ``simple-module-chat`` -> ``simple-module-chat``
|
|
69
|
+
* ``MyApp`` -> rejected (mixed case is ambiguous: ``my-app`` or ``my_app``?)
|
|
70
|
+
* ``1chat`` -> rejected (must start with a letter)
|
|
71
|
+
* ``foo_bar-baz`` -> rejected (mixed separators leave no canonical form)
|
|
72
|
+
"""
|
|
73
|
+
if not name or not _VALID_NAME_RE.match(name) or ("_" in name and "-" in name):
|
|
74
|
+
raise InvalidScaffoldNameError(
|
|
75
|
+
f"{name!r} is not a valid scaffold name. Use lowercase letters and "
|
|
76
|
+
"digits with at most one separator style (all '_' or all '-'), "
|
|
77
|
+
"starting with a letter. e.g. 'my_app', 'my-app', or 'myapp'."
|
|
78
|
+
)
|
|
79
|
+
return name
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
"""Root `
|
|
1
|
+
"""Root `smpy` Typer app — scaffolders + plugin mount.
|
|
2
2
|
|
|
3
3
|
Built-in commands:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
smpy new
|
|
5
|
+
smpy create-host
|
|
6
|
+
smpy create-module
|
|
7
|
+
smpy skills add / list / update
|
|
8
8
|
|
|
9
9
|
Plugins discovered via the ``simple_module_cli.cli_plugins`` entry-point
|
|
10
|
-
group are mounted as named subgroups (e.g. ``
|
|
10
|
+
group are mounted as named subgroups (e.g. ``smpy host gen-pages``).
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
@@ -101,7 +101,7 @@ discover_and_mount(app)
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
def main() -> None:
|
|
104
|
-
"""Entry point for the `
|
|
104
|
+
"""Entry point for the `smpy` console script."""
|
|
105
105
|
app()
|
|
106
106
|
|
|
107
107
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""``
|
|
1
|
+
"""``smpy new`` Typer command — flag-driven or interactive scaffolder."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -11,11 +11,14 @@ from typing import Annotated
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
13
|
from simple_module_cli.app_project import create_app_project
|
|
14
|
+
from simple_module_cli.case import InvalidScaffoldNameError, to_kebab_case, validate_scaffold_name
|
|
14
15
|
from simple_module_cli.catalog import PRESETS, expand_deps
|
|
15
16
|
from simple_module_cli.wizard import run_wizard
|
|
16
17
|
|
|
17
18
|
__all__ = ["new_project"]
|
|
18
19
|
|
|
20
|
+
_ALEMBIC = ("uv", "run", "alembic")
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
class Db(StrEnum):
|
|
21
24
|
sqlite = "sqlite"
|
|
@@ -61,7 +64,7 @@ def new_project(
|
|
|
61
64
|
bool,
|
|
62
65
|
typer.Option(
|
|
63
66
|
"--no-install",
|
|
64
|
-
help="Skip 'uv sync' / 'npm install' /
|
|
67
|
+
help=("Skip 'uv sync' / 'npm install' / initial alembic migration after scaffolding."),
|
|
65
68
|
),
|
|
66
69
|
] = False,
|
|
67
70
|
flat: Annotated[
|
|
@@ -76,6 +79,14 @@ def new_project(
|
|
|
76
79
|
] = False,
|
|
77
80
|
) -> None:
|
|
78
81
|
"""Scaffold a new SimpleModule app, optionally with background jobs."""
|
|
82
|
+
try:
|
|
83
|
+
validate_scaffold_name(name)
|
|
84
|
+
except InvalidScaffoldNameError as exc:
|
|
85
|
+
typer.echo(f"ERROR: {exc}", err=True)
|
|
86
|
+
raise typer.Exit(code=1) from exc
|
|
87
|
+
pypi_name = to_kebab_case(name)
|
|
88
|
+
if pypi_name != name:
|
|
89
|
+
typer.echo(f"Normalizing PyPI name to {pypi_name!r}.")
|
|
79
90
|
target = dest or Path.cwd() / name
|
|
80
91
|
extra_list = [m.strip() for m in extra.split(",") if m.strip()]
|
|
81
92
|
flag_driven = preset is not None or bool(extra_list)
|
|
@@ -100,7 +111,7 @@ def new_project(
|
|
|
100
111
|
raise typer.Exit(code=1) from None
|
|
101
112
|
|
|
102
113
|
try:
|
|
103
|
-
create_app_project(
|
|
114
|
+
host_dir, preserved = create_app_project(
|
|
104
115
|
target,
|
|
105
116
|
name=name,
|
|
106
117
|
db=db_final,
|
|
@@ -114,12 +125,21 @@ def new_project(
|
|
|
114
125
|
|
|
115
126
|
typer.echo(f"Created app '{name}' at {target}")
|
|
116
127
|
typer.echo(f"Modules: {', '.join(resolved)}")
|
|
128
|
+
if preserved:
|
|
129
|
+
typer.echo(
|
|
130
|
+
"\nPreserved existing files (scaffold's versions were skipped — "
|
|
131
|
+
"merge by hand if you want their contents):"
|
|
132
|
+
)
|
|
133
|
+
for path in preserved:
|
|
134
|
+
rel = path.relative_to(target) if path.is_relative_to(target) else path
|
|
135
|
+
typer.echo(f" {rel}")
|
|
117
136
|
typer.echo("\nNext steps:")
|
|
118
137
|
typer.echo(f" cd {target}")
|
|
119
138
|
if no_install:
|
|
120
139
|
typer.echo(" uv sync")
|
|
121
140
|
typer.echo(" npm install")
|
|
122
|
-
typer.echo(
|
|
141
|
+
typer.echo(' make migration msg="initial schema"')
|
|
142
|
+
typer.echo(" make migrate")
|
|
123
143
|
typer.echo(" make dev")
|
|
124
144
|
if "background_tasks" in resolved:
|
|
125
145
|
typer.echo(" docker compose up -d redis worker beat # background jobs")
|
|
@@ -143,7 +163,24 @@ def new_project(
|
|
|
143
163
|
)
|
|
144
164
|
return
|
|
145
165
|
|
|
146
|
-
|
|
166
|
+
_bootstrap_initial_migration(host_dir)
|
|
167
|
+
subprocess.run([*_ALEMBIC, "upgrade", "head"], cwd=host_dir, check=False)
|
|
147
168
|
typer.echo("\nSetup complete. Run `make dev` in the new directory.")
|
|
148
169
|
if "background_tasks" in resolved:
|
|
149
170
|
typer.echo("For background jobs, also run: docker compose up -d redis worker beat")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _bootstrap_initial_migration(host_dir: Path) -> None:
|
|
174
|
+
"""Autogenerate the baseline migration if the scaffold ships none.
|
|
175
|
+
|
|
176
|
+
Without a real revision, ``alembic upgrade head`` is a silent no-op
|
|
177
|
+
against an empty schema — the bundled modules' tables never exist.
|
|
178
|
+
"""
|
|
179
|
+
versions_dir = host_dir / "migrations" / "versions"
|
|
180
|
+
if any(p.name != "__init__.py" for p in versions_dir.glob("*.py")):
|
|
181
|
+
return
|
|
182
|
+
subprocess.run(
|
|
183
|
+
[*_ALEMBIC, "revision", "--autogenerate", "-m", "initial schema"],
|
|
184
|
+
cwd=host_dir,
|
|
185
|
+
check=False,
|
|
186
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""``
|
|
1
|
+
"""``smpy package-update`` — bump simple_module_* deps to latest PyPI versions.
|
|
2
2
|
|
|
3
3
|
Walks the project's ``pyproject.toml`` (and any ``[tool.uv.workspace]`` members),
|
|
4
4
|
finds every dependency whose distribution name starts with ``simple_module_`` /
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Plugin discovery for ``
|
|
1
|
+
"""Plugin discovery for ``smpy`` via the ``simple_module_cli.cli_plugins`` group.
|
|
2
2
|
|
|
3
3
|
Each entry-point's value (``module:attr``) must resolve to a
|
|
4
4
|
:class:`typer.Typer` instance. The entry-point name becomes the
|
|
5
|
-
subcommand namespace under ``
|
|
5
|
+
subcommand namespace under ``smpy`` (e.g. ``smpy host gen-pages``).
|
|
6
6
|
|
|
7
7
|
Failed loads (broken import, wrong type) print one line to stderr and
|
|
8
|
-
are skipped — ``
|
|
8
|
+
are skipped — ``smpy`` keeps working with whatever else loads.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Per-module post-scaffold recipes.
|
|
2
2
|
|
|
3
|
-
A recipe is invoked by ``
|
|
3
|
+
A recipe is invoked by ``smpy new`` after the base host scaffold lands. It
|
|
4
4
|
performs module-specific actions (write helper scripts, append Make
|
|
5
5
|
targets, drop a docker-compose stack). The framework layer is kept free
|
|
6
6
|
of devex concerns — recipes know about Makefiles and compose, framework
|
|
@@ -68,7 +68,7 @@ class BackgroundTasksRecipe:
|
|
|
68
68
|
if path.exists():
|
|
69
69
|
raise FileExistsError(
|
|
70
70
|
f"{path} already exists — refusing to clobber. "
|
|
71
|
-
"Remove the file or run `
|
|
71
|
+
"Remove the file or run `smpy new` against an empty directory."
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
run_worker_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -21,15 +21,35 @@ import shutil
|
|
|
21
21
|
from collections.abc import Mapping, Sequence
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
|
-
from simple_module_cli.case import
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
from simple_module_cli.case import (
|
|
25
|
+
to_kebab_case,
|
|
26
|
+
to_pascal_case,
|
|
27
|
+
to_snake_case,
|
|
28
|
+
validate_scaffold_name,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"SAFE_PRESERVED_NAMES",
|
|
33
|
+
"create_host",
|
|
34
|
+
"create_module",
|
|
35
|
+
"create_workspace",
|
|
36
|
+
]
|
|
27
37
|
|
|
28
38
|
logger = logging.getLogger(__name__)
|
|
29
39
|
|
|
30
40
|
_TEMPLATES_PACKAGE = "simple_module_cli.templates"
|
|
31
41
|
_PACKAGE_PATH_TOKEN = "__PACKAGE__"
|
|
32
42
|
|
|
43
|
+
# Pre-existing entries we tolerate at a scaffold target — typical leftovers
|
|
44
|
+
# from ``git init`` / ``gh repo create`` / IDE setup.
|
|
45
|
+
SAFE_PRESERVED_NAMES = frozenset(
|
|
46
|
+
{".git", ".gitignore", ".gitattributes", ".editorconfig", ".DS_Store"}
|
|
47
|
+
| {".claude", ".vscode", ".idea"}
|
|
48
|
+
| {"README", "README.md", "README.rst"}
|
|
49
|
+
| {"LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"}
|
|
50
|
+
| {"CHANGELOG.md", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md"}
|
|
51
|
+
)
|
|
52
|
+
|
|
33
53
|
|
|
34
54
|
def _module_to_pypi_name(name: str) -> str:
|
|
35
55
|
return f"simple_module_{name.lower()}"
|
|
@@ -45,12 +65,21 @@ def _iter_template_files(template_root: Path):
|
|
|
45
65
|
yield path
|
|
46
66
|
|
|
47
67
|
|
|
48
|
-
def _require_empty_dest(dest: Path) -> None:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
def _require_empty_dest(dest: Path, *, preserve_existing: frozenset[str] = frozenset()) -> None:
|
|
69
|
+
"""Refuse a non-empty destination unless every top-level entry is allowed.
|
|
70
|
+
|
|
71
|
+
``preserve_existing`` is matched against the *name* of each top-level entry,
|
|
72
|
+
so callers can permit common pre-existing files (``.git``, ``README.md``,
|
|
73
|
+
...) without silently overwriting unrelated user content.
|
|
74
|
+
"""
|
|
75
|
+
if dest.exists():
|
|
76
|
+
unexpected = sorted(p.name for p in dest.iterdir() if p.name not in preserve_existing)
|
|
77
|
+
if unexpected:
|
|
78
|
+
raise FileExistsError(
|
|
79
|
+
f"Destination {dest} exists and contains files that would collide "
|
|
80
|
+
f"with the scaffold: {', '.join(unexpected)}. "
|
|
81
|
+
"Move them aside or choose another path."
|
|
82
|
+
)
|
|
54
83
|
dest.mkdir(parents=True, exist_ok=True)
|
|
55
84
|
|
|
56
85
|
|
|
@@ -66,13 +95,20 @@ def _apply_template_files(
|
|
|
66
95
|
substitutions: Mapping[str, str],
|
|
67
96
|
*,
|
|
68
97
|
path_rewrites: Mapping[str, str] | None = None,
|
|
69
|
-
|
|
98
|
+
preserve_existing: frozenset[str] = frozenset(),
|
|
99
|
+
) -> list[Path]:
|
|
100
|
+
"""Write template files into ``dest``; return paths skipped to preserve the user's copy."""
|
|
101
|
+
preserved: list[Path] = []
|
|
70
102
|
for src in _iter_template_files(src_root):
|
|
71
103
|
rel_str = str(src.relative_to(src_root))
|
|
72
104
|
for old, new in (path_rewrites or {}).items():
|
|
73
105
|
rel_str = rel_str.replace(old, new)
|
|
74
106
|
rel_str = rel_str.removesuffix(".tpl")
|
|
75
107
|
target = dest / rel_str
|
|
108
|
+
top = Path(rel_str).parts[0] if rel_str else ""
|
|
109
|
+
if top in preserve_existing and target.exists():
|
|
110
|
+
preserved.append(target)
|
|
111
|
+
continue
|
|
76
112
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
77
113
|
if src.suffix == ".tpl":
|
|
78
114
|
text = src.read_text(encoding="utf-8")
|
|
@@ -81,29 +117,41 @@ def _apply_template_files(
|
|
|
81
117
|
target.write_text(text, encoding="utf-8")
|
|
82
118
|
else:
|
|
83
119
|
shutil.copy2(src, target)
|
|
120
|
+
return preserved
|
|
84
121
|
|
|
85
122
|
|
|
86
123
|
def create_workspace(
|
|
87
124
|
dest: Path,
|
|
88
125
|
name: str,
|
|
89
126
|
template_root: Path | None = None,
|
|
90
|
-
|
|
91
|
-
|
|
127
|
+
*,
|
|
128
|
+
preserve_existing: frozenset[str] = frozenset(),
|
|
129
|
+
) -> list[Path]:
|
|
130
|
+
"""Materialize the workspace-root shell at ``dest``; return preserved paths.
|
|
92
131
|
|
|
93
132
|
Lays down the top-level ``pyproject.toml`` (uv workspace), ``package.json``
|
|
94
133
|
(npm workspace), ``Makefile`` (delegates to host), ``.env.example``,
|
|
95
134
|
``.gitignore``, and ``README.md``. Does NOT create the host or any
|
|
96
135
|
modules — those go under ``dest/host`` and ``dest/modules/`` afterwards.
|
|
136
|
+
|
|
137
|
+
``preserve_existing`` lists top-level entry names that may already exist
|
|
138
|
+
in ``dest``; the scaffold's copy is skipped and the preserved path is
|
|
139
|
+
included in the returned list. Other pre-existing entries raise
|
|
140
|
+
``FileExistsError``.
|
|
97
141
|
"""
|
|
98
142
|
dest = Path(dest)
|
|
99
|
-
dest
|
|
100
|
-
_apply_template_files(
|
|
143
|
+
_require_empty_dest(dest, preserve_existing=preserve_existing)
|
|
144
|
+
preserved = _apply_template_files(
|
|
101
145
|
_resolve_template_root("workspace", template_root),
|
|
102
146
|
dest,
|
|
103
|
-
{
|
|
147
|
+
{
|
|
148
|
+
"{{HOST_NAME}}": validate_scaffold_name(name),
|
|
149
|
+
"{{HOST_PYPI_NAME}}": to_kebab_case(name),
|
|
150
|
+
},
|
|
151
|
+
preserve_existing=preserve_existing,
|
|
104
152
|
)
|
|
105
153
|
logger.info("Scaffolded workspace root at %s", dest)
|
|
106
|
-
return
|
|
154
|
+
return preserved
|
|
107
155
|
|
|
108
156
|
|
|
109
157
|
def create_host(
|
|
@@ -112,23 +160,31 @@ def create_host(
|
|
|
112
160
|
modules: Sequence[str],
|
|
113
161
|
template_root: Path | None = None,
|
|
114
162
|
framework_version: str = "*",
|
|
115
|
-
|
|
163
|
+
*,
|
|
164
|
+
preserve_existing: frozenset[str] = frozenset(),
|
|
165
|
+
) -> list[Path]:
|
|
166
|
+
"""Scaffold a host project at ``dest``; return preserved pre-existing paths.
|
|
167
|
+
|
|
168
|
+
``preserve_existing`` semantics match :func:`create_workspace`.
|
|
169
|
+
"""
|
|
116
170
|
dest = Path(dest)
|
|
117
|
-
_require_empty_dest(dest)
|
|
171
|
+
_require_empty_dest(dest, preserve_existing=preserve_existing)
|
|
118
172
|
module_dep_lines = "\n".join(f' "{_module_to_pypi_name(m)}>=0.1,<1.0",' for m in modules)
|
|
119
|
-
_apply_template_files(
|
|
173
|
+
preserved = _apply_template_files(
|
|
120
174
|
_resolve_template_root("host", template_root),
|
|
121
175
|
dest,
|
|
122
176
|
{
|
|
123
|
-
"{{HOST_NAME}}": name,
|
|
177
|
+
"{{HOST_NAME}}": validate_scaffold_name(name),
|
|
178
|
+
"{{HOST_PYPI_NAME}}": to_kebab_case(name),
|
|
124
179
|
"{{MODULE_DEPS}}": module_dep_lines,
|
|
125
180
|
"{{FRAMEWORK_VERSION}}": framework_version,
|
|
126
181
|
},
|
|
182
|
+
preserve_existing=preserve_existing,
|
|
127
183
|
)
|
|
128
184
|
logger.info(
|
|
129
185
|
"Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
|
|
130
186
|
)
|
|
131
|
-
return
|
|
187
|
+
return preserved
|
|
132
188
|
|
|
133
189
|
|
|
134
190
|
def create_module(
|