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.
Files changed (71) hide show
  1. simple_module_cli/app_project.py +19 -1
  2. simple_module_cli/case.py +11 -3
  3. simple_module_cli/catalog.py +1 -8
  4. simple_module_cli/cli.py +6 -1
  5. simple_module_cli/new.py +8 -0
  6. simple_module_cli/package_update.py +287 -0
  7. simple_module_cli/skills/README.md +58 -0
  8. simple_module_cli/skills/simple-module-cli/SKILL.md +170 -0
  9. simple_module_cli/skills/simple-module-conventions/SKILL.md +97 -0
  10. simple_module_cli/skills/simple-module-creating/SKILL.md +104 -0
  11. simple_module_cli/skills/simple-module-database/SKILL.md +98 -0
  12. simple_module_cli/skills/simple-module-doctor/SKILL.md +41 -0
  13. simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +93 -0
  14. simple_module_cli/skills/simple-module-locales/SKILL.md +125 -0
  15. simple_module_cli/skills/simple-module-migrations/SKILL.md +103 -0
  16. simple_module_cli/skills/simple-module-registries/SKILL.md +144 -0
  17. simple_module_cli/skills/simple-module-testing/SKILL.md +102 -0
  18. simple_module_cli/skills_cmd.py +249 -0
  19. simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +11 -2
  20. simple_module_cli/templates/host/client_app/app.tsx +22 -1
  21. simple_module_cli/templates/host/client_app/package.json.tpl +4 -0
  22. simple_module_cli/templates/host/client_app/pages.ts +3 -3
  23. simple_module_cli/templates/host/client_app/styles.css +9 -4
  24. simple_module_cli/templates/host/client_app/vite.config.ts +22 -1
  25. simple_module_cli-0.0.4.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +26 -0
  26. simple_module_cli-0.0.4.data/data/simple_module_cli/templates/host/client_app/app.tsx +37 -0
  27. {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
  28. {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
  29. simple_module_cli-0.0.4.data/data/simple_module_cli/templates/host/client_app/styles.css +12 -0
  30. {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
  31. {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/METADATA +1 -1
  32. {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/RECORD +66 -55
  33. simple_module_cli/templates/module/tests/__init__.py +0 -0
  34. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -17
  35. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/app.tsx +0 -16
  36. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/styles.css +0 -7
  37. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/__init__.py +0 -0
  38. {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/.env.example +0 -0
  39. {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/.gitignore +0 -0
  40. {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/Makefile +0 -0
  41. {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
  42. {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
  43. {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
  44. {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
  45. {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/alembic.ini +0 -0
  46. {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
  47. {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
  48. {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
  49. {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/host/main.py +0 -0
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {simple_module_cli-0.0.2.data → simple_module_cli-0.0.4.data}/data/simple_module_cli/templates/module/.gitignore +0 -0
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/WHEEL +0 -0
  70. {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/entry_points.txt +0 -0
  71. {simple_module_cli-0.0.2.dist-info → simple_module_cli-0.0.4.dist-info}/licenses/LICENSE +0 -0
@@ -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
- _FRAMEWORK_VERSION = "0.0.1"
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
- s = re.sub(r"(?<!^)(?=[A-Z])", "_", name)
18
- s = re.sub(r"[\s\-]+", "_", s)
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
 
@@ -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", "products"),
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,Products).",
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