simple-module-cli 0.0.3__tar.gz → 0.0.5__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.3 → simple_module_cli-0.0.5}/PKG-INFO +1 -1
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/pyproject.toml +8 -1
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/app_project.py +19 -1
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/case.py +11 -3
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/catalog.py +1 -8
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/cli.py +6 -1
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/new.py +8 -0
- simple_module_cli-0.0.5/simple_module_cli/package_update.py +287 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/README.md +58 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-cli/SKILL.md +170 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-conventions/SKILL.md +97 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-creating/SKILL.md +104 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-database/SKILL.md +98 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-doctor/SKILL.md +41 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +93 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-locales/SKILL.md +125 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-migrations/SKILL.md +103 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-registries/SKILL.md +144 -0
- simple_module_cli-0.0.5/simple_module_cli/skills/simple-module-testing/SKILL.md +102 -0
- simple_module_cli-0.0.5/simple_module_cli/skills_cmd.py +249 -0
- simple_module_cli-0.0.5/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +26 -0
- simple_module_cli-0.0.5/simple_module_cli/templates/host/client_app/app.tsx +37 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/client_app/package.json.tpl +4 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/client_app/pages.ts +3 -3
- simple_module_cli-0.0.5/simple_module_cli/templates/host/client_app/styles.css +12 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/client_app/vite.config.ts +22 -1
- simple_module_cli-0.0.5/tests/test_build_packaging.py +87 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_cli_catalog.py +3 -10
- simple_module_cli-0.0.5/tests/test_cli_package_update.py +173 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_cli_wizard.py +2 -3
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_scaffolding_host.py +10 -10
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_scaffolding_module.py +5 -1
- simple_module_cli-0.0.5/tests/test_skills_cmd.py +254 -0
- simple_module_cli-0.0.3/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -17
- simple_module_cli-0.0.3/simple_module_cli/templates/host/client_app/app.tsx +0 -16
- simple_module_cli-0.0.3/simple_module_cli/templates/host/client_app/styles.css +0 -7
- simple_module_cli-0.0.3/simple_module_cli/templates/module/tests/__init__.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/.gitignore +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/LICENSE +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/README.md +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_cli_new.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.3 → simple_module_cli-0.0.5}/tests/test_plugin_discovery.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Standalone scaffolder for the SimpleModule framework — `sm new`, `sm 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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.5"
|
|
4
4
|
description = "Standalone scaffolder for the SimpleModule framework — `sm new`, `sm create-module`, plugin host."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -38,6 +38,13 @@ requires = ["hatchling"]
|
|
|
38
38
|
build-backend = "hatchling.build"
|
|
39
39
|
|
|
40
40
|
[tool.hatch.build.targets.wheel]
|
|
41
|
+
# The canonical ``skills/`` lives at the repo root (so `npx skills add` from
|
|
42
|
+
# vercel-labs/skills picks it up) and is symlinked into
|
|
43
|
+
# ``simple_module_cli/skills`` for editable installs and packaging.
|
|
44
|
+
# Hatchling follows that symlink during sdist + wheel builds, so the real
|
|
45
|
+
# files land inside the package — no force-include needed (and force-include
|
|
46
|
+
# with ``../../skills`` breaks the standard sdist→wheel rebuild because the
|
|
47
|
+
# unpacked sdist has no parent ``skills/``).
|
|
41
48
|
packages = ["simple_module_cli"]
|
|
42
49
|
|
|
43
50
|
[tool.hatch.build.targets.wheel.shared-data]
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -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.
|