Metadata-Version: 2.4
Name: sum-cli
Version: 3.3.4
Summary: SUM Platform CLI: single control plane for site lifecycle management
Author: Mark Ashton
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/markashton480/sum-platform
Project-URL: Repository, https://github.com/markashton480/sum-platform
Project-URL: Issues, https://github.com/markashton480/sum-platform/issues
Project-URL: Documentation, https://github.com/markashton480/sum-platform/tree/main/docs/dev/cli.md
Keywords: sum,cli,django,wagtail
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.1.7
Requires-Dist: packaging>=21.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: gitea
Requires-Dist: httpx>=0.27.0; extra == "gitea"
Dynamic: license-file

# SUM CLI (v3.3.4)

[![PyPI](https://img.shields.io/pypi/v/sum-cli.svg)](https://pypi.org/project/sum-cli/)

The SUM CLI is the single control plane for deploying and managing SUM Platform client sites on staging and production servers.

## Install

```bash
pip install sum-cli
sum-platform --version
```

### With Gitea Support

If using Gitea instead of GitHub for repository hosting:

```bash
pip install sum-cli[gitea]
```

## Initial Setup

Before using the CLI, configure your infrastructure settings:

```bash
sudo sum-platform setup
```

This interactive command creates `/etc/sum/config.yml` with your staging/production server settings.

## Commands

| Command | Description | Requires Sudo |
|---------|-------------|---------------|
| `setup` | Configure infrastructure settings | Yes |
| `init` | Create new site at `/srv/sum/<name>/` | Yes |
| `update` | Pull updates, migrate, restart | No (staging) |
| `backup` | PostgreSQL backup via pgBackRest | No |
| `restore` | Point-in-time database restore | No |
| `destroy` | Tear down a site and all its infrastructure | Yes |
| `monitor` | Check backup health, send alerts | No |
| `promote` | Deploy staging site to production | No |
| `check` | Validate project setup | No |
| `theme` | Theme management (list, check, update) | No |
| `themes` | List themes (deprecated, use `theme list`) | No |

## Pre-flight Checks

Every state-mutating command (`init`, `promote`, `backup`, `restore`, `update`, `destroy`) runs pre-flight checks before executing. These read-only checks validate that prerequisites are met — missing config files, unreachable servers, and missing tokens are caught before any work begins.

### Example Output

```
Pre-flight checks:
────────────────────────────────────────────────────────────
  ✅ Site directory: /srv/sum/acme exists
  ✅ PostgreSQL cluster: Cluster 'acme' is online
  ✅ SSH connectivity: SSH to 10.0.0.1:22 succeeded
  ✅ Disk space: 15.2 GB free on /srv/sum
  ❌ Gitea token: GITEA_TOKEN not set in environment
      ↳ Fix: Export GITEA_TOKEN or add it to /etc/sum/env
────────────────────────────────────────────────────────────
```

If any required check fails, the command aborts with actionable fix hints. Recommended checks (e.g., `destroy`'s site existence check) warn but don't block.

### Skipping Pre-flight

For automation or CI, bypass checks with the global flag or environment variable:

```bash
sum-platform --skip-preflight backup acme
SUM_SKIP_PREFLIGHT=1 sum-platform backup acme
```

### Checks Per Command

| Command | Checks |
|---------|--------|
| `init` | SystemConfig, DiskSpace, GiteaToken (if Gitea) |
| `promote` | SiteExists, PostgresCluster, SSHConnectivity, DiskSpace, GiteaToken + GiteaRepo (if Gitea), CipherPassFile + PgBackRestStanza (if backups) |
| `backup` | SiteExists, PostgresCluster, CipherPassFile, PgBackRestStanza (if backups configured) |
| `restore` | SiteExists, PostgresCluster, CipherPassFile, PgBackRestStanza (if backups configured) |
| `update` | SiteExists, SSHConnectivity (if `--target prod`) |
| `destroy` | SiteExists (warning only) |

## Theme Management

The `theme` command group provides tools for managing themes in existing projects.

### Listing Themes

```bash
# List local themes (from themes/ directory or SUM_THEME_PATH)
sum-platform theme list

# List remote themes from sum-themes repository
sum-platform theme list --remote
```

### Checking for Updates

```bash
# Compare current theme version against latest available
cd /path/to/project
sum-platform theme check
```

### Viewing Available Versions

```bash
# Show all available versions for a theme
sum-platform theme versions theme_a
```

### Updating Themes

```bash
# Update to latest version
cd /path/to/project
sum-platform theme update

# Update to specific version
sum-platform theme update --version 1.2.0

# Allow downgrade to older version
sum-platform theme update --version 1.0.0 --allow-downgrade

# Force reinstall even if at target version
sum-platform theme update --force
```

Theme updates are atomic with automatic rollback on failure. The lockfile at `.sum/theme.json` tracks version history.

## Site Directory Structure

Each site lives at `/srv/sum/<slug>/`:

```
/srv/sum/<slug>/
├── app/          # Django project (git checkout)
├── venv/         # Python virtualenv
├── static/       # collectstatic output
├── media/        # User uploads
└── backups/      # Database backups
```

### Ownership

The CLI runs as root (`sudo sum-platform init`). After setup, `fix_site_ownership()` sets the final permissions:

| Directory | Owner | Group | Why |
|-----------|-------|-------|-----|
| `app/` | deploy | www-data | Gunicorn reads code; deploy user runs git pulls |
| `static/` | deploy | www-data | collectstatic writes here; Caddy serves directly |
| `media/` | deploy | www-data | Django writes uploads here; Caddy serves directly |
| `venv/` | root | root | Security — prevents application code from modifying its own runtime |
| `backups/` | root | root | Security — prevents application code from accessing/tampering with backups |

The `deploy` user runs gunicorn, owns application files, and is a member of `www-data`. It has no sudo access. Configured in `/etc/sum/config.yml` under `defaults.deploy_user`.

Operations on site files (`app/`, `static/`, `media/`) should run as the deploy user:

```bash
sudo -u deploy /srv/sum/mysite/venv/bin/python /srv/sum/mysite/app/manage.py migrate
sudo -u deploy /srv/sum/mysite/venv/bin/python /srv/sum/mysite/app/manage.py collectstatic
```

Operations on `venv/` and `backups/` require root.

## Creating Sites

### With GitHub

```bash
sudo sum-platform init acme --git-provider github --git-org acme-corp
```

### With Gitea

```bash
sudo sum-platform init acme --git-provider gitea --git-org clients \
  --gitea-url https://gitea.agency.com
```

### With Gitea (Custom SSH Port)

```bash
sudo sum-platform init acme --git-provider gitea --git-org clients \
  --gitea-url https://gitea.agency.com --gitea-ssh-port 2222
```

### Without Git

```bash
sudo sum-platform init acme --no-git
```

### Additional Init Options

```bash
sudo sum-platform init acme --git-provider github --git-org acme-corp \
  --theme theme_a \
  --profile sage-stone \
  --content-path /path/to/custom/content

# Skip systemd service installation
sudo sum-platform init acme --no-git --skip-systemd

# Skip Caddy reverse proxy configuration
sudo sum-platform init acme --no-git --skip-caddy

# Set custom Django superuser username (default: admin)
sudo sum-platform init acme --no-git --superuser myuser
```

### Dev / Testing (Unreleased sum-core)

By default, `init` installs sum-core from a pinned git tag in the boilerplate `requirements.txt`. To test with unreleased changes, use one of these overrides (listed in priority order):

**`SUM_CORE_SOURCE` environment variable** — raw pip requirement line, always takes priority:

```bash
SUM_CORE_SOURCE="-e /path/to/local/core" sudo sum-platform init acme --no-git
SUM_CORE_SOURCE="sum-core==0.8.0" sudo sum-platform init acme --no-git
```

**`--dev` flag** — editable install from the monorepo using an absolute path (must run from within the monorepo):

```bash
cd /path/to/sum-platform
sudo sum-platform init acme --dev --no-git
```

**`--core-ref` flag** — install from a specific git branch or tag:

```bash
sudo sum-platform init acme --core-ref develop --no-git
sudo sum-platform init acme --core-ref feature/my-branch --no-git
```

`--dev` and `--core-ref` are mutually exclusive. `SUM_CORE_SOURCE` silently overrides both.

## Destroying Sites

> **WARNING:** The `destroy` command is irreversible. It permanently removes a site and all its infrastructure.

`destroy` reverses everything created by `init`, tearing down resources in safe order:

1. Stop and disable the systemd service
2. Remove the systemd service file and reload daemon
3. Drop the PostgreSQL cluster (and deallocate port)
4. Remove pgBackRest stanza config and backup cron
5. Remove the Caddy site config and reload
6. Remove the site directory (`/srv/sum/<slug>/`)

### Usage

```bash
# Interactive — prompts you to type the site slug to confirm
sudo sum-platform destroy acme

# Skip confirmation prompts (for automation/scripts)
sudo sum-platform destroy acme --force

# Also permanently delete remote backup data
sudo sum-platform destroy acme --purge-backups
```

### Options

| Option | Description |
|--------|-------------|
| `--force` | Skip all confirmation prompts |
| `--purge-backups` | Also delete remote backup data via pgBackRest stanza-delete. **IRREVERSIBLE.** |

### Safety Features

- **Slug confirmation:** You must type the exact site slug to proceed. A mismatch aborts the operation.
- **Double confirmation for backups:** When `--purge-backups` is used, you must also type `DELETE BACKUPS` to confirm.
- **Path safety check:** The command refuses to remove directory paths that don't contain the site slug or are too shallow (prevents accidental deletion of system directories).
- **Non-fatal warnings:** If individual teardown steps fail (e.g., service already stopped), the command continues and reports warnings rather than aborting.
- **Requires root:** The command escalates to root privileges automatically.

### Recommended: Back Up Before Destroying

```bash
# Create a full backup first
sum-platform backup acme --type=full

# Then destroy (without --purge-backups to keep remote backups)
sudo sum-platform destroy acme
```

## Validating Sites

The `check` command validates that a project is correctly set up and ready to run.

### Usage

```bash
# Run from inside a project directory
cd /srv/sum/acme/app && sum-platform check

# Or specify a project name (resolves under clients/ directory)
sum-platform check acme
```

### Checks Performed

| Check | What It Validates |
|-------|-------------------|
| Virtualenv | Virtual environment exists with required packages (django, wagtail) |
| Credentials | `.env.local` exists (created during init with superuser credentials) |
| Database | All migrations applied (`manage.py migrate --check`) |
| Homepage | Homepage is set as the default site root page |
| Theme compiled CSS | Compiled CSS exists at `theme/active/static/<slug>/css/main.css` and is >5KB |
| Theme slug match | Theme slug in `.sum/theme.json` matches `theme/active/theme.json` |
| Required env vars | All keys from `.env.example` are set in `.env` or environment |
| sum_core import | `sum_core` package is importable in the project's Python environment |

### Example Output

```
[OK] Virtualenv: Virtualenv exists with required packages
[OK] Credentials: .env.local found
[OK] Database: Migrations up to date
[OK] Homepage: Homepage set as site root
[OK] Theme compiled CSS: /srv/sum/acme/app/theme/active/static/theme_a/css/main.css
[OK] Theme slug match: theme_a
[OK] Required env vars: Required env vars present
[OK] sum_core import: sum_core importable

✅ All checks passed
```

Failed checks show remediation hints:

```
[FAIL] Virtualenv: Package 'django' not installed
       → Run 'pip install -r requirements.txt'
[FAIL] Database: Pending migrations
       → Run 'python manage.py migrate'
```

The command exits with code `0` on success or `1` if any check fails.

## Managing Sites

```bash
# Update a deployed site (pull, migrate, restart)
sum-platform update acme

# Update production site
sum-platform update acme --target prod

# Update without running migrations
sum-platform update acme --skip-migrations

# Backup database (PostgreSQL via pgBackRest)
sum-platform backup acme --type full

# Validate project setup
sum-platform check acme

# List available themes
sum-platform theme list

# List remote themes with latest versions
sum-platform theme list --remote

# Check for theme updates in a project
cd /srv/sum/acme && sum-platform theme check

# Update theme to latest version
cd /srv/sum/acme && sum-platform theme update
```

For production updates, the remote app-tree mutation steps run as the configured
deploy user, not the SSH login user. This keeps updates working when production
SSH uses a service account but the deployed checkout is owned by the stable
deploy identity.

### Promoting to Production

The `promote` command deploys a working staging site to a production server with a custom domain. It requires a git-enabled site (created without `--no-git`).

```bash
sum-platform promote acme --domain acme-client.com
```

**Options:**

| Option | Required | Description |
|--------|----------|-------------|
| `--domain` | Yes | Production domain for the site (e.g., `acme-client.com`) |

**What promote does (11 steps):**

1. Backs up the staging database
2. Copies the backup to the production server via `scp`
3. Provisions production infrastructure (database, user, directories, `.env`)
4. Clones the site repository on production (HTTPS with token, falls back to SSH)
5. Creates a virtualenv and installs dependencies
6. Restores the database from backup
7. Syncs media files from staging via `rsync`
8. Runs Django migrations and `collectstatic`
9. Installs and enables systemd service
10. Configures Caddy reverse proxy
11. Starts the service and verifies health

After completion, the command prints the production URL and reminds you to configure DNS.

**Prerequisites:**
- The staging site must exist and have a git repository
- SSH access to the production server must be configured in `/etc/sum/config.yml`
- The `production.ssh_host` setting must be set in system config

## Backup and Recovery

```bash
# Create differential backup
sum-platform backup acme

# Create full backup
sum-platform backup acme --type=full

# List available backups
sum-platform backup acme --list

# List restore points with details
sum-platform restore acme --list

# Restore to specific point in time
sum-platform restore acme --time "2024-01-15 14:30:00"

# Restore to latest backup
sum-platform restore acme --latest

# Clean up pre-restore data after verifying restore
sum-platform restore acme --latest --cleanup

# Skip confirmation prompt (useful for automation)
sum-platform restore acme --latest --confirm
```

## Backup Monitoring

The `monitor` command checks backup freshness and sends email alerts for stale backups.

```bash
# Check all sites, send alerts for problems
sum-platform monitor

# Check without sending alerts
sum-platform monitor --no-alerts

# Verbose output (show all sites)
sum-platform monitor -v
```

### Cron Setup

Add to crontab for hourly monitoring:

```cron
0 * * * * /usr/local/bin/sum-platform monitor
```

Alerts are sent when:
- Backup is older than 48 hours
- Backup status file is missing

## Configuration

### Global Config (`/etc/sum/config.yml`)

Infrastructure settings only. Created via `sum-platform setup`.

```yaml
agency:
  name: Your Agency Name

staging:
  server: staging.example.com
  domain_pattern: "{slug}.staging.example.com"
  base_dir: /srv/sum

production:
  server: prod.example.com
  ssh_host: 10.0.0.1
  base_dir: /srv/sum

templates:
  dir: /opt/your-ops/infra
  systemd: systemd/sum-site-gunicorn.service.template
  caddy: caddy/Caddyfile.template

defaults:
  theme: theme_a
  seed_profile: starter
  deploy_user: deploy
  postgres_port: 5432

backups:
  storage_box:
    host: u123456.your-storagebox.de
    user: u123456
    fingerprint: SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    port: 23
    ssh_key: /etc/sum/backup-key
    base_path: /backups
  retention:
    full_backups: 2
    diff_backups: 7
  alerts:
    email: alerts@agency.com
```

### Site Config (`/srv/sum/<site>/.sum/config.yml`)

Per-site configuration. Auto-created when you run `init`.

**GitHub site:**
```yaml
site:
  slug: acme
  theme: theme_a
  created: 2026-02-03T14:30:00Z

git:
  provider: github
  org: acme-corp
```

**Gitea site:**
```yaml
site:
  slug: acme
  theme: theme_a
  created: 2026-02-03T14:30:00Z

git:
  provider: gitea
  org: clients
  url: https://gitea.agency.com
  ssh_port: 2222
  token_env: GITEA_TOKEN
```

**No-git site:**
```yaml
site:
  slug: acme
  theme: theme_a
  created: 2026-02-03T14:30:00Z

git: null
```

## Git Provider Setup

### GitHub

Requires the GitHub CLI (`gh`) to be installed and authenticated:

```bash
gh auth login
```

### Gitea

Set the API token environment variable:

```bash
export GITEA_TOKEN=your-token-here
```

The CLI also detects tokens from the tea CLI config at `~/.config/tea/config.yml` as a fallback. If the token is found in the tea config, it is automatically injected into the environment for privilege escalation.

**Important:** When using `--git-provider gitea`, the `init` command warns if no token is detected. The `promote` command hard-errors — Gitea access is required to clone the repository on production.

Or use a custom environment variable name:

```bash
sudo sum-platform init acme --git-provider gitea --git-org clients \
  --gitea-url https://gitea.agency.com --gitea-token-env MY_GITEA_TOKEN
```

## Common Workflows

### Set Up a New Client Site

```bash
# 1. Create the site with git integration
sudo sum-platform init acme --git-provider github --git-org acme-corp

# 2. Validate the setup
sum-platform check acme

# 3. Visit the staging URL shown in init output
```

### Update sum_core on a Deployed Site

```bash
# 1. Create a backup before updating
sum-platform backup acme --type=full

# 2. Pull latest code and apply updates
sum-platform update acme

# 3. Verify the site is working
sum-platform check acme
```

### Recover from Backup

```bash
# 1. List available restore points
sum-platform restore acme --list

# 2. Restore to the latest backup
sum-platform restore acme --latest

# 3. Verify the restored site
sum-platform check acme

# 4. Clean up pre-restore data once verified
sum-platform restore acme --latest --cleanup
```

### Promote a Staging Site to Production

```bash
# 1. Verify the staging site is ready
sum-platform check acme

# 2. Create a full backup before promoting
sum-platform backup acme --type=full

# 3. Promote to production with a custom domain
sum-platform promote acme --domain acme-client.com

# 4. Configure DNS to point to the production server
```

### Destroy a Site Safely

```bash
# 1. Create a final backup (in case you need it later)
sum-platform backup acme --type=full

# 2. Destroy the site (keeps remote backups by default)
sudo sum-platform destroy acme

# 3. To also purge remote backup data (irreversible)
sudo sum-platform destroy acme --purge-backups
```

## Troubleshooting

### "Database configuration required" error

The `.env` file is missing or incomplete.

```bash
cp .env.example .env
# Edit .env with your database credentials
make db-up && make dev-reset
```

### `check` reports "sum_core import" failure

The `sum_core` package is not installed in the site's virtualenv.

```bash
cd /srv/sum/acme
source venv/bin/activate
pip install sum-core
```

### `destroy` warns "Could not stop service"

The systemd service may already be stopped or never created (e.g., if `--skip-systemd` was used during init). This warning is non-fatal — the command continues removing other resources.

### `promote` fails with "Cannot promote without git repository"

The staging site was created with `--no-git`. Promote requires a git repository to clone on the production server. Re-create the site with a git provider.

### `init` fails with permission errors

The `init` command requires root. Run with `sudo`:

```bash
sudo sum-platform init acme --no-git
```

### Backup/restore fails with pgBackRest errors

Ensure pgBackRest is installed and the stanza is initialized:

```bash
# Check pgBackRest info for the site
sudo -u postgres pgbackrest --stanza=acme info
```

If the stanza doesn't exist, it was likely not created during init. Re-running `init` is the safest fix.

## Development Install (monorepo)

```bash
pip install -e ./cli
```
