# CLI Module Design ## Problem `pyproject.toml` declares `porchlight = "porchlight.cli:main"` with `typer>=0.15` as a dependency, but no `cli.py` exists. The app has no CLI entry point for administrative tasks -- operators must use the Python shell or raw SQL to create invite links or bootstrap initial users. ## Decision Create `src/porchlight/cli.py` with two commands: `create-invite` and `initial-admin`. Skip a `serve` command since Docker/uvicorn handles that already. ## Commands ### `porchlight create-invite ` Generate a magic link registration URL for a new user. Options: - `--ttl SECONDS` -- link expiration (default: from settings, 86400s) - `--note TEXT` -- optional note stored with the link Behavior: - Opens SQLite DB via existing `open_db()` context manager - Creates `MagicLinkService` with `SQLiteMagicLinkRepository` - Calls `service.create(username=username, note=note, created_by="cli")` - Prints full URL: `{issuer}/register/{token}` - Reads `OIDC_OP_ISSUER` env var for base URL (required) - Reads `OIDC_OP_SQLITE_PATH` for DB path (default: `data/oidc_op.db`) ### `porchlight initial-admin ` Bootstrap the first admin user with a registration link. Options: - `--group TEXT` -- groups to assign (default: `["admin", "users"]`), repeatable Behavior: - Opens SQLite DB - Checks if username already exists -- error if so - Generates unique userid via `generate_unique_userid()` - Creates user with specified groups - Creates a magic link so the admin can visit the URL to set up credentials - Prints full URL: `{issuer}/register/{token}` ## Route change: `/register/{token}` Modify `register_magic_link` to handle existing users: - Before creating the user, call `user_repo.get_by_username(link.username)` - If user exists: skip creation, log them in, redirect to `/manage/credentials?setup=1` - If user doesn't exist: create as before with `groups=["users"]` This makes the registration route work for both fresh invites and admin-created users who need to set up credentials. ## Implementation notes - Both commands use `asyncio.run()` to call async helpers - Config loaded via `Settings()` (pydantic-settings reads env vars) - No new dependencies required -- Typer is already declared - No schema changes needed ## Files changed | File | Change | |------|--------| | `src/porchlight/cli.py` | New -- Typer app with two commands | | `src/porchlight/authn/routes.py` | Modify `register_magic_link` for existing users | | `tests/test_cli.py` | New -- tests for both CLI commands | | `tests/test_auth_routes/test_register_magic_link.py` | Add test for existing user registration |