Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# simple_module_python skills

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).

## Install

Pick your scope and run one command:

```bash
# All skills in this repo, into the current project
npx skills add antosubash/simple_module_python

# Globally (available in every project on your machine)
npx skills add antosubash/simple_module_python -g

# Just one skill, into a specific agent
npx skills add antosubash/simple_module_python --skill simple-module-creating -a claude-code

# List what's available without installing
npx skills add antosubash/simple_module_python --list
```

The CLI is [vercel-labs/skills](https://github.com/vercel-labs/skills); see its README for symlink-vs-copy and other options.

## What's here

| Skill | Use when |
|---|---|
| [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. |
| [simple-module-creating](./simple-module-creating/SKILL.md) | Adding a new feature package — scaffolding, entry-point, `ModuleMeta` |
| [simple-module-conventions](./simple-module-conventions/SKILL.md) | Writing or reviewing module code — the invariant list (SQLModel everywhere, settings layout, framework→plugin direction, etc.) |
| [simple-module-database](./simple-module-database/SKILL.md) | Adding SQLModel tables, picking a mixin, or debugging session/transaction behavior |
| [simple-module-migrations](./simple-module-migrations/SKILL.md) | Generating, applying, or reviewing Alembic migrations after installing or changing a module |
| [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 |
| [simple-module-doctor](./simple-module-doctor/SKILL.md) | Interpreting a diagnostic code (`SM001`–`SM018`) printed at boot |

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.

## Contributing

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.

PRs welcome.
150 changes: 150 additions & 0 deletions skills/simple-module-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
name: simple-module-cli
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, or creating an admin user. Triggers on "sm new", "sm create-host", "sm create-module", "sm host gen-pages", "sm users create-admin", or any unfamiliar `sm` subcommand.
---

# simple_module_python: the `sm` CLI

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, and admin shortcuts for the bundled modules.

## Top-level commands

| Command | When to use |
|---|---|
| `sm new <name>` | Greenfield: scaffold a complete app (host + selected modules) in one shot, with an interactive wizard for DB / tenancy / module preset |
| `sm create-host <name>` | You want just a bare host project; you'll add modules later by `pip install`-ing them |
| `sm create-module <name>` | You're authoring a publishable module package (separate repo, distributed via PyPI) |
| `sm host …` | Project-time helpers run from inside a host directory (page manifest, JS dep sync) |
| `sm settings …` | Settings-module admin — currently `import-from-env` |
| `sm users …` | Users-module admin — currently `create-admin` |

## `sm new <name>` — the wizard

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.

```bash
# Interactive (asks for DB, tenancy, preset, module list)
sm new MyApp

# Non-interactive: take all defaults (sqlite, no tenancy, standard preset)
sm new MyApp --yes

# Pick a preset and add extras
sm new MyApp --preset full --tenancy --db postgres
sm new MyApp --preset minimal --with background_tasks,file_storage --yes

# Scaffold only — skip uv sync / npm install / alembic upgrade head
sm new MyApp --no-install
```

**Module presets:**

| Preset | Modules included |
|---|---|
| `minimal` | `users` (and `auth` as a dep) |
| `standard` (default) | `users`, `dashboard`, `permissions` (+ deps) |
| `full` | every module in the catalog |
| `custom` | interactive — pick each module yes/no |

`--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.

**Options summary:**

| Flag | Default | Meaning |
|---|---|---|
| `--dest <PATH>` | `./<name>` | Where to write the project |
| `--db sqlite\|postgres` | `sqlite` | Backend configured in `.env.example` |
| `--tenancy / --no-tenancy` | `--no-tenancy` | Enable the multi-tenant middleware |
| `--preset minimal\|standard\|full` | wizard asks | Module bundle |
| `--with <names>` | none | Extra catalog keys beyond the preset |
| `--yes / -y` | off | Skip prompts; accept defaults |
| `--no-install` | off | Skip `uv sync` / `npm install` / `alembic upgrade head` |

## `sm create-host <name>` — bare host

```bash
sm create-host MyApp # empty host, no modules declared
sm create-host MyApp --with Auth,Products # declare module deps in pyproject.toml
sm create-host MyApp --dest ./apps/myapp # custom destination
```

`--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.

## `sm create-module <name>` — module package

For module authors publishing to PyPI. Scaffolds a standalone repo containing one module.

```bash
sm create-module orders # writes ./simple_module_orders/
sm create-module orders --dest ./packages/orders
```

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.

`<name>` accepts any case — `orders`, `Orders`, `ORDERS`, `blog_posts` all work. The CLI lowercases it for the directory and PascalCases it for `ModuleMeta.name`.

For the post-scaffold steps (entry point, Inertia namespace, etc.) see **simple-module-creating**.

## `sm host gen-pages` — regenerate the Inertia manifest

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`.

```bash
sm host gen-pages # uses ./client_app
sm host gen-pages --host-dir=apps/web/client_app
```

`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.

## `sm host sync-js-deps` — install module JS deps

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.

```bash
sm host sync-js-deps # uses ./client_app
sm host sync-js-deps --host-client-app=apps/web/client_app
```

Run after `pip install <module>` if the new module ships frontend code.

## `sm settings import-from-env`

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.

```bash
sm settings import-from-env
```

## `sm users create-admin`

Bootstraps the first admin user, or rotates an existing admin's password.

```bash
sm users create-admin -e admin@example.com -p hunter2
sm users create-admin -e admin@example.com -p new-password --force # rotate
sm users create-admin -e admin@example.com -p hunter2 --full-name "Admin"
```

| Flag | Meaning |
|---|---|
| `-e, --email` (required) | Admin email |
| `-p, --password` (required) | Initial password (or new password with `--force`) |
| `--full-name` | Display name |
| `--force` | Update the password if the admin already exists; without it, the command exits if the email is taken |

Don't bake `--password` literals into a script you commit; use a secrets store and pass via shell expansion.

## Pitfalls

- **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.
- **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.
- **Ran `sm host gen-pages` from outside the host directory.** Defaults to `./client_app`; pass `--host-dir` from elsewhere.
- **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.
- **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**.
- **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).

## Related skills

- **simple-module-creating** — what `sm create-module` produces and the post-scaffold contract
- **simple-module-inertia-pages** — what `sm host gen-pages` regenerates and why
- **simple-module-migrations** — the `alembic upgrade head` step `sm new` runs
97 changes: 97 additions & 0 deletions skills/simple-module-conventions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: simple-module-conventions
description: Use when writing or reviewing code inside modules/ in a simple_module_python codebase, hitting type-check or lint failures in module code, or unsure which base class / settings prefix / file structure to use. Triggers on "add a model", "where do settings go", "why is doctor warning", or any edit under modules/.
---

# simple_module_python conventions

The framework relies on these invariants. Every rule here exists because something concrete breaks without it.

## The hard rules

### 1. SQLModel everywhere — both tables and DTOs

```python
from sqlmodel import Field, SQLModel
from simple_module_db.base import create_module_base

Base = create_module_base("orders")

class Order(Base, AuditMixin, table=True): # table
__tablename__ = "orders_order"
id: int | None = Field(default=None, primary_key=True)
name: str = Field(max_length=200)

class OrderOut(SQLModel): # DTO — plain SQLModel, no table=True
model_config = ConfigDict(from_attributes=True)
id: int
name: str
```

Forbidden in module code: `pydantic.BaseModel` for DTOs, and SQLAlchemy `DeclarativeBase` + `Mapped[...]` / `mapped_column(...)` for tables. Autogenerate, fixtures, Inertia serialization, and the shared mixins all assume the SQLModel metaclass.

### 2. The file-size cap

Treat 300 lines as the design pressure on every `.py`, `.ts`, and `.tsx` file (the reference codebase enforces this in CI). When you approach it, **split by responsibility** — don't golf comments and blank lines to squeeze under.

### 3. Per-module settings: `SM_<MODULE>_*` on `app.state.<module>`

```python
class UsersModule(ModuleBase):
def register_settings(self, app: FastAPI) -> None:
from users.settings import UsersSettings # reads SM_USERS_* env
from users.state import UsersState
app.state.users = UsersState(settings=UsersSettings())
```

- Env-var prefix is **always** `SM_<MODULE_UPPER>_*`. Cross-module reads are discouraged by convention (no enforcement) — go through the registry or event bus instead.
- The state attribute key is the lowercase package name or the lowercased `meta.name` — the diagnostics check accepts either. For module package `auth` with `meta.name="Auth"`, `app.state.auth` works either way.
- If you override `register_settings` and don't add anything to `app.state.<module>`, `SM012` fires at dev boot.

### 4. Framework must not import from plugins

`framework/*` packages are not allowed to `import` from anything in `modules/*`. The doctor raises **SM009 ERROR** if it finds one.

Plugin → framework imports are fine; framework → plugin is the forbidden direction. If framework code seems to need a module's symbol, the symbol belongs in `simple_module_core` or `simple_module_hosting`.

### 5. Construct translated Zod schemas inside hooks, never at module scope

```tsx
// ❌ wrong — freezes against the locale active at first render, forever
const schema = z.object({
name: z.string().min(1, t('products.validation.name_required')),
});

// ✅ right
export function useProductSchema() {
const { t } = useT();
return z.object({
name: z.string().min(1, t('products.validation.name_required')),
});
}
```

**Why:** `t()` resolves once when the schema is constructed. Module-scope construction = first-render-locale-only forever, no matter what the user switches to.

### 6. CSRF: rely on `SameSite=Lax`, don't add a token header

There is no CSRF token middleware. Starlette's default `SameSite=Lax` session cookie isn't attached to cross-site POST/PUT/DELETE, so forged form-submits are unauthenticated. Page-side `fetch()` calls don't need a token header.

### 7. Don't call `session.commit()` in service code

The per-request `get_db` dependency auto-commits when there are pending writes. `flush()` only when you need DB-assigned values mid-request. Manual `commit()` double-commits on success and breaks the no-writes-then-rollback optimization.

### 8. Locales: `<package>/locales/<lang>.json`, namespace = lowercase module name

Keys are flattened: `{"browse": {"title": "X"}}` under namespace `orders` becomes `orders.browse.title` at runtime. Pluralize with CLDR suffixes: `_zero / _one / _two / _few / _many / _other`. Only `_other` is required.

Locale-related diagnostics: SM013 (missing file), SM014 (missing keys vs default), SM015 (extra keys), SM016 (invalid JSON / non-string leaves).

### 9. Don't re-enable the silenced ty rules

Projects using the `ty` type checker should globally suppress `unresolved-attribute`, `unsupported-operator`, `unknown-argument`, `no-matching-overload`, and `invalid-argument-type` in `pyproject.toml`. SQLModel runtime-instruments fields as SQLAlchemy attributes, so the type checker can't see what's there. Real bugs surface in tests.

## Related skills

- **simple-module-database** — per-module Base, mixins, session lifecycle
- **simple-module-doctor** — what each `SMnnn` diagnostic code means and how to fix it
Loading
Loading