simple-module-cli 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
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/app_project.py +19 -1
- simple_module_cli/case.py +11 -3
- simple_module_cli/catalog.py +1 -8
- simple_module_cli/cli.py +6 -1
- simple_module_cli/new.py +8 -0
- simple_module_cli/package_update.py +287 -0
- simple_module_cli/skills/README.md +58 -0
- simple_module_cli/skills/simple-module-cli/SKILL.md +170 -0
- simple_module_cli/skills/simple-module-conventions/SKILL.md +97 -0
- simple_module_cli/skills/simple-module-creating/SKILL.md +104 -0
- simple_module_cli/skills/simple-module-database/SKILL.md +98 -0
- simple_module_cli/skills/simple-module-doctor/SKILL.md +41 -0
- simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +93 -0
- simple_module_cli/skills/simple-module-locales/SKILL.md +125 -0
- simple_module_cli/skills/simple-module-migrations/SKILL.md +103 -0
- simple_module_cli/skills/simple-module-registries/SKILL.md +144 -0
- simple_module_cli/skills/simple-module-testing/SKILL.md +102 -0
- simple_module_cli/skills_cmd.py +249 -0
- simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +11 -2
- simple_module_cli/templates/host/client_app/app.tsx +22 -1
- simple_module_cli/templates/host/client_app/package.json.tpl +4 -0
- simple_module_cli/templates/host/client_app/pages.ts +3 -3
- simple_module_cli/templates/host/client_app/styles.css +9 -4
- simple_module_cli/templates/host/client_app/vite.config.ts +22 -1
- simple_module_cli-0.0.4.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +26 -0
- simple_module_cli-0.0.4.data/data/simple_module_cli/templates/host/client_app/app.tsx +37 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/client_app/package.json.tpl +4 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/client_app/pages.ts +3 -3
- simple_module_cli-0.0.4.data/data/simple_module_cli/templates/host/client_app/styles.css +12 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/client_app/vite.config.ts +22 -1
- {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/METADATA +1 -1
- {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/RECORD +66 -55
- simple_module_cli/templates/module/tests/__init__.py +0 -0
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -17
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/app.tsx +0 -16
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/styles.css +0 -7
- simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/__init__.py +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/WHEEL +0 -0
- {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/entry_points.txt +0 -0
- {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/licenses/LICENSE +0 -0
simple_module_cli/app_project.py
CHANGED
|
@@ -15,6 +15,8 @@ from __future__ import annotations
|
|
|
15
15
|
import json as _json
|
|
16
16
|
import secrets as _secrets
|
|
17
17
|
from collections.abc import Sequence
|
|
18
|
+
from importlib.metadata import PackageNotFoundError
|
|
19
|
+
from importlib.metadata import version as _pkg_version
|
|
18
20
|
from pathlib import Path
|
|
19
21
|
|
|
20
22
|
from simple_module_cli._env import set_env_key
|
|
@@ -25,7 +27,23 @@ from simple_module_cli.scaffolding import create_host
|
|
|
25
27
|
|
|
26
28
|
__all__ = ["create_app_project"]
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
|
|
31
|
+
def _resolve_framework_version() -> str:
|
|
32
|
+
"""Resolve the framework version to pin scaffolded apps against.
|
|
33
|
+
|
|
34
|
+
The CLI ships in lockstep with the rest of the framework (one
|
|
35
|
+
``bump_version.py`` rewrites every ``pyproject.toml`` in the repo), so
|
|
36
|
+
its own installed version is the source of truth. Falling back to a
|
|
37
|
+
placeholder lets editable installs without dist-info still scaffold —
|
|
38
|
+
but that path should never be reached in a release wheel.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
return _pkg_version("simple_module_cli")
|
|
42
|
+
except PackageNotFoundError:
|
|
43
|
+
return "0.0.0"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_FRAMEWORK_VERSION = _resolve_framework_version()
|
|
29
47
|
|
|
30
48
|
_APP_PY_DEV_DEPS = [f"simple_module_test=={_FRAMEWORK_VERSION}", "pytest>=8.0"]
|
|
31
49
|
|
simple_module_cli/case.py
CHANGED
|
@@ -13,9 +13,17 @@ __all__ = ["to_kebab_case", "to_pascal_case", "to_snake_case"]
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def to_snake_case(name: str) -> str:
|
|
16
|
-
"""'MyFeature' / 'my-feature' / 'My Feature' -> 'my_feature'.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
"""'MyFeature' / 'my-feature' / 'My Feature' / 'URLPath' -> 'my_feature' / 'url_path'.
|
|
17
|
+
|
|
18
|
+
Handles acronyms by treating ``Acronym|Word`` and ``word|Capital`` as
|
|
19
|
+
boundaries: ``URLPath`` -> ``url_path``, ``APIClient`` -> ``api_client``,
|
|
20
|
+
``HTTPServer2`` -> ``http_server2``. The single-pass ``(?=[A-Z])`` form
|
|
21
|
+
that preceded this would emit ``u_r_l_path`` and propagate the typo
|
|
22
|
+
into the PyPI slug + display name.
|
|
23
|
+
"""
|
|
24
|
+
s = re.sub(r"[\s\-]+", "_", name)
|
|
25
|
+
s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", s)
|
|
26
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
|
|
19
27
|
return s.lower()
|
|
20
28
|
|
|
21
29
|
|
simple_module_cli/catalog.py
CHANGED
|
@@ -36,12 +36,11 @@ CATALOG: dict[str, ModuleEntry] = {
|
|
|
36
36
|
"Permissions",
|
|
37
37
|
requires=("auth", "users"),
|
|
38
38
|
),
|
|
39
|
-
"products": ModuleEntry("products", "simple_module_products", "Products"),
|
|
40
39
|
"dashboard": ModuleEntry(
|
|
41
40
|
"dashboard",
|
|
42
41
|
"simple_module_dashboard",
|
|
43
42
|
"Dashboard",
|
|
44
|
-
requires=("users",
|
|
43
|
+
requires=("users",),
|
|
45
44
|
),
|
|
46
45
|
"settings": ModuleEntry("settings", "simple_module_settings", "Settings"),
|
|
47
46
|
"feature_flags": ModuleEntry("feature_flags", "simple_module_feature_flags", "Feature Flags"),
|
|
@@ -58,12 +57,6 @@ CATALOG: dict[str, ModuleEntry] = {
|
|
|
58
57
|
requires=("users",),
|
|
59
58
|
recipe="background_tasks",
|
|
60
59
|
),
|
|
61
|
-
"datasets": ModuleEntry(
|
|
62
|
-
"datasets",
|
|
63
|
-
"simple_module_datasets",
|
|
64
|
-
"Datasets",
|
|
65
|
-
requires=("file_storage", "background_tasks"),
|
|
66
|
-
),
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
|
simple_module_cli/cli.py
CHANGED
|
@@ -4,6 +4,7 @@ Built-in commands:
|
|
|
4
4
|
sm new
|
|
5
5
|
sm create-host
|
|
6
6
|
sm create-module
|
|
7
|
+
sm skills add / list / update
|
|
7
8
|
|
|
8
9
|
Plugins discovered via the ``simple_module_cli.cli_plugins`` entry-point
|
|
9
10
|
group are mounted as named subgroups (e.g. ``sm host gen-pages``).
|
|
@@ -18,9 +19,11 @@ import typer
|
|
|
18
19
|
|
|
19
20
|
from simple_module_cli.case import to_kebab_case
|
|
20
21
|
from simple_module_cli.new import new_project
|
|
22
|
+
from simple_module_cli.package_update import package_update
|
|
21
23
|
from simple_module_cli.plugins import discover_and_mount
|
|
22
24
|
from simple_module_cli.scaffolding import create_host as _create_host
|
|
23
25
|
from simple_module_cli.scaffolding import create_module as _create_module
|
|
26
|
+
from simple_module_cli.skills_cmd import app as skills_app
|
|
24
27
|
|
|
25
28
|
app = typer.Typer(
|
|
26
29
|
help="SimpleModule developer CLI.",
|
|
@@ -29,6 +32,8 @@ app = typer.Typer(
|
|
|
29
32
|
)
|
|
30
33
|
|
|
31
34
|
app.command("new")(new_project)
|
|
35
|
+
app.command("package-update")(package_update)
|
|
36
|
+
app.add_typer(skills_app, name="skills")
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
@app.command("create-host")
|
|
@@ -42,7 +47,7 @@ def create_host(
|
|
|
42
47
|
str,
|
|
43
48
|
typer.Option(
|
|
44
49
|
"--with",
|
|
45
|
-
help="Comma-separated module names to declare as deps (e.g. Auth,
|
|
50
|
+
help="Comma-separated module names to declare as deps (e.g. Auth,Dashboard).",
|
|
46
51
|
),
|
|
47
52
|
] = "",
|
|
48
53
|
) -> None:
|
simple_module_cli/new.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
from enum import StrEnum
|
|
7
8
|
from pathlib import Path
|
|
@@ -109,6 +110,13 @@ def new_project(
|
|
|
109
110
|
|
|
110
111
|
typer.echo("Installing dependencies...")
|
|
111
112
|
for cmd in (["uv", "sync"], ["npm", "install"]):
|
|
113
|
+
if shutil.which(cmd[0]) is None:
|
|
114
|
+
typer.echo(
|
|
115
|
+
f"WARNING: '{cmd[0]}' not found on PATH; skipping `{' '.join(cmd)}`. "
|
|
116
|
+
"Install it and finish setup manually.",
|
|
117
|
+
err=True,
|
|
118
|
+
)
|
|
119
|
+
return
|
|
112
120
|
result = subprocess.run(cmd, cwd=target, check=False)
|
|
113
121
|
if result.returncode != 0:
|
|
114
122
|
typer.echo(
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""``sm package-update`` — bump simple_module_* deps to latest PyPI versions.
|
|
2
|
+
|
|
3
|
+
Walks the project's ``pyproject.toml`` (and any ``[tool.uv.workspace]`` members),
|
|
4
|
+
finds every dependency whose distribution name starts with ``simple_module_`` /
|
|
5
|
+
``simple-module-``, queries PyPI for the latest non-yanked release, and rewrites
|
|
6
|
+
the constraint to ``name>=<latest>``.
|
|
7
|
+
|
|
8
|
+
Dependencies whose ``[tool.uv.sources]`` entry points at a workspace member, a
|
|
9
|
+
local path, a git ref, or a URL are left untouched — those aren't installed
|
|
10
|
+
from PyPI.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Annotated, Any
|
|
24
|
+
|
|
25
|
+
import tomlkit
|
|
26
|
+
import typer
|
|
27
|
+
from tomlkit.items import Array, Table
|
|
28
|
+
|
|
29
|
+
__all__ = ["package_update", "run_update"]
|
|
30
|
+
|
|
31
|
+
Fetcher = Callable[[str], dict[str, Any]]
|
|
32
|
+
|
|
33
|
+
_PYPI_URL = "https://pypi.org/pypi/{name}/json"
|
|
34
|
+
_SM_PREFIX_RE = re.compile(r"^simple[_-]module[_-]", re.IGNORECASE)
|
|
35
|
+
# PEP 440 release segments contain only digits + dots; any letter signals
|
|
36
|
+
# a pre/post/dev release (a, b, rc, post, dev). Coarser than packaging.version
|
|
37
|
+
# but `packaging` isn't a CLI dep (see test_no_framework_deps.py).
|
|
38
|
+
_PRE_RELEASE_RE = re.compile(r"[a-zA-Z]")
|
|
39
|
+
_REQ_OPS = ("===", "==", ">=", "<=", "!=", "~=", ">", "<")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Change:
|
|
44
|
+
file: Path
|
|
45
|
+
package: str
|
|
46
|
+
old: str
|
|
47
|
+
new: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Skip:
|
|
52
|
+
file: Path
|
|
53
|
+
package: str
|
|
54
|
+
reason: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_sm_package(name: str) -> bool:
|
|
58
|
+
return bool(_SM_PREFIX_RE.match(name))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _dep_name(spec: str) -> str | None:
|
|
62
|
+
"""Extract the distribution name from a PEP 508 requirement string."""
|
|
63
|
+
base = spec.split(";", 1)[0].strip()
|
|
64
|
+
base = base.split("[", 1)[0]
|
|
65
|
+
for op in _REQ_OPS:
|
|
66
|
+
if op in base:
|
|
67
|
+
base = base.split(op, 1)[0]
|
|
68
|
+
break
|
|
69
|
+
name = base.strip()
|
|
70
|
+
return name or None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_local_source(source: Any) -> bool:
|
|
74
|
+
if not isinstance(source, dict):
|
|
75
|
+
return False
|
|
76
|
+
if source.get("workspace") is True:
|
|
77
|
+
return True
|
|
78
|
+
return any(key in source for key in ("path", "git", "url"))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_uv_section(doc: tomlkit.TOMLDocument, key: str) -> dict[str, Any] | None:
|
|
82
|
+
tool = doc.get("tool")
|
|
83
|
+
if not isinstance(tool, dict):
|
|
84
|
+
return None
|
|
85
|
+
uv = tool.get("uv")
|
|
86
|
+
if not isinstance(uv, dict):
|
|
87
|
+
return None
|
|
88
|
+
section = uv.get(key)
|
|
89
|
+
return section if isinstance(section, dict) else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _fetch_latest(name: str, *, include_pre: bool, fetcher: Fetcher) -> str | None:
|
|
93
|
+
try:
|
|
94
|
+
data = fetcher(_PYPI_URL.format(name=name))
|
|
95
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
96
|
+
return None
|
|
97
|
+
releases = data.get("releases") or {}
|
|
98
|
+
candidates: list[str] = []
|
|
99
|
+
for version, files in releases.items():
|
|
100
|
+
if not files:
|
|
101
|
+
continue
|
|
102
|
+
if any(f.get("yanked") for f in files):
|
|
103
|
+
continue
|
|
104
|
+
if not include_pre and _PRE_RELEASE_RE.search(version):
|
|
105
|
+
continue
|
|
106
|
+
candidates.append(version)
|
|
107
|
+
if candidates:
|
|
108
|
+
return max(candidates, key=_version_key)
|
|
109
|
+
info = data.get("info") or {}
|
|
110
|
+
return info.get("version")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _version_key(v: str) -> tuple[int, ...]:
|
|
114
|
+
parts: list[int] = []
|
|
115
|
+
for part in v.split("."):
|
|
116
|
+
digits = re.match(r"\d+", part)
|
|
117
|
+
parts.append(int(digits.group()) if digits else 0)
|
|
118
|
+
return tuple(parts)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _default_fetcher(url: str) -> dict[str, Any]:
|
|
122
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
123
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _workspace_member_dirs(root_pyproject: Path, doc: tomlkit.TOMLDocument) -> list[Path]:
|
|
127
|
+
workspace = _get_uv_section(doc, "workspace")
|
|
128
|
+
if workspace is None:
|
|
129
|
+
return []
|
|
130
|
+
members = workspace.get("members") or []
|
|
131
|
+
base = root_pyproject.parent
|
|
132
|
+
out: list[Path] = []
|
|
133
|
+
for pattern in members:
|
|
134
|
+
for match in sorted(base.glob(str(pattern))):
|
|
135
|
+
if (match / "pyproject.toml").is_file():
|
|
136
|
+
out.append(match / "pyproject.toml")
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _local_sources(doc: tomlkit.TOMLDocument) -> set[str]:
|
|
141
|
+
sources = _get_uv_section(doc, "sources")
|
|
142
|
+
if sources is None:
|
|
143
|
+
return set()
|
|
144
|
+
return {name for name, src in sources.items() if _is_local_source(src)}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _collect_sm_deps(doc: tomlkit.TOMLDocument) -> list[str]:
|
|
148
|
+
"""Return distribution names of simple_module_* deps in this doc that aren't local-sourced."""
|
|
149
|
+
project = doc.get("project")
|
|
150
|
+
if not isinstance(project, (dict, Table)):
|
|
151
|
+
return []
|
|
152
|
+
deps = project.get("dependencies")
|
|
153
|
+
if not isinstance(deps, (list, Array)):
|
|
154
|
+
return []
|
|
155
|
+
local = _local_sources(doc)
|
|
156
|
+
out: list[str] = []
|
|
157
|
+
for raw in deps:
|
|
158
|
+
name = _dep_name(str(raw))
|
|
159
|
+
if name and _is_sm_package(name) and name not in local:
|
|
160
|
+
out.append(name)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _process_file(
|
|
165
|
+
path: Path,
|
|
166
|
+
doc: tomlkit.TOMLDocument,
|
|
167
|
+
*,
|
|
168
|
+
cache: dict[str, str | None],
|
|
169
|
+
) -> tuple[list[Change], list[Skip], tomlkit.TOMLDocument | None]:
|
|
170
|
+
project = doc.get("project")
|
|
171
|
+
if not isinstance(project, (dict, Table)):
|
|
172
|
+
return [], [], None
|
|
173
|
+
deps = project.get("dependencies")
|
|
174
|
+
if not isinstance(deps, (list, Array)):
|
|
175
|
+
return [], [], None
|
|
176
|
+
|
|
177
|
+
local = _local_sources(doc)
|
|
178
|
+
changes: list[Change] = []
|
|
179
|
+
skips: list[Skip] = []
|
|
180
|
+
|
|
181
|
+
for idx, raw in enumerate(deps):
|
|
182
|
+
dep_str = str(raw)
|
|
183
|
+
name = _dep_name(dep_str)
|
|
184
|
+
if not name or not _is_sm_package(name):
|
|
185
|
+
continue
|
|
186
|
+
if name in local:
|
|
187
|
+
skips.append(Skip(path, name, "workspace/local source"))
|
|
188
|
+
continue
|
|
189
|
+
latest = cache.get(name)
|
|
190
|
+
if latest is None:
|
|
191
|
+
skips.append(Skip(path, name, "not found on PyPI"))
|
|
192
|
+
continue
|
|
193
|
+
new_dep = f"{name}>={latest}"
|
|
194
|
+
if new_dep == dep_str.strip():
|
|
195
|
+
continue
|
|
196
|
+
deps[idx] = new_dep
|
|
197
|
+
changes.append(Change(path, name, dep_str.strip(), new_dep))
|
|
198
|
+
|
|
199
|
+
return changes, skips, doc if changes else None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _print_summary(changes: list[Change], skips: list[Skip], dry_run: bool) -> None:
|
|
203
|
+
if not changes and not skips:
|
|
204
|
+
typer.echo("No simple_module_* dependencies found.")
|
|
205
|
+
return
|
|
206
|
+
by_file: dict[Path, list[str]] = {}
|
|
207
|
+
for c in changes:
|
|
208
|
+
by_file.setdefault(c.file, []).append(f" {c.package}: {c.old} → {c.new}")
|
|
209
|
+
for s in skips:
|
|
210
|
+
by_file.setdefault(s.file, []).append(f" {s.package}: skipped ({s.reason})")
|
|
211
|
+
for file, lines in by_file.items():
|
|
212
|
+
typer.echo(f"\n{file}")
|
|
213
|
+
for line in lines:
|
|
214
|
+
typer.echo(line)
|
|
215
|
+
if changes:
|
|
216
|
+
verb = "Would update" if dry_run else "Updated"
|
|
217
|
+
typer.echo(f"\n{verb} {len(changes)} dependency(ies) across {len(by_file)} file(s).")
|
|
218
|
+
if not dry_run:
|
|
219
|
+
typer.echo("Run `uv sync` to apply.")
|
|
220
|
+
elif skips:
|
|
221
|
+
typer.echo("\nNo updates applied.")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def run_update(
|
|
225
|
+
path: Path,
|
|
226
|
+
*,
|
|
227
|
+
dry_run: bool = False,
|
|
228
|
+
include_pre: bool = False,
|
|
229
|
+
fetcher: Fetcher | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Programmatic entry point — separated from the Typer command for testing."""
|
|
232
|
+
root = path if path.name == "pyproject.toml" else path / "pyproject.toml"
|
|
233
|
+
if not root.is_file():
|
|
234
|
+
typer.echo(f"ERROR: {root} not found.", err=True)
|
|
235
|
+
raise typer.Exit(code=1)
|
|
236
|
+
|
|
237
|
+
fetch = fetcher or _default_fetcher
|
|
238
|
+
root_doc = tomlkit.parse(root.read_text(encoding="utf-8"))
|
|
239
|
+
files: list[tuple[Path, tomlkit.TOMLDocument]] = [(root, root_doc)]
|
|
240
|
+
for member in _workspace_member_dirs(root, root_doc):
|
|
241
|
+
files.append((member, tomlkit.parse(member.read_text(encoding="utf-8"))))
|
|
242
|
+
|
|
243
|
+
# Pre-fetch all unique sm packages in parallel — PyPI calls dominate runtime.
|
|
244
|
+
unique_names = sorted({n for _, doc in files for n in _collect_sm_deps(doc)})
|
|
245
|
+
cache: dict[str, str | None] = {}
|
|
246
|
+
if unique_names:
|
|
247
|
+
with ThreadPoolExecutor(max_workers=min(8, len(unique_names))) as pool:
|
|
248
|
+
results = pool.map(
|
|
249
|
+
lambda n: (n, _fetch_latest(n, include_pre=include_pre, fetcher=fetch)),
|
|
250
|
+
unique_names,
|
|
251
|
+
)
|
|
252
|
+
cache = dict(results)
|
|
253
|
+
|
|
254
|
+
all_changes: list[Change] = []
|
|
255
|
+
all_skips: list[Skip] = []
|
|
256
|
+
pending: list[tuple[Path, tomlkit.TOMLDocument]] = []
|
|
257
|
+
|
|
258
|
+
for file, doc in files:
|
|
259
|
+
changes, skips, new_doc = _process_file(file, doc, cache=cache)
|
|
260
|
+
all_changes.extend(changes)
|
|
261
|
+
all_skips.extend(skips)
|
|
262
|
+
if new_doc is not None:
|
|
263
|
+
pending.append((file, new_doc))
|
|
264
|
+
|
|
265
|
+
if not dry_run:
|
|
266
|
+
for file, doc in pending:
|
|
267
|
+
file.write_text(tomlkit.dumps(doc), encoding="utf-8")
|
|
268
|
+
|
|
269
|
+
_print_summary(all_changes, all_skips, dry_run)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def package_update(
|
|
273
|
+
path: Annotated[
|
|
274
|
+
Path,
|
|
275
|
+
typer.Option("--path", help="Project root or pyproject.toml. Defaults to cwd."),
|
|
276
|
+
] = Path(),
|
|
277
|
+
dry_run: Annotated[
|
|
278
|
+
bool,
|
|
279
|
+
typer.Option("--dry-run", help="Show planned changes without writing."),
|
|
280
|
+
] = False,
|
|
281
|
+
include_pre: Annotated[
|
|
282
|
+
bool,
|
|
283
|
+
typer.Option("--include-pre", help="Include pre-release versions."),
|
|
284
|
+
] = False,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Update all simple_module_* dependencies to the latest PyPI versions."""
|
|
287
|
+
run_update(path, dry_run=dry_run, include_pre=include_pre)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# simple_module_python skills
|
|
2
|
+
|
|
3
|
+
Agent skills for working in a [simple_module_python](https://github.com/antosubash/simple_module_python) codebase. Compatible with Claude Code, Codex, Cursor, Windsurf, OpenCode, and any other agent that supports the [Agent Skills format](https://agentskills.io/specification).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
There are two install paths — pick whichever fits your project.
|
|
8
|
+
|
|
9
|
+
### Option A — `sm skills` (recommended for `simple_module_cli` users)
|
|
10
|
+
|
|
11
|
+
Every project produced by `sm new` already depends on `simple_module_cli`, which ships these skills inside its wheel. From the project root:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
sm skills list # see what's available
|
|
15
|
+
sm skills add # install ALL skills into ./.claude/skills/
|
|
16
|
+
sm skills add simple-module-creating # install just one
|
|
17
|
+
sm skills add -g # install into ~/.claude/skills (machine-wide)
|
|
18
|
+
sm skills add --dest agents/skills # custom target dir
|
|
19
|
+
sm skills add --symlink # symlink to the bundled source (good for skill devs)
|
|
20
|
+
sm skills update # re-pull updates for skills already installed
|
|
21
|
+
sm skills update simple-module-doctor # explicit re-pull (force-overwrites)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`sm skills` resolves the bundled set against whatever version of `simple_module_cli` is installed, so upgrading the CLI ships skill updates the next time you run `sm skills update`.
|
|
25
|
+
|
|
26
|
+
### Option B — `npx skills` (no Python install needed)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx skills add antosubash/simple_module_python # all skills, current project
|
|
30
|
+
npx skills add antosubash/simple_module_python -g # globally
|
|
31
|
+
npx skills add antosubash/simple_module_python --skill simple-module-creating -a claude-code
|
|
32
|
+
npx skills add antosubash/simple_module_python --list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The CLI is [vercel-labs/skills](https://github.com/vercel-labs/skills); see its README for symlink-vs-copy and other options.
|
|
36
|
+
|
|
37
|
+
## What's here
|
|
38
|
+
|
|
39
|
+
| Skill | Use when |
|
|
40
|
+
|---|---|
|
|
41
|
+
| [simple-module-cli](./simple-module-cli/SKILL.md) | Invoking the `sm` CLI — `sm new`, `sm create-host`, `sm create-module`, `sm host gen-pages`, `sm users create-admin`, etc. |
|
|
42
|
+
| [simple-module-creating](./simple-module-creating/SKILL.md) | Adding a new feature package — scaffolding, entry-point, `ModuleMeta` |
|
|
43
|
+
| [simple-module-conventions](./simple-module-conventions/SKILL.md) | Writing or reviewing module code — the invariant list (SQLModel everywhere, settings layout, framework→plugin direction, etc.) |
|
|
44
|
+
| [simple-module-database](./simple-module-database/SKILL.md) | Adding SQLModel tables, picking a mixin, or debugging session/transaction behavior |
|
|
45
|
+
| [simple-module-migrations](./simple-module-migrations/SKILL.md) | Generating, applying, or reviewing Alembic migrations after installing or changing a module |
|
|
46
|
+
| [simple-module-inertia-pages](./simple-module-inertia-pages/SKILL.md) | Adding or debugging an Inertia page in a module — render keys, shared props, common pitfalls |
|
|
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
|
+
| [simple-module-registries](./simple-module-registries/SKILL.md) | Contributing menu items, permissions, feature flags, or events from a module |
|
|
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 |
|
|
51
|
+
|
|
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
|
+
|
|
54
|
+
## Contributing
|
|
55
|
+
|
|
56
|
+
Each skill is a directory containing a `SKILL.md` with YAML frontmatter (`name`, `description`). The description is "use when…" triggers only — never a workflow summary, because agents will follow the description in lieu of reading the body.
|
|
57
|
+
|
|
58
|
+
PRs welcome.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: simple-module-cli
|
|
3
|
+
description: Use when invoking the `sm` CLI for a simple_module_python project — starting a new app, scaffolding a host or a publishable module, regenerating the Inertia page manifest, importing settings overrides from env, creating an admin user, or installing the bundled agent skills. Triggers on "sm new", "sm create-host", "sm create-module", "sm host gen-pages", "sm users create-admin", "sm skills add", or any unfamiliar `sm` subcommand.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# simple_module_python: the `sm` CLI
|
|
7
|
+
|
|
8
|
+
The `sm` 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.
|
|
9
|
+
|
|
10
|
+
## Top-level commands
|
|
11
|
+
|
|
12
|
+
| Command | When to use |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `sm new <name>` | Greenfield: scaffold a complete app (host + selected modules) in one shot, with an interactive wizard for DB / tenancy / module preset |
|
|
15
|
+
| `sm create-host <name>` | You want just a bare host project; you'll add modules later by `pip install`-ing them |
|
|
16
|
+
| `sm create-module <name>` | You're authoring a publishable module package (separate repo, distributed via PyPI) |
|
|
17
|
+
| `sm skills …` | Install / update the bundled agent-skill packs into a project (`add`, `list`, `update`) |
|
|
18
|
+
| `sm host …` | Project-time helpers run from inside a host directory (page manifest, JS dep sync) |
|
|
19
|
+
| `sm settings …` | Settings-module admin — currently `import-from-env` |
|
|
20
|
+
| `sm users …` | Users-module admin — currently `create-admin` |
|
|
21
|
+
|
|
22
|
+
## `sm new <name>` — the wizard
|
|
23
|
+
|
|
24
|
+
The fastest way from zero to a working app. It calls `create-host` under the hood, then installs and wires up the modules you pick.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Interactive (asks for DB, tenancy, preset, module list)
|
|
28
|
+
sm new MyApp
|
|
29
|
+
|
|
30
|
+
# Non-interactive: take all defaults (sqlite, no tenancy, standard preset)
|
|
31
|
+
sm new MyApp --yes
|
|
32
|
+
|
|
33
|
+
# Pick a preset and add extras
|
|
34
|
+
sm new MyApp --preset full --tenancy --db postgres
|
|
35
|
+
sm new MyApp --preset minimal --with background_tasks,file_storage --yes
|
|
36
|
+
|
|
37
|
+
# Scaffold only — skip uv sync / npm install / alembic upgrade head
|
|
38
|
+
sm new MyApp --no-install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Module presets:**
|
|
42
|
+
|
|
43
|
+
| Preset | Modules included |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `minimal` | `users` (and `auth` as a dep) |
|
|
46
|
+
| `standard` (default) | `users`, `dashboard`, `permissions` (+ deps) |
|
|
47
|
+
| `full` | every module in the catalog |
|
|
48
|
+
| `custom` | interactive — pick each module yes/no |
|
|
49
|
+
|
|
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
|
+
|
|
52
|
+
**Options summary:**
|
|
53
|
+
|
|
54
|
+
| Flag | Default | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `--dest <PATH>` | `./<name>` | Where to write the project |
|
|
57
|
+
| `--db sqlite\|postgres` | `sqlite` | Backend configured in `.env.example` |
|
|
58
|
+
| `--tenancy / --no-tenancy` | `--no-tenancy` | Enable the multi-tenant middleware |
|
|
59
|
+
| `--preset minimal\|standard\|full` | wizard asks | Module bundle |
|
|
60
|
+
| `--with <names>` | none | Extra catalog keys beyond the preset |
|
|
61
|
+
| `--yes / -y` | off | Skip prompts; accept defaults |
|
|
62
|
+
| `--no-install` | off | Skip `uv sync` / `npm install` / `alembic upgrade head` |
|
|
63
|
+
|
|
64
|
+
## `sm create-host <name>` — bare host
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
sm create-host MyApp # empty host, no modules declared
|
|
68
|
+
sm create-host MyApp --with Auth,Products # declare module deps in pyproject.toml
|
|
69
|
+
sm create-host MyApp --dest ./apps/myapp # custom destination
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--with` takes **PascalCase module names** (matching `ModuleMeta.name`), not catalog keys. Use this when you want to drive the build yourself rather than via `sm new`'s wizard.
|
|
73
|
+
|
|
74
|
+
## `sm create-module <name>` — module package
|
|
75
|
+
|
|
76
|
+
For module authors publishing to PyPI. Scaffolds a standalone repo containing one module.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
sm create-module orders # writes ./simple_module_orders/
|
|
80
|
+
sm create-module orders --dest ./packages/orders
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The result is a complete package: `pyproject.toml` with the entry point declared, `module.py` with `ModuleBase`/`ModuleMeta` skeleton, `models.py`, `contracts/`, `endpoints/{api,views}.py`, `pages/`, `locales/en.json`, plus a tests directory wired up with `simple_module_test` fixtures.
|
|
84
|
+
|
|
85
|
+
`<name>` accepts any case — `orders`, `Orders`, `ORDERS`, `blog_posts` all work. The CLI lowercases it for the directory and PascalCases it for `ModuleMeta.name`.
|
|
86
|
+
|
|
87
|
+
For the post-scaffold steps (entry point, Inertia namespace, etc.) see **simple-module-creating**.
|
|
88
|
+
|
|
89
|
+
## `sm skills` — install the bundled agent skills
|
|
90
|
+
|
|
91
|
+
`simple_module_cli` ships a set of [SKILL.md](https://agentskills.io/specification) packs (the ones in this directory). Drop them into any project so Claude Code / Cursor / Codex / etc. find them automatically.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
sm skills list # see what's available
|
|
95
|
+
sm skills add # install ALL skills into ./.claude/skills/
|
|
96
|
+
sm skills add simple-module-creating simple-module-cli # specific ones only
|
|
97
|
+
sm skills add -g # ~/.claude/skills (machine-wide)
|
|
98
|
+
sm skills add --dest agents/skills # explicit target dir
|
|
99
|
+
sm skills add --symlink # symlink to bundled source (good when iterating on the skills themselves)
|
|
100
|
+
sm skills update # re-pull whatever is already installed at the dest
|
|
101
|
+
sm skills update simple-module-doctor # explicitly re-pull one (always force-overwrites)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Without `--force`, `sm skills add` skips skills that already exist at the destination** — so re-running it is safe. Use `--force` (or `sm skills update`) to overwrite.
|
|
105
|
+
|
|
106
|
+
The bundle resolves against your installed `simple_module_cli`. To get newer skills, upgrade the CLI (`uv sync` or `pip install -U simple_module_cli`) and re-run `sm skills update`.
|
|
107
|
+
|
|
108
|
+
## `sm host gen-pages` — regenerate the Inertia manifest
|
|
109
|
+
|
|
110
|
+
Run from a host project. Scans every installed module's `pages/*.tsx`, writes `client_app/modules.{manifest.json,generated.ts,generated.css}`, and extends Vite's `server.fs.allow`.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
sm host gen-pages # uses ./client_app
|
|
114
|
+
sm host gen-pages --host-dir=apps/web/client_app
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`sm new` runs this at scaffold time; you only need to call it manually after adding/renaming `.tsx` files mid-session, or after `pip install`-ing a new module that ships pages.
|
|
118
|
+
|
|
119
|
+
## `sm host sync-js-deps` — install module JS deps
|
|
120
|
+
|
|
121
|
+
Wheel-installed modules ship `package.json` declarations that need to land in the host's `client_app/node_modules`. This command does that. **In-repo workspace modules don't need it** — npm workspaces resolve them automatically.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
sm host sync-js-deps # uses ./client_app
|
|
125
|
+
sm host sync-js-deps --host-client-app=apps/web/client_app
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Run after `pip install <module>` if the new module ships frontend code.
|
|
129
|
+
|
|
130
|
+
## `sm settings import-from-env`
|
|
131
|
+
|
|
132
|
+
Walks the live environment for every `SM_<PREFIX>_<FIELD>` variable matching a registered settings dataclass and writes a SYSTEM-tier override into the settings module's store. Useful when promoting from environment-driven config (typical in Docker) to in-DB overrides (manageable in the admin UI) without re-keying values by hand.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
sm settings import-from-env
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## `sm users create-admin`
|
|
139
|
+
|
|
140
|
+
Bootstraps the first admin user, or rotates an existing admin's password.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
sm users create-admin -e admin@example.com -p hunter2
|
|
144
|
+
sm users create-admin -e admin@example.com -p new-password --force # rotate
|
|
145
|
+
sm users create-admin -e admin@example.com -p hunter2 --full-name "Admin"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Flag | Meaning |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `-e, --email` (required) | Admin email |
|
|
151
|
+
| `-p, --password` (required) | Initial password (or new password with `--force`) |
|
|
152
|
+
| `--full-name` | Display name |
|
|
153
|
+
| `--force` | Update the password if the admin already exists; without it, the command exits if the email is taken |
|
|
154
|
+
|
|
155
|
+
Don't bake `--password` literals into a script you commit; use a secrets store and pass via shell expansion.
|
|
156
|
+
|
|
157
|
+
## Pitfalls
|
|
158
|
+
|
|
159
|
+
- **Wrong shell for `--with`.** `sm new` `--with auth,users` (catalog keys, lowercase). `sm create-host` `--with Auth,Users` (`ModuleMeta.name`, PascalCase). They're not interchangeable.
|
|
160
|
+
- **Ran `sm new` inside an existing project.** The default `--dest ./<name>` creates a sibling directory. If the directory already exists and is non-empty, the command errors out — pass `--dest` explicitly to disambiguate.
|
|
161
|
+
- **Ran `sm host gen-pages` from outside the host directory.** Defaults to `./client_app`; pass `--host-dir` from elsewhere.
|
|
162
|
+
- **Forgot `sm host sync-js-deps` after `pip install`-ing a module with pages.** Vite resolves module imports against `client_app/node_modules`; the new module's JS deps won't land until you sync.
|
|
163
|
+
- **Used `sm create-module` to add a module to an existing host.** That command is for **publishable** packages, intended to live in their own repo. To add a module to an existing host: install it (`pip install simple_module_<name>` or add to `pyproject.toml` and `uv sync`), then autogenerate a migration. See **simple-module-creating** + **simple-module-migrations**.
|
|
164
|
+
- **Calling `sm create-admin` before migrations have run.** The users tables don't exist yet; the command will error. Run `alembic upgrade head` first (or use `sm new` which does it for you when `--no-install` isn't set).
|
|
165
|
+
|
|
166
|
+
## Related skills
|
|
167
|
+
|
|
168
|
+
- **simple-module-creating** — what `sm create-module` produces and the post-scaffold contract
|
|
169
|
+
- **simple-module-inertia-pages** — what `sm host gen-pages` regenerates and why
|
|
170
|
+
- **simple-module-migrations** — the `alembic upgrade head` step `sm new` runs
|