diff --git a/.claude/skills/dev-setup/SKILL.md b/.claude/skills/dev-setup/SKILL.md new file mode 100644 index 0000000..68e8107 --- /dev/null +++ b/.claude/skills/dev-setup/SKILL.md @@ -0,0 +1,164 @@ +--- +name: dev-setup +description: Development environment setup, scripts, configuration, and deployment for SquishMark +--- + +# Development Environment + +## Local Setup + +```bash +# Clone the repo +git clone https://github.com/xeek-dev/squishmark.git +cd squishmark + +# Create virtual environment +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows + +# Install dependencies +pip install -e ".[dev]" + +# Run locally +uvicorn squishmark.main:app --reload +``` + +## Development Scripts + +Two user-facing scripts in `scripts/` and two agent-facing scripts in skill directories: + +### Environment Check + +Run the readiness check before starting work: + +```bash +python .claude/skills/dev-setup/scripts/check-env.py +``` + +### User-Facing Scripts (`scripts/`) + +### `start-dev.py` — Dev server with multi-server management + +Starts a uvicorn dev server pointing at the local `content/` directory. Supports running multiple named server instances simultaneously (tracked in `.dev-servers.json`). The default instance name is the current git branch. + +```bash +# Basic usage +python scripts/start-dev.py # foreground on :8000 +python scripts/start-dev.py -b # background on :8000 +python scripts/start-dev.py -b --port 8001 # background on :8001 +python scripts/start-dev.py --name api -b # named background instance + +# Server management +python scripts/start-dev.py --list # show all tracked servers +python scripts/start-dev.py --stop # stop current branch's server +python scripts/start-dev.py --stop api # stop named server +python scripts/start-dev.py --stop 12345 # stop server by PID +python scripts/start-dev.py --stop-all # stop all servers +python scripts/start-dev.py --restart -b # restart in background + +# Other options +python scripts/start-dev.py --host 0.0.0.0 # bind to all interfaces +python scripts/start-dev.py --no-reload # disable auto-reload +``` + +> **Stale port gotcha:** Background start (`-b`) can silently fail if a stale process holds the port. The old process keeps serving while the new one exits. Always verify after restart: `--stop ` then `lsof -ti: | xargs kill` before starting fresh. + +### `run-checks.py` — Local CI checks + +Runs the same checks as CI: **ruff format**, **ruff check**, **pytest**, and **pyright**. By default runs all checks and reports a summary. + +```bash +python scripts/run-checks.py # run all checks +python scripts/run-checks.py --fail-fast # stop on first failure +python scripts/run-checks.py --docker # also run docker build (slow) +``` + +### Agent-Facing Scripts (in skill directories) + +#### `setup-worktree.py` — Git worktree management (in `git` skill) + +Creates isolated worktrees in `.worktrees/` for parallel development. + +```bash +python .claude/skills/git/scripts/setup-worktree.py feat/42-dark-mode # create worktree + branch +python .claude/skills/git/scripts/setup-worktree.py feat/42-dark-mode --install # also pip install -e +python .claude/skills/git/scripts/setup-worktree.py feat/42-dark-mode --with-content # also copy content/ +python .claude/skills/git/scripts/setup-worktree.py feat/42-dark-mode --integration # --install + --with-content +python .claude/skills/git/scripts/setup-worktree.py --list # list active worktrees +python .claude/skills/git/scripts/setup-worktree.py --cleanup 42-dark-mode # remove worktree + branch +``` + +#### `github-issue-updater.py` — Issue metadata updater (in `github` skill) + +Sets issue type (task/bug/feature), adds labels, and assigns milestones in a single command. + +```bash +python .claude/skills/github/scripts/github-issue-updater.py 42 --type task +python .claude/skills/github/scripts/github-issue-updater.py 42 --add-label "engine,themes" +python .claude/skills/github/scripts/github-issue-updater.py 42 --milestone "SquishMark 1.0" +``` + +## Configuration + +### Environment Variables + +```bash +# Required +GITHUB_CONTENT_REPO=xeek-dev/xeek-dev-content +GITHUB_TOKEN=ghp_... # Only for private repos + +# Optional +CACHE_TTL_SECONDS=300 +DATABASE_URL=sqlite:///data/squishmark.db + +# GitHub OAuth (for admin features) +GITHUB_CLIENT_ID=... +GITHUB_CLIENT_SECRET=... +``` + +### config.yml (in content repo) + +```yaml +site: + title: "My Blog" + description: "A blog about things" + author: "Your Name" + url: "https://example.com" + favicon: "/static/user/custom-icon.png" # Optional: override auto-detected favicon + featured_max: 5 # Optional: max featured posts (default: 5) + +theme: + name: default + pygments_style: monokai + +posts: + per_page: 10 +``` + +## Deployment (Fly.io) + +```bash +# First time +fly launch + +# Create volume for SQLite +fly volumes create squishmark_data --size 1 + +# Set secrets +fly secrets set GITHUB_TOKEN=ghp_... +fly secrets set GITHUB_CLIENT_ID=... +fly secrets set GITHUB_CLIENT_SECRET=... + +# Deploy +fly deploy +``` + +## Common Tasks + +### Adding a new route +1. Create router in `src/squishmark/routers/` +2. Register in `main.py` +3. Add corresponding Jinja2 template if needed + +### Testing with a local content repo +Set `GITHUB_CONTENT_REPO` to a local path (prefixed with `file://`) for development without GitHub API calls. diff --git a/.claude/skills/dev-setup/scripts/check-env.py b/.claude/skills/dev-setup/scripts/check-env.py new file mode 100644 index 0000000..4b150e3 --- /dev/null +++ b/.claude/skills/dev-setup/scripts/check-env.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Check that the SquishMark development environment is ready.""" + +import os +import sys +from pathlib import Path + + +def check(label: str, condition: bool, fix: str) -> bool: + if condition: + print(f" OK {label}") + else: + print(f" FAIL {label}") + print(f" Fix: {fix}") + return condition + + +def main() -> None: + root = Path(__file__).resolve().parents[4] # .claude/skills/dev-setup/scripts/check-env.py -> repo root + os.chdir(root) + + print("Checking SquishMark dev environment...\n") + checks = [] + + # venv exists + venv = root / ".venv" + checks.append( + check( + "Virtual environment exists", + venv.is_dir(), + "python -m venv .venv", + ) + ) + + # venv is active + checks.append( + check( + "Virtual environment is active", + sys.prefix != sys.base_prefix, + "source .venv/bin/activate", + ) + ) + + # package is importable + try: + import squishmark # noqa: F401 + + importable = True + except ImportError: + importable = False + checks.append( + check( + "squishmark package installed", + importable, + 'pip install -e ".[dev]"', + ) + ) + + # content directory + checks.append( + check( + "content/ directory exists", + (root / "content").is_dir(), + "Create a content/ directory with posts/, pages/, and config.yml", + ) + ) + + # data directory + data_dir = root / "data" + checks.append( + check( + "data/ directory exists", + data_dir.is_dir(), + "mkdir data", + ) + ) + + print() + passed = sum(checks) + total = len(checks) + if passed == total: + print(f"All {total} checks passed.") + else: + print(f"{passed}/{total} checks passed.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/git/SKILL.md b/.claude/skills/git/SKILL.md new file mode 100644 index 0000000..47babe4 --- /dev/null +++ b/.claude/skills/git/SKILL.md @@ -0,0 +1,45 @@ +--- +name: git +description: Git conventions — conventional commits, branch naming, commit message format +--- + +# Git Conventions + +## Conventional Commits + +Commit messages and PR titles **must** use [Conventional Commits](https://www.conventionalcommits.org/) format: + +``` +type(scope): description +``` + +Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `ci` + +Scope is optional but recommended. Examples: +- `feat(terminal): add pixel art title renderer` +- `fix(docs): correct cache-busting instructions` +- `refactor(theme): extract loader into subpackage` +- `chore(dx): clean up permission settings` + +## Branch Naming + +``` +type/issue-description +``` + +Use the same type prefix as the anticipated merge commit. Examples: +- `feat/42-dark-mode` +- `fix/15-header-overflow` +- `refactor/16-theme-subpackage` +- `chore/23-update-dependencies` +- `docs/8-api-documentation` + +## Commit Messages + +- Use multiple `-m` flags instead of `$()` heredoc substitution, which triggers a command-substitution security prompt +- Example: `git commit -m "fix(engine): wire notes into rendering" -m "Detailed body here." -m "Co-Authored-By: ..."` + +## Issue Titles + +- Use plain English descriptions, **not** conventional commit format +- Conventional commit format is only for commit messages and PR titles diff --git a/scripts/setup-worktree.py b/.claude/skills/git/scripts/setup-worktree.py similarity index 99% rename from scripts/setup-worktree.py rename to .claude/skills/git/scripts/setup-worktree.py index abcae62..9c9f06e 100644 --- a/scripts/setup-worktree.py +++ b/.claude/skills/git/scripts/setup-worktree.py @@ -20,7 +20,7 @@ import sys from pathlib import Path -PROJECT_ROOT = Path(__file__).parent.parent.resolve() +PROJECT_ROOT = Path(__file__).resolve().parents[4] # .claude/skills/git/scripts/file.py -> repo root WORKTREES_DIR = PROJECT_ROOT / ".worktrees" diff --git a/.claude/skills/github/SKILL.md b/.claude/skills/github/SKILL.md index 36a444a..0d21b02 100644 --- a/.claude/skills/github/SKILL.md +++ b/.claude/skills/github/SKILL.md @@ -27,7 +27,7 @@ Use the `gh` CLI for all GitHub operations. Run individual `gh` commands via the | Task | Tool | Example | |------|------|---------| | Standard CRUD (issues, PRs, labels) | `gh issue`, `gh pr`, `gh label` | `gh issue create --title "Bug"` | -| Fields not exposed by CLI (issue types, sub-issues) | `gh api graphql` | See GraphQL sections below | +| Fields not exposed by CLI (issue types, sub-issues) | `gh api graphql` | See `references/graphql.md` | | Simple REST endpoints | `gh api` | `gh api repos/{owner}/{repo}/milestones` | ## Issues @@ -41,7 +41,7 @@ Use the `gh` CLI for all GitHub operations. Run individual `gh` commands via the 2. **Create the issue:** ```bash - gh issue create --title "feat: add dark mode" --body "$(cat <<'EOF' + gh issue create --title "Add dark mode" --body "$(cat <<'EOF' ## Description Add dark mode support. @@ -52,186 +52,26 @@ Use the `gh` CLI for all GitHub operations. Run individual `gh` commands via the )" --label "enhancement" ``` -3. **Set issue type** (see Issue Types section) - -4. **Apply labels:** +3. **Set issue type** — see `references/graphql.md` for GraphQL mutations, or use the helper script: ```bash - gh issue edit 42 --add-label "engine,enhancement" + python .claude/skills/github/scripts/github-issue-updater.py 42 --type task ``` -5. **Prompt about milestone** — ask the user if the issue should be assigned to a milestone. Current milestone: **SquishMark 1.0**: - ```bash - # List milestones to get the number - gh api repos/{owner}/{repo}/milestones --jq '.[].title' +4. **Apply labels:** `gh issue edit 42 --add-label "engine,enhancement"` — see `references/labels.md` - # Assign milestone by title - gh issue edit 42 --milestone "SquishMark 1.0" - ``` +5. **Prompt about milestone** — ask the user. See `references/milestones.md` ### Edit and Search ```bash -# Edit title or body gh issue edit 42 --title "New title" -gh issue edit 42 --body "New body" - -# List open issues gh issue list - -# Search with filters gh issue list --search "label:bug sort:updated-desc" -gh issue list --label "engine" --state open -``` - -## Issue Types (GraphQL) - -Issue types are not exposed via the standard CLI. Use GraphQL mutations. - -**Shortcut:** Use `python scripts/github-issue-updater.py --type task|bug|feature` to set type, labels, and milestone in one command. - -### Lookup Project Issue Types - -```bash -gh api graphql -f query=' - query { - repository(owner: "xeek-dev", name: "squishmark") { - issueTypes(first: 10) { - nodes { id name } - } - } - } -' -``` - -### Known Issue Type IDs - -| Type | ID | -|------|----| -| Task | `IT_kwDOBA-w0M4Aw6D3` | -| Bug | `IT_kwDOBA-w0M4Aw6D4` | -| Feature | `IT_kwDOBA-w0M4Aw6D7` | - -### Set Issue Type - -```bash -gh api graphql -f query=' - mutation { - updateIssue(input: { - id: "ISSUE_NODE_ID", - issueTypeId: "IT_kwDOBA-w0M4Aw6D3" - }) { - issue { title } - } - } -' -``` - -To get the issue node ID: `gh issue view 42 --json id --jq .id` - -## Sub-Issues (GraphQL) - -Sub-issues use a parent-child relationship. Each issue can have at most one parent. - -### Add Sub-Issue - -```bash -gh api graphql -f query=' - mutation { - addSubIssue(input: { - issueId: "PARENT_NODE_ID", - subIssueId: "CHILD_NODE_ID" - }) { - issue { title } - subIssue { title } - } - } -' -``` - -## Labels - -### Existing Labels - -bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, engine, themes, content, seo, ai, dx - -### Commands - -```bash -# Add labels -gh issue edit 42 --add-label "engine,themes" - -# Remove labels -gh issue edit 42 --remove-label "duplicate" - -# List all labels -gh label list - -# Create a new label -gh label create "new-label" --color "0E8A16" --description "Label description" -``` - -## Milestones - -Current milestone: **SquishMark 1.0** - -```bash -# List milestones (REST) -gh api repos/{owner}/{repo}/milestones --jq '.[] | "\(.number) \(.title)"' - -# Assign milestone to issue -gh issue edit 42 --milestone "SquishMark 1.0" - -# Assign milestone to PR -gh pr edit 42 --milestone "SquishMark 1.0" ``` ## Pull Requests -### Create - -Always use HEREDOC for the body and link to the issue with `Closes #N`: - -```bash -gh pr create --title "feat(engine): add dark mode" --body "$(cat <<'EOF' -## Summary -- Add dark mode toggle to settings -- Persist preference across sessions - -Closes #42 -EOF -)" -``` - -### Review and Comment - -```bash -# View PR details -gh pr view 42 - -# Check CI status -gh pr checks 42 - -# Add a comment (always sign with *— Claude*) -gh pr comment 42 --body "$(cat <<'EOF' -Looks good! One minor suggestion on the error handling. - -*— Claude* -EOF -)" - -# Approve -gh pr review 42 --approve --body "LGTM" -``` - -### Merge - -```bash -# Squash merge (preferred) -gh pr merge 42 --squash - -# With custom commit message -gh pr merge 42 --squash --subject "feat(engine): add dark mode (#42)" -``` +See `references/pull-requests.md` for create, review, comment, and merge commands. ## Conventions @@ -245,23 +85,7 @@ Your comment text here. *— Claude* ``` -### Conventional Commits - -Commit messages and PR titles must use [Conventional Commits](https://www.conventionalcommits.org/) format: - -``` -type(scope): description -``` - -Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `ci` - -### Branch Naming - -``` -type/issue-description -``` - -Examples: `feat/42-dark-mode`, `fix/15-header-overflow`, `docs/20-github-skill` +For git conventions (conventional commits, branch naming), see the `git` skill. ## Auth Troubleshooting diff --git a/.claude/skills/github/references/graphql.md b/.claude/skills/github/references/graphql.md new file mode 100644 index 0000000..fd362c8 --- /dev/null +++ b/.claude/skills/github/references/graphql.md @@ -0,0 +1,66 @@ +# GitHub GraphQL Operations + +## Issue Types + +Issue types are not exposed via the standard CLI. Use GraphQL mutations. + +**Shortcut:** Use `python .claude/skills/github/scripts/github-issue-updater.py --type task|bug|feature` to set type, labels, and milestone in one command. + +### Lookup Project Issue Types + +```bash +gh api graphql -f query=' + query { + repository(owner: "xeek-dev", name: "squishmark") { + issueTypes(first: 10) { + nodes { id name } + } + } + } +' +``` + +### Known Issue Type IDs + +| Type | ID | +|------|----| +| Task | `IT_kwDOBA-w0M4Aw6D3` | +| Bug | `IT_kwDOBA-w0M4Aw6D4` | +| Feature | `IT_kwDOBA-w0M4Aw6D7` | + +### Set Issue Type + +```bash +gh api graphql -f query=' + mutation { + updateIssue(input: { + id: "ISSUE_NODE_ID", + issueTypeId: "IT_kwDOBA-w0M4Aw6D3" + }) { + issue { title } + } + } +' +``` + +To get the issue node ID: `gh issue view 42 --json id --jq .id` + +## Sub-Issues + +Sub-issues use a parent-child relationship. Each issue can have at most one parent. + +### Add Sub-Issue + +```bash +gh api graphql -f query=' + mutation { + addSubIssue(input: { + issueId: "PARENT_NODE_ID", + subIssueId: "CHILD_NODE_ID" + }) { + issue { title } + subIssue { title } + } + } +' +``` diff --git a/.claude/skills/github/references/labels.md b/.claude/skills/github/references/labels.md new file mode 100644 index 0000000..0a2ed47 --- /dev/null +++ b/.claude/skills/github/references/labels.md @@ -0,0 +1,21 @@ +# Labels + +## Existing Labels + +bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, engine, themes, content, seo, ai, dx + +## Commands + +```bash +# Add labels +gh issue edit 42 --add-label "engine,themes" + +# Remove labels +gh issue edit 42 --remove-label "duplicate" + +# List all labels +gh label list + +# Create a new label +gh label create "new-label" --color "0E8A16" --description "Label description" +``` diff --git a/.claude/skills/github/references/milestones.md b/.claude/skills/github/references/milestones.md new file mode 100644 index 0000000..cf2d604 --- /dev/null +++ b/.claude/skills/github/references/milestones.md @@ -0,0 +1,14 @@ +# Milestones + +Current milestone: **SquishMark 1.0** + +```bash +# List milestones (REST) +gh api repos/{owner}/{repo}/milestones --jq '.[] | "\(.number) \(.title)"' + +# Assign milestone to issue +gh issue edit 42 --milestone "SquishMark 1.0" + +# Assign milestone to PR +gh pr edit 42 --milestone "SquishMark 1.0" +``` diff --git a/.claude/skills/github/references/pull-requests.md b/.claude/skills/github/references/pull-requests.md new file mode 100644 index 0000000..0bbbe58 --- /dev/null +++ b/.claude/skills/github/references/pull-requests.md @@ -0,0 +1,47 @@ +# Pull Requests + +## Create + +Always use HEREDOC for the body and link to the issue with `Closes #N`: + +```bash +gh pr create --title "feat(engine): add dark mode" --body "$(cat <<'EOF' +## Summary +- Add dark mode toggle to settings +- Persist preference across sessions + +Closes #42 +EOF +)" +``` + +## Review and Comment + +```bash +# View PR details +gh pr view 42 + +# Check CI status +gh pr checks 42 + +# Add a comment (always sign with *— Claude*) +gh pr comment 42 --body "$(cat <<'EOF' +Looks good! One minor suggestion on the error handling. + +*— Claude* +EOF +)" + +# Approve +gh pr review 42 --approve --body "LGTM" +``` + +## Merge + +```bash +# Squash merge (preferred) +gh pr merge 42 --squash + +# With custom commit message +gh pr merge 42 --squash --subject "feat(engine): add dark mode (#42)" +``` diff --git a/scripts/github-issue-updater.py b/.claude/skills/github/scripts/github-issue-updater.py similarity index 100% rename from scripts/github-issue-updater.py rename to .claude/skills/github/scripts/github-issue-updater.py diff --git a/.claude/skills/theme-creator/SKILL.md b/.claude/skills/theme-creator/SKILL.md index aee69b1..8613fef 100644 --- a/.claude/skills/theme-creator/SKILL.md +++ b/.claude/skills/theme-creator/SKILL.md @@ -23,142 +23,7 @@ themes/{name}/ └── pygments.css # Syntax highlighting styles ``` -## Template Variables - -`site` and `theme` are populated from the content repository's `config.yml`. - -### Global (available in all templates) - -| Variable | Type | Description | -|----------|------|-------------| -| `site` | `SiteConfig` | Site-wide configuration from `config.yml` | -| `theme` | `ThemeConfig` | Theme configuration from `config.yml` | -| `theme_name` | `str` | Active theme name (e.g. `"terminal"`) | -| `favicon_url` | `str \| None` | Resolved favicon URL, if any | -| `featured_posts` | `list[Post]` | Featured posts, sorted and limited by `site.featured_max` (always present, may be empty) | - -### Per-Template Context - -| Template | Additional Variables | -|----------|---------------------| -| `index.html` | `posts` (list[Post]), `pagination` (Pagination), `notes` (list) | -| `post.html` | `post` (Post), `notes` (list) | -| `page.html` | `page` (Page), `notes` (list) | -| `404.html` | Global context only | -| `admin/admin.html` | `user` (dict), `analytics` (dict), `notes` (list[NoteResponse]), `cache_size` (int) | - -## Model Fields - -### SiteConfig - -| Field | Type | Description | -|-------|------|-------------| -| `title` | `str` | Site title | -| `description` | `str` | Site description | -| `author` | `str` | Site author name | -| `url` | `str` | Site URL | -| `favicon` | `str \| None` | Custom favicon path override | -| `featured_max` | `int` | Max number of featured posts returned (default: 5) | - -### Post - -| Field | Type | Description | -|-------|------|-------------| -| `slug` | `str` | URL slug | -| `title` | `str` | Post title | -| `date` | `datetime.date \| None` | Publication date | -| `tags` | `list[str]` | Tag list | -| `description` | `str` | Short description / excerpt | -| `content` | `str` | Raw markdown content | -| `html` | `str` | Rendered HTML content | -| `draft` | `bool` | Draft flag | -| `featured` | `bool` | Whether the post is featured | -| `featured_order` | `int \| None` | Sort order within featured posts (lower = first, nulls last) | -| `template` | `str \| None` | Custom template override | -| `theme` | `str \| None` | Per-post theme override | -| `author` | `str \| None` | Post author (overrides site author) | -| `url` | `str` | Computed URL (property) — e.g. `/posts/my-post` | - -### Page - -| Field | Type | Description | -|-------|------|-------------| -| `slug` | `str` | URL slug | -| `title` | `str` | Page title | -| `content` | `str` | Raw markdown content | -| `html` | `str` | Rendered HTML content | -| `template` | `str \| None` | Custom template override | -| `theme` | `str \| None` | Per-page theme override | -| `url` | `str` | Computed URL (property) — e.g. `/about` | - -### Pagination - -| Field | Type | Description | -|-------|------|-------------| -| `page` | `int` | Current page number | -| `per_page` | `int` | Items per page | -| `total_items` | `int` | Total number of posts | -| `total_pages` | `int` | Total number of pages | -| `has_prev` | `bool` | Whether a previous page exists (property) | -| `has_next` | `bool` | Whether a next page exists (property) | -| `prev_page` | `int \| None` | Previous page number (property) | -| `next_page` | `int \| None` | Next page number (property) | - -## Template Inheritance - -Templates extend `base.html` using Jinja2 inheritance: - -```jinja2 -{% extends "base.html" %} - -{% block title %}{{ post.title }} - {{ site.title }}{% endblock %} - -{% block content %} - {# page content here #} -{% endblock %} -``` - -### Available Blocks in base.html - -| Block | Purpose | -|-------|---------| -| `title` | Page `` tag content | -| `description` | Meta description content | -| `head` | Additional `<head>` elements (CSS, JS, meta tags) | -| `content` | Main page content | - -**Note:** `admin/admin.html` is standalone and does NOT extend `base.html`. - -## Jinja2 Filters - -| Filter | Signature | Description | -|--------|-----------|-------------| -| `format_date` | `format_date(value, fmt="%B %d, %Y")` | Formats a date — e.g. `{{ post.date \| format_date }}` | -| `accent_first_word` | `accent_first_word(value)` | Wraps first word in `<span class="accent">` | -| `accent_last_word` | `accent_last_word(value)` | Wraps last word in `<span class="accent">` | - -All accent filters return `Markup` (safe HTML). - -## Static Files - -### URL Patterns - -| Type | URL | Source | -|------|-----|--------| -| Theme static | `/static/{theme_name}/{file_path}` | `themes/{name}/static/` | -| User static | `/static/user/{path}` | Content repo `static/` directory | - -- Allowed extensions: `.ico` `.png` `.svg` `.jpg` `.jpeg` `.webp` `.gif` `.css` `.js` -- Cache-Control: `public, max-age=86400` (1 day) -- Theme static files fall back to the `default` theme if not found in the current theme - -### Referencing Static Files in Templates - -```jinja2 -<link rel="stylesheet" href="/static/{{ theme_name }}/style.css"> -<link rel="stylesheet" href="/static/{{ theme_name }}/pygments.css"> -<img src="/static/user/logo.png" alt="Logo"> -``` +For template variables, model fields, filters, and other reference material, see the `references/` directory. ## Theme Resolution Order @@ -182,49 +47,37 @@ template: custom.html # Use custom.html instead of post.html --- ``` -## Pygments CSS +## Content Repository Structure -Generate syntax highlighting CSS for your theme: +Users create a content repo with this structure: -```bash -pygmentize -S <style> -f html > themes/{name}/static/pygments.css +``` +my-content-repo/ +├── posts/ +│ ├── 2026-01-01-hello-world.md +│ └── 2026-01-15-another-post.md +├── pages/ +│ └── about.md +├── static/ # User static files (favicon, images) +│ └── favicon.ico # Auto-detected and served at /favicon.ico +├── theme/ # Optional custom theme +│ └── ... +└── config.yml ``` -Common styles: `monokai`, `dracula`, `github-dark`, `one-dark`, `gruvbox-dark`, `nord`. - -## ThemeConfig Extensibility - -ThemeConfig has 5 built-in fields: - -| Field | Type | Description | -|-------|------|-------------| -| `name` | `str` | Theme name | -| `pygments_style` | `str` | Pygments syntax highlighting style | -| `background` | `str \| None` | Background option (theme-specific) | -| `nav_image` | `str \| None` | Navigation image path | -| `hero_image` | `str \| None` | Hero section image path | - -**Any extra fields** added under `theme:` in `config.yml` are accessible as `{{ theme.fieldname }}` in templates. This is enabled by `model_config = {"extra": "allow"}` on `ThemeConfig`. - -Example — a theme that supports a custom accent color: +### Frontmatter Format ```yaml -# config.yml -theme: - name: my-theme - pygments_style: monokai - accent_color: "#ff6600" - show_sidebar: true -``` +--- +title: My Post Title +date: 2026-01-15 +tags: [python, blogging] +draft: false +featured: true # Optional: include in featured_posts template context +featured_order: 1 # Optional: sort order (lower = first, nulls last) +--- -```jinja2 -{# In a template #} -<style> - :root { --accent: {{ theme.accent_color }}; } -</style> -{% if theme.show_sidebar %} - <aside>...</aside> -{% endif %} +Post content in markdown... ``` ## Live Reload diff --git a/.claude/skills/theme-creator/references/filters.md b/.claude/skills/theme-creator/references/filters.md new file mode 100644 index 0000000..e982059 --- /dev/null +++ b/.claude/skills/theme-creator/references/filters.md @@ -0,0 +1,9 @@ +# Jinja2 Filters + +| Filter | Signature | Description | +|--------|-----------|-------------| +| `format_date` | `format_date(value, fmt="%B %d, %Y")` | Formats a date — e.g. `{{ post.date \| format_date }}` | +| `accent_first_word` | `accent_first_word(value)` | Wraps first word in `<span class="accent">` | +| `accent_last_word` | `accent_last_word(value)` | Wraps last word in `<span class="accent">` | + +All accent filters return `Markup` (safe HTML). diff --git a/.claude/skills/theme-creator/references/model-fields.md b/.claude/skills/theme-creator/references/model-fields.md new file mode 100644 index 0000000..8db0a69 --- /dev/null +++ b/.claude/skills/theme-creator/references/model-fields.md @@ -0,0 +1,56 @@ +# Model Fields + +## SiteConfig + +| Field | Type | Description | +|-------|------|-------------| +| `title` | `str` | Site title | +| `description` | `str` | Site description | +| `author` | `str` | Site author name | +| `url` | `str` | Site URL | +| `favicon` | `str \| None` | Custom favicon path override | +| `featured_max` | `int` | Max number of featured posts returned (default: 5) | + +## Post + +| Field | Type | Description | +|-------|------|-------------| +| `slug` | `str` | URL slug | +| `title` | `str` | Post title | +| `date` | `datetime.date \| None` | Publication date | +| `tags` | `list[str]` | Tag list | +| `description` | `str` | Short description / excerpt | +| `content` | `str` | Raw markdown content | +| `html` | `str` | Rendered HTML content | +| `draft` | `bool` | Draft flag | +| `featured` | `bool` | Whether the post is featured | +| `featured_order` | `int \| None` | Sort order within featured posts (lower = first, nulls last) | +| `template` | `str \| None` | Custom template override | +| `theme` | `str \| None` | Per-post theme override | +| `author` | `str \| None` | Post author (overrides site author) | +| `url` | `str` | Computed URL (property) — e.g. `/posts/my-post` | + +## Page + +| Field | Type | Description | +|-------|------|-------------| +| `slug` | `str` | URL slug | +| `title` | `str` | Page title | +| `content` | `str` | Raw markdown content | +| `html` | `str` | Rendered HTML content | +| `template` | `str \| None` | Custom template override | +| `theme` | `str \| None` | Per-page theme override | +| `url` | `str` | Computed URL (property) — e.g. `/about` | + +## Pagination + +| Field | Type | Description | +|-------|------|-------------| +| `page` | `int` | Current page number | +| `per_page` | `int` | Items per page | +| `total_items` | `int` | Total number of posts | +| `total_pages` | `int` | Total number of pages | +| `has_prev` | `bool` | Whether a previous page exists (property) | +| `has_next` | `bool` | Whether a next page exists (property) | +| `prev_page` | `int \| None` | Previous page number (property) | +| `next_page` | `int \| None` | Next page number (property) | diff --git a/.claude/skills/theme-creator/references/pygments.md b/.claude/skills/theme-creator/references/pygments.md new file mode 100644 index 0000000..735395f --- /dev/null +++ b/.claude/skills/theme-creator/references/pygments.md @@ -0,0 +1,15 @@ +# Pygments CSS + +Pygments renders code blocks server-side — HTML comes pre-highlighted with no client-side JavaScript. Supports 500+ languages. + +## Generate CSS + +```bash +pygmentize -S <style> -f html > themes/{name}/static/pygments.css +``` + +## Common Styles + +`monokai`, `dracula`, `github-dark`, `one-dark`, `gruvbox-dark`, `nord` + +Theme CSS controls the colors. diff --git a/.claude/skills/theme-creator/references/static-files.md b/.claude/skills/theme-creator/references/static-files.md new file mode 100644 index 0000000..5c96862 --- /dev/null +++ b/.claude/skills/theme-creator/references/static-files.md @@ -0,0 +1,20 @@ +# Static Files + +## URL Patterns + +| Type | URL | Source | +|------|-----|--------| +| Theme static | `/static/{theme_name}/{file_path}` | `themes/{name}/static/` | +| User static | `/static/user/{path}` | Content repo `static/` directory | + +- Allowed extensions: `.ico` `.png` `.svg` `.jpg` `.jpeg` `.webp` `.gif` `.css` `.js` +- Cache-Control: `public, max-age=86400` (1 day) +- Theme static files fall back to the `default` theme if not found in the current theme + +## Referencing Static Files in Templates + +```jinja2 +<link rel="stylesheet" href="/static/{{ theme_name }}/style.css"> +<link rel="stylesheet" href="/static/{{ theme_name }}/pygments.css"> +<img src="/static/user/logo.png" alt="Logo"> +``` diff --git a/.claude/skills/theme-creator/references/template-inheritance.md b/.claude/skills/theme-creator/references/template-inheritance.md new file mode 100644 index 0000000..4ce9106 --- /dev/null +++ b/.claude/skills/theme-creator/references/template-inheritance.md @@ -0,0 +1,24 @@ +# Template Inheritance + +Templates extend `base.html` using Jinja2 inheritance: + +```jinja2 +{% extends "base.html" %} + +{% block title %}{{ post.title }} - {{ site.title }}{% endblock %} + +{% block content %} + {# page content here #} +{% endblock %} +``` + +## Available Blocks in base.html + +| Block | Purpose | +|-------|---------| +| `title` | Page `<title>` tag content | +| `description` | Meta description content | +| `head` | Additional `<head>` elements (CSS, JS, meta tags) | +| `content` | Main page content | + +**Note:** `admin/admin.html` is standalone and does NOT extend `base.html`. diff --git a/.claude/skills/theme-creator/references/template-variables.md b/.claude/skills/theme-creator/references/template-variables.md new file mode 100644 index 0000000..8b2ca71 --- /dev/null +++ b/.claude/skills/theme-creator/references/template-variables.md @@ -0,0 +1,23 @@ +# Template Variables + +`site` and `theme` are populated from the content repository's `config.yml`. + +## Global (available in all templates) + +| Variable | Type | Description | +|----------|------|-------------| +| `site` | `SiteConfig` | Site-wide configuration from `config.yml` | +| `theme` | `ThemeConfig` | Theme configuration from `config.yml` | +| `theme_name` | `str` | Active theme name (e.g. `"terminal"`) | +| `favicon_url` | `str \| None` | Resolved favicon URL, if any | +| `featured_posts` | `list[Post]` | Featured posts, sorted and limited by `site.featured_max` (always present, may be empty) | + +## Per-Template Context + +| Template | Additional Variables | +|----------|---------------------| +| `index.html` | `posts` (list[Post]), `pagination` (Pagination), `notes` (list) | +| `post.html` | `post` (Post), `notes` (list) | +| `page.html` | `page` (Page), `notes` (list) | +| `404.html` | Global context only | +| `admin/admin.html` | `user` (dict), `analytics` (dict), `notes` (list[NoteResponse]), `cache_size` (int) | diff --git a/.claude/skills/theme-creator/references/theme-config.md b/.claude/skills/theme-creator/references/theme-config.md new file mode 100644 index 0000000..c5022e3 --- /dev/null +++ b/.claude/skills/theme-creator/references/theme-config.md @@ -0,0 +1,34 @@ +# ThemeConfig Extensibility + +ThemeConfig has 5 built-in fields: + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | Theme name | +| `pygments_style` | `str` | Pygments syntax highlighting style | +| `background` | `str \| None` | Background option (theme-specific) | +| `nav_image` | `str \| None` | Navigation image path | +| `hero_image` | `str \| None` | Hero section image path | + +**Any extra fields** added under `theme:` in `config.yml` are accessible as `{{ theme.fieldname }}` in templates. This is enabled by `model_config = {"extra": "allow"}` on `ThemeConfig`. + +## Example — Custom Accent Color + +```yaml +# config.yml +theme: + name: my-theme + pygments_style: monokai + accent_color: "#ff6600" + show_sidebar: true +``` + +```jinja2 +{# In a template #} +<style> + :root { --accent: {{ theme.accent_color }}; } +</style> +{% if theme.show_sidebar %} + <aside>...</aside> +{% endif %} +``` diff --git a/.gitignore b/.gitignore index 76b840a..42117f1 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,6 @@ docker-compose.override.yml # Claude Code local settings (machine-specific) .claude/settings.local.json + +# Playwright artifacts +.playwright*/ diff --git a/CLAUDE.md b/CLAUDE.md index 19d66e3..87fed42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,28 +1,20 @@ -# CLAUDE.md - SquishMark Development Guide +# CLAUDE.md - SquishMark -This document provides context for AI assistants (and humans) working on the SquishMark codebase. - -## Project Overview - -SquishMark is a lightweight, GitHub-powered blogging engine. Content (posts, pages) lives in a separate GitHub repository and is fetched at runtime. Themes use Jinja2 templates, making them accessible to web developers without Python knowledge. - -## Shell Conventions - -- **Do not prefix every Bash command with `cd /path/to/repo &&`.** The working directory persists between Bash calls. Only change directory when you actually need to be somewhere different. -- Before your first shell command in a session, run `pwd` to confirm cwd if unsure — then just run commands directly. -- **Git commit messages:** Use multiple `-m` flags instead of `$()` heredoc substitution, which triggers a command-substitution security prompt. Example: `git commit -m "title" -m "body" -m "Co-Authored-By: ..."` +SquishMark is a lightweight, GitHub-powered blogging engine. Content lives in a separate GitHub repo and is fetched at runtime. Themes use Jinja2 templates. ## Skills -**INSTRUCTION:** If your task involves Python, Docker, browser testing, GitHub operations, or theme creation, invoke the relevant skill using `Skill(skill-name)`. +**INSTRUCTION:** If your task involves any of these domains, invoke the relevant skill using `Skill(skill-name)`. | Skill | Description | |-------|-------------| -| `python` | Python coding standards and gotchas (SQLAlchemy, python-markdown, async patterns) | +| `python` | Python coding standards (FastAPI, SQLAlchemy, Pydantic, async patterns) | | `docker` | Dockerfile standards (hatchling builds, multi-stage patterns) | -| `playwright` | E2E browser testing with Playwright MCP (navigation, screenshots, hard refresh, cache busting) | +| `playwright-cli` | Browser testing with Playwright (navigation, screenshots, verification) | | `github` | GitHub operations via gh CLI (issues, PRs, labels, milestones, GraphQL) | +| `git` | Git conventions (conventional commits, branch naming, commit messages) | | `theme-creator` | Theme authoring — templates, variables, filters, static files | +| `dev-setup` | Dev environment, scripts, configuration, deployment | ## Planning Workflow @@ -47,10 +39,18 @@ When planning implementation work, follow this workflow: 2. **Implementation** - Follow the approved plan - - Run verification (tests, lint, format) - - Manual testing: start dev server with `python scripts/start-dev.py` (or `-b` for background) and verify changes work - -3. **PR Workflow** + - Don't duplicate code — extract shared utilities immediately + - Write tests for new functionality (don't just fix broken existing tests) + - Run `python scripts/run-checks.py` (format, lint, pytest, pyright) — see `dev-setup` skill for options + +3. **Verification** + - Start dev server with `python scripts/start-dev.py` (or `-b` for background) — see `dev-setup` skill for options + - Manually verify changes work with Playwright (`playwright-cli`) + - Test all bundled themes: default, blue-tech, terminal + - Test both positive and negative cases (e.g. admin vs anonymous, with data vs empty) + - Don't state facts without verifying (check files exist, check CI status, etc.) + +4. **PR Workflow** - Commit messages and PR titles **must** use [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): description` - Examples: `feat(terminal): add pixel art title renderer`, `fix(docs): correct cache-busting instructions` - Use the same `type` as the branch prefix; `scope` is optional but recommended @@ -65,349 +65,8 @@ For all GitHub operations (issues, PRs, labels, milestones), use the `github` sk When commenting on PRs or issues, always sign with `*— Claude*`. -## Architecture - -```mermaid -flowchart TB - subgraph Fly["Fly.io"] - subgraph Container["Single Container"] - App["SquishMark<br/>(FastAPI)"] - DB[("SQLite<br/>/data/app.db")] - end - Volume[("Fly Volume<br/>$0.15/GB/mo")] - end - - GitHub[("GitHub Content Repo<br/>(posts, pages, config)")] - - App -->|fetch content| GitHub - App -->|read/write| DB - Volume -.->|persists| DB -``` - -### Request Flow - -```mermaid -sequenceDiagram - participant User - participant App as SquishMark - participant Cache - participant GitHub - participant DB as SQLite - - User->>App: GET /posts/hello-world - App->>Cache: Check cache - alt Cache hit - Cache-->>App: Return cached content - else Cache miss - App->>GitHub: Fetch markdown - GitHub-->>App: Raw content - App->>App: Parse frontmatter + render markdown - App->>Cache: Store in cache - end - App->>DB: Log page view (async) - App->>App: Render Jinja2 template - App-->>User: HTML response -``` - -## Tech Stack - -| Component | Choice | Notes | -|-----------|--------|-------| -| Language | Python 3.14 | Type hints throughout | -| Framework | FastAPI | Async, modern, auto-docs | -| Templating | Jinja2 | Familiar to Jekyll/Hugo users | -| Database | SQLite | On Fly Volume, no backup service | -| ORM | SQLAlchemy | With aiosqlite for async | -| Markdown | python-markdown + Pygments | Server-side syntax highlighting | -| HTTP Client | httpx | Async GitHub API calls | -| Auth | Authlib | GitHub OAuth for admin features | - -## Repository Structure - -``` -squishmark/ -├── src/ -│ └── squishmark/ -│ ├── __init__.py -│ ├── main.py # FastAPI app entry -│ ├── config.py # Pydantic settings -│ ├── dependencies.py # FastAPI dependency injection -│ ├── routers/ -│ │ ├── posts.py # Blog post routes -│ │ ├── pages.py # Static page routes -│ │ ├── admin.py # Notes, cache refresh, analytics -│ │ ├── auth.py # GitHub OAuth authentication -│ │ └── webhooks.py # GitHub webhook handling -│ ├── services/ -│ │ ├── github.py # Content fetching from GitHub -│ │ ├── markdown.py # Parsing + Pygments highlighting -│ │ ├── cache.py # In-memory content cache -│ │ ├── analytics.py # Page view tracking -│ │ ├── notes.py # Admin notes functionality -│ │ ├── url_rewriter.py # URL rewriting for markdown -│ │ └── theme/ # Jinja2 theme engine (subpackage) -│ │ ├── __init__.py -│ │ ├── engine.py -│ │ ├── loader.py -│ │ ├── filters.py -│ │ └── favicon.py -│ └── models/ -│ ├── content.py # Post, Page, FrontMatter -│ └── db.py # SQLAlchemy models -├── themes/ -│ ├── default/ # Bundled default theme -│ │ ├── base.html -│ │ ├── index.html -│ │ ├── post.html -│ │ ├── page.html -│ │ └── static/ -│ │ └── style.css -│ ├── blue-tech/ # Dark SaaS aesthetic -│ └── terminal/ # Dark terminal with pixel art titles -├── tests/ -├── pyproject.toml -├── Dockerfile -├── fly.toml -├── CLAUDE.md -├── LICENSE -└── README.md -``` - -## Related Repositories - -- **squishmark-starter**: Template repo for users to create their own content -- **xeek-dev-content**: Private content repo for xeek.dev (example implementation) - -## Key Design Decisions - -### Content from GitHub -- Posts and pages are fetched from a configurable GitHub repository -- Supports both public repos (no auth) and private repos (GitHub token) -- Content is cached in memory with configurable TTL -- Cache can be manually refreshed via admin endpoint or GitHub webhook - -### Theming -- Themes are Jinja2 templates (`.html` files) -- Theme resolution order: - 1. Custom theme in content repo (`/theme/` directory) - 2. Bundled default theme -- Theme authors only need HTML/CSS/Jinja2 knowledge, no Python required - -```mermaid -flowchart TD - Request["Render page"] - Check{"Custom theme<br/>in content repo?"} - Custom["Load from<br/>content repo /theme/"] - Default["Load bundled<br/>default theme"] - Render["Render with Jinja2"] - - Request --> Check - Check -->|Yes| Custom - Check -->|No| Default - Custom --> Render - Default --> Render -``` - -### Syntax Highlighting -- Pygments renders code blocks server-side -- HTML comes pre-highlighted, no client-side JavaScript needed -- Supports 500+ languages -- Theme CSS controls colors (monokai, dracula, github-dark, etc.) - -### Database (SQLite on Fly Volume) -- Stores analytics (page views), admin notes, sessions -- NOT for content - content lives in GitHub -- Acceptable data loss risk since blog content is safe in GitHub -- Simple setup: just mount the Fly Volume at `/data` - -```mermaid -erDiagram - PAGE_VIEW { - int id PK - string path - string referrer - string user_agent - datetime timestamp - } - - NOTE { - int id PK - string path - string text - boolean is_public - datetime created_at - datetime updated_at - } - - SESSION { - string id PK - string user_id - string github_login - datetime expires_at - } -``` - -### Admin Features -- GitHub OAuth login for admin access -- Notes/corrections: add public or private notes to any page -- Analytics: basic page view tracking -- Cache control: manual refresh endpoint - -## Content Repository Structure - -Users create a content repo with this structure: - -``` -my-content-repo/ -├── posts/ -│ ├── 2026-01-01-hello-world.md -│ └── 2026-01-15-another-post.md -├── pages/ -│ └── about.md -├── static/ # User static files (favicon, images) -│ └── favicon.ico # Auto-detected and served at /favicon.ico -├── theme/ # Optional custom theme -│ └── ... -└── config.yml -``` - -### Frontmatter Format - -```yaml ---- -title: My Post Title -date: 2026-01-15 -tags: [python, blogging] -draft: false -featured: true # Optional: include in featured_posts template context -featured_order: 1 # Optional: sort order (lower = first, nulls last) ---- - -Post content in markdown... -``` - -## Configuration - -### Environment Variables - -```bash -# Required -GITHUB_CONTENT_REPO=xeek-dev/xeek-dev-content -GITHUB_TOKEN=ghp_... # Only for private repos - -# Optional -CACHE_TTL_SECONDS=300 -DATABASE_URL=sqlite:///data/squishmark.db - -# GitHub OAuth (for admin features) -GITHUB_CLIENT_ID=... -GITHUB_CLIENT_SECRET=... -``` - -### config.yml (in content repo) - -```yaml -site: - title: "My Blog" - description: "A blog about things" - author: "Your Name" - url: "https://example.com" - favicon: "/static/user/custom-icon.png" # Optional: override auto-detected favicon - featured_max: 5 # Optional: max featured posts (default: 5) - -theme: - name: default - pygments_style: monokai - -posts: - per_page: 10 -``` - ## Development -### Local Setup - -```bash -# Clone the repo -git clone https://github.com/xeek-dev/squishmark.git -cd squishmark - -# Create virtual environment -python -m venv .venv -source .venv/bin/activate # or .venv\Scripts\activate on Windows - -# Install dependencies -pip install -e ".[dev]" - -# Run locally -uvicorn squishmark.main:app --reload -``` - -### Development Scripts - -Three scripts in `scripts/` streamline local development: - -#### `start-dev.py` — Dev server with multi-server management - -Starts a uvicorn dev server pointing at the local `content/` directory. Supports running multiple named server instances simultaneously (tracked in `.dev-servers.json`). The default instance name is the current git branch. - -```bash -# Basic usage -python scripts/start-dev.py # foreground on :8000 -python scripts/start-dev.py -b # background on :8000 -python scripts/start-dev.py -b --port 8001 # background on :8001 -python scripts/start-dev.py --name api -b # named background instance - -# Server management -python scripts/start-dev.py --list # show all tracked servers -python scripts/start-dev.py --stop # stop current branch's server -python scripts/start-dev.py --stop api # stop named server -python scripts/start-dev.py --stop 12345 # stop server by PID -python scripts/start-dev.py --stop-all # stop all servers -python scripts/start-dev.py --restart -b # restart in background - -# Other options -python scripts/start-dev.py --host 0.0.0.0 # bind to all interfaces -python scripts/start-dev.py --no-reload # disable auto-reload -``` - -> **Stale port gotcha:** Background start (`-b`) can silently fail if a stale process holds the port. The old process keeps serving while the new one exits. Always verify after restart: `--stop <name>` then `lsof -ti:<port> | xargs kill` before starting fresh. - -#### `run-checks.py` — Local CI checks - -Runs the same checks as CI: **ruff format**, **ruff check**, **pytest**, and **pyright**. By default runs all checks and reports a summary. - -```bash -python scripts/run-checks.py # run all checks -python scripts/run-checks.py --fail-fast # stop on first failure -python scripts/run-checks.py --docker # also run docker build (slow) -``` - -#### `setup-worktree.py` — Git worktree management - -Creates isolated worktrees in `.worktrees/` for parallel development. Branch names follow the project's `type/issue-description` convention; the type prefix is stripped to form the directory name (e.g., `feat/42-dark-mode` becomes `.worktrees/42-dark-mode`). - -```bash -python scripts/setup-worktree.py feat/42-dark-mode # create worktree + branch -python scripts/setup-worktree.py feat/42-dark-mode --install # also pip install -e -python scripts/setup-worktree.py feat/42-dark-mode --with-content # also copy content/ -python scripts/setup-worktree.py feat/42-dark-mode --integration # --install + --with-content -python scripts/setup-worktree.py --list # list active worktrees -python scripts/setup-worktree.py --cleanup 42-dark-mode # remove worktree + branch -python scripts/setup-worktree.py --cleanup 42-dark-mode --force # skip confirmation -``` - -#### `github-issue-updater.py` — Issue metadata updater - -Sets issue type (task/bug/feature), adds labels, and assigns milestones in a single command. Issue types require GraphQL mutations which this script handles automatically. - -```bash -python scripts/github-issue-updater.py 42 --type task -python scripts/github-issue-updater.py 42 --add-label "engine,themes" -python scripts/github-issue-updater.py 42 --milestone "SquishMark 1.0" -python scripts/github-issue-updater.py 42 --type task --add-label engine --milestone "SquishMark 1.0" -``` - ### Running Tests ```bash @@ -420,35 +79,3 @@ pytest docker build -t squishmark . docker run -p 8000:8000 -e GITHUB_CONTENT_REPO=user/repo squishmark ``` - -## Deployment (Fly.io) - -```bash -# First time -fly launch - -# Create volume for SQLite -fly volumes create squishmark_data --size 1 - -# Set secrets -fly secrets set GITHUB_TOKEN=ghp_... -fly secrets set GITHUB_CLIENT_ID=... -fly secrets set GITHUB_CLIENT_SECRET=... - -# Deploy -fly deploy -``` - -## Common Tasks - -### Adding a new route -1. Create router in `src/squishmark/routers/` -2. Register in `main.py` -3. Add corresponding Jinja2 template if needed - -### Adding a Pygments theme -1. Generate CSS: `pygmentize -S monokai -f html > themes/default/static/pygments.css` -2. Include in theme's base template - -### Testing with a local content repo -Set `GITHUB_CONTENT_REPO` to a local path (prefixed with `file://`) for development without GitHub API calls.