From c3c856db87cb501aa38314ffeb9f1b609f5c2922 Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Tue, 28 Apr 2026 13:37:08 -0400 Subject: [PATCH 1/8] devcontainer and mcp setup notes on lint and e2e drop cache file code review fixes code review fixes security and additional fixes additional security fixes more code review --- .apm/instructions/mcp.instructions.md | 8 + .claude/rules/mcp.md | 8 + .cursor/mcp.json | 8 + .cursor/rules/mcp.mdc | 8 + .cursor/skills/sippy-dev-app/SKILL.md | 17 + .cursor/skills/sippy-dev-frontend/SKILL.md | 14 + .cursor/skills/sippy-dev-migrate/SKILL.md | 14 + .../sippy-dev-regression-cache/SKILL.md | 16 + .cursor/skills/sippy-dev-serve/SKILL.md | 13 + .cursor/skills/sippy-dev-tests/SKILL.md | 17 + .devcontainer/.env.example | 7 + .devcontainer/Dockerfile | 56 ++ .devcontainer/README.md | 135 ++++ .devcontainer/devcontainer.json | 53 ++ .devcontainer/init-services.sh | 49 ++ .devcontainer/post-create.sh | 31 + .gitattributes | 6 +- .gitignore | 5 + DEVELOPMENT.md | 13 + Makefile | 4 +- apm.lock.yaml | 4 + mcp/AGENTS.md | 16 + mcp/CLAUDE.md | 17 + mcp/README.md | 76 +++ mcp/requirements.txt | 1 + mcp/server.py | 583 ++++++++++++++++++ 26 files changed, 1176 insertions(+), 3 deletions(-) create mode 100644 .apm/instructions/mcp.instructions.md create mode 100644 .claude/rules/mcp.md create mode 100644 .cursor/mcp.json create mode 100644 .cursor/rules/mcp.mdc create mode 100644 .cursor/skills/sippy-dev-app/SKILL.md create mode 100644 .cursor/skills/sippy-dev-frontend/SKILL.md create mode 100644 .cursor/skills/sippy-dev-migrate/SKILL.md create mode 100644 .cursor/skills/sippy-dev-regression-cache/SKILL.md create mode 100644 .cursor/skills/sippy-dev-serve/SKILL.md create mode 100644 .cursor/skills/sippy-dev-tests/SKILL.md create mode 100644 .devcontainer/.env.example create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/README.md create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/init-services.sh create mode 100755 .devcontainer/post-create.sh create mode 100644 mcp/AGENTS.md create mode 100644 mcp/CLAUDE.md create mode 100644 mcp/README.md create mode 100644 mcp/requirements.txt create mode 100644 mcp/server.py diff --git a/.apm/instructions/mcp.instructions.md b/.apm/instructions/mcp.instructions.md new file mode 100644 index 000000000..d91d16f42 --- /dev/null +++ b/.apm/instructions/mcp.instructions.md @@ -0,0 +1,8 @@ +--- +description: "MCP server (sippy-dev) for AI-callable dev tasks" +applyTo: "mcp/**" +--- + +Shared MCP server for AI-callable dev tasks (migrate, serve, lint, test, e2e). Configuration, tool list, logs, and extension notes: **[README.md](../../mcp/README.md)**. + +When adding or modifying MCP tools, follow existing patterns in `server.py` (subprocess, `_repo_path`, `_ensure_dev_log_dir`, credentials helpers). Restart the MCP server after changes. diff --git a/.claude/rules/mcp.md b/.claude/rules/mcp.md new file mode 100644 index 000000000..95448fee4 --- /dev/null +++ b/.claude/rules/mcp.md @@ -0,0 +1,8 @@ +--- +paths: + - "mcp/**" +--- + +Shared MCP server for AI-callable dev tasks (migrate, serve, lint, test, e2e). Configuration, tool list, logs, and extension notes: **[README.md](../../mcp/README.md)**. + +When adding or modifying MCP tools, follow existing patterns in `server.py` (subprocess, `_repo_path`, `_ensure_dev_log_dir`, credentials helpers). Restart the MCP server after changes. diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 000000000..31d365017 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "sippy-dev": { + "command": "mcp/.venv/bin/python", + "args": ["mcp/server.py"] + } + } +} diff --git a/.cursor/rules/mcp.mdc b/.cursor/rules/mcp.mdc new file mode 100644 index 000000000..c72638e72 --- /dev/null +++ b/.cursor/rules/mcp.mdc @@ -0,0 +1,8 @@ +--- +description: MCP server (sippy-dev) for AI-callable dev tasks +globs: "mcp/**" +--- + +Shared MCP server for AI-callable dev tasks (migrate, serve, lint, test, e2e). Configuration, tool list, logs, and extension notes: **[README.md](../../mcp/README.md)**. + +When adding or modifying MCP tools, follow existing patterns in `server.py` (subprocess, `_repo_path`, `_ensure_dev_log_dir`, credentials helpers). Restart the MCP server after changes. diff --git a/.cursor/skills/sippy-dev-app/SKILL.md b/.cursor/skills/sippy-dev-app/SKILL.md new file mode 100644 index 000000000..97bda60ef --- /dev/null +++ b/.cursor/skills/sippy-dev-app/SKILL.md @@ -0,0 +1,17 @@ +--- +name: sippy-dev-app +description: >- + Starts the local Sippy stack by calling sippy-dev MCP tools sippy_serve then + sippy_ng_start (backend then frontend). Use when the user wants both API/UI dev + servers, full local Sippy, or a single workflow instead of separate serve and + frontend steps. +--- + +# Sippy dev MCP — backend + frontend + +Two **`call_mcp_tool`** calls, same server (**`sippy-dev`** or prefixed, e.g. **`project-0-workspace-sippy-dev`**). Do not use shell **`go run ./cmd/sippy serve`** or **`npm start`**. + +1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set (see **`mcp/server.py`** / **sippy-dev-serve**). +2. **`sippy_ng_start`** + +Backend first, then frontend. Each tool returns listen hints (typically **8080** / **3000**) and log/pid paths; if a tool says already running, leave that process as-is. diff --git a/.cursor/skills/sippy-dev-frontend/SKILL.md b/.cursor/skills/sippy-dev-frontend/SKILL.md new file mode 100644 index 000000000..e87467dd0 --- /dev/null +++ b/.cursor/skills/sippy-dev-frontend/SKILL.md @@ -0,0 +1,14 @@ +--- +name: sippy-dev-frontend +description: >- + Starts the sippy-ng React dev server via the sippy-dev MCP tool sippy_ng_start + (background npm start in sippy-ng). Use when running the Sippy UI against a local + API or when the user mentions sippy_ng_start, sippy-ng dev server, or npm start + for the frontend. +--- + +# Sippy dev MCP — frontend (sippy-ng) + +**`call_mcp_tool`**: tool **`sippy_ng_start`**. Server: **`sippy-dev`** or prefixed (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `npm start` in `sippy-ng` instead. + +**`open_browser`** defaults to **`false`** (no browser launched). See **`mcp/server.py`** for all parameters. diff --git a/.cursor/skills/sippy-dev-migrate/SKILL.md b/.cursor/skills/sippy-dev-migrate/SKILL.md new file mode 100644 index 000000000..77345a55f --- /dev/null +++ b/.cursor/skills/sippy-dev-migrate/SKILL.md @@ -0,0 +1,14 @@ +--- +name: sippy-dev-migrate +description: >- + Runs Sippy PostgreSQL schema migration via the sippy-dev MCP tool migrate_db. + Use when initializing or updating the local Sippy database, fixing missing + tables (e.g. test_regressions), or when the user mentions migrate_db, sippy + migrate, or DB schema for Sippy. +--- + +# Sippy dev MCP — migrate + +**`call_mcp_tool`**: tool **`migrate_db`**. Server: **`sippy-dev`** (from **`.cursor/mcp.json`**) or Cursor’s prefixed id (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `go run ./cmd/sippy migrate` instead. + +Optional: **`database_dsn`**. Otherwise the server uses `SIPPY_DATABASE_DSN` or its default (see **`mcp/server.py`**). diff --git a/.cursor/skills/sippy-dev-regression-cache/SKILL.md b/.cursor/skills/sippy-dev-regression-cache/SKILL.md new file mode 100644 index 000000000..63f023023 --- /dev/null +++ b/.cursor/skills/sippy-dev-regression-cache/SKILL.md @@ -0,0 +1,16 @@ +--- +name: sippy-dev-regression-cache +description: >- + Runs the Sippy regression-cache loader via the sippy-dev MCP tool regression_cache + (BigQuery, Redis, component readiness cache). Use when priming regression cache, + component readiness cache, rerunning regression-cache logs under sippy-dev-logs, + or when the user mentions regression_cache or regression-cache loader for Sippy. +--- + +# Sippy dev MCP — regression-cache + +**`call_mcp_tool`**: tool **`regression_cache`**. Server: **`sippy-dev`** or prefixed (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `go run ./cmd/sippy load --loader regression-cache` instead. + +Run **`migrate_db`** first if the DB is new. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. See **`mcp/server.py`** for all parameters. diff --git a/.cursor/skills/sippy-dev-serve/SKILL.md b/.cursor/skills/sippy-dev-serve/SKILL.md new file mode 100644 index 000000000..f8ba0533e --- /dev/null +++ b/.cursor/skills/sippy-dev-serve/SKILL.md @@ -0,0 +1,13 @@ +--- +name: sippy-dev-serve +description: >- + Starts the Sippy HTTP API/UI via the sippy-dev MCP tool sippy_serve (background + go run ./cmd/sippy serve). Use when running Sippy locally for debugging, component + readiness UI, or when the user mentions sippy_serve, sippy serve, or local Sippy server. +--- + +# Sippy dev MCP — serve + +**`call_mcp_tool`**: tool **`sippy_serve`**. Server: **`sippy-dev`** or prefixed (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `go run ./cmd/sippy serve` instead. + +**`bigquery_credentials_file`**: same as regression-cache; optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. See **`mcp/server.py`** for all parameters. diff --git a/.cursor/skills/sippy-dev-tests/SKILL.md b/.cursor/skills/sippy-dev-tests/SKILL.md new file mode 100644 index 000000000..c65aca949 --- /dev/null +++ b/.cursor/skills/sippy-dev-tests/SKILL.md @@ -0,0 +1,17 @@ +--- +name: sippy-dev-tests +description: >- + Runs make lint, make test, and make e2e via three sippy-dev MCP tools (run_lint, + run_test, run_e2e) in that order. Use for the full local CI suite or when the user + mentions lint plus unit tests plus e2e for Sippy. +--- + +# Sippy dev MCP — full test suite + +Three **`call_mcp_tool`** calls, same server (**`sippy-dev`** or prefixed, e.g. **`project-0-workspace-sippy-dev`**). Order: **`run_lint`** → **`run_test`** → **`run_e2e`**. Stop if any step fails. Do not run **`make lint` / `make test` / `make e2e`** manually for this workflow. + +1. **`run_lint`** — inside the devcontainer, set **`CI=true`** (the MCP tool does this automatically) so `hack/go-lint.sh` uses the host-installed `golangci-lint` instead of spawning a nested container. +2. **`run_test`** +3. **`run_e2e`** — pass **`bigquery_credentials_file`** when cred env vars are unset (same SA JSON as other tools; sets **`GCS_SA_JSON_PATH`**). **Note:** e2e requires Podman/Docker-in-Docker support and **does not work inside the devcontainer** — run it on the host instead. + +Logs: **`sippy-dev-logs/run_lint.log`**, **`sippy-dev-logs/run_test.log`**, **`sippy-dev-logs/run_e2e.log`**. Timeouts: **`mcp/server.py`**. diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example new file mode 100644 index 000000000..80f1fa845 --- /dev/null +++ b/.devcontainer/.env.example @@ -0,0 +1,7 @@ +# Claude Code (Vertex AI auth) +CLAUDE_CODE_USE_VERTEX=1 +ANTHROPIC_VERTEX_PROJECT_ID= +CLOUD_ML_REGION=global + +# Google Cloud (BigQuery, GCS) - optional, for data loading +# GOOGLE_APPLICATION_CREDENTIALS=/workspace/path-to-service-account.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..bf37e6d19 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,56 @@ +FROM registry.access.redhat.com/ubi9/ubi:latest + +ARG GOLANGCI_LINT_VERSION=1.64.8 + +RUN dnf module enable nodejs:18 -y && \ + dnf install -y --allowerasing \ + git \ + make \ + jq \ + tar \ + gzip \ + wget \ + vim-minimal \ + findutils \ + procps-ng \ + sudo \ + go \ + npm \ + python3.12 \ + libstdc++ \ + && dnf clean all + +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "aarch64" ]; then LINT_ARCH="arm64"; else LINT_ARCH="amd64"; fi && \ + rpm -i "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${LINT_ARCH}.rpm" + +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "aarch64" ]; then GCLOUD_ARCH="arm"; else GCLOUD_ARCH="x86_64"; fi && \ + curl -fsSL "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-${GCLOUD_ARCH}.tar.gz" \ + | tar xz -C /opt && \ + /opt/google-cloud-sdk/install.sh --quiet --path-update=false && \ + ln -s /opt/google-cloud-sdk/bin/gcloud /usr/local/bin/gcloud && \ + ln -s /opt/google-cloud-sdk/bin/gsutil /usr/local/bin/gsutil && \ + ln -s /opt/google-cloud-sdk/bin/bq /usr/local/bin/bq + +RUN groupadd -g 1000 vscode && \ + useradd -u 1000 -g 1000 -m -s /bin/bash vscode && \ + echo "vscode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/vscode + +ENV GOPATH=/home/vscode/go +ENV PATH="/home/vscode/.local/bin:${GOPATH}/bin:/usr/local/go/bin:${PATH}" + +USER vscode + +RUN go install gotest.tools/gotestsum@latest + +# Cursor's install script calls base64 -D (macOS flag) but GNU coreutils uses -d +RUN sudo cp /usr/bin/base64 /usr/bin/base64.gnu && \ + printf '#!/bin/sh\nfor a in "$@"; do shift; case "$a" in -D) set -- "$@" -d;; *) set -- "$@" "$a";; esac; done\nexec /usr/bin/base64.gnu "$@"\n' | sudo tee /usr/bin/base64 > /dev/null && \ + sudo chmod +x /usr/bin/base64 + +# No versioned URL available; bust cache with CLAUDE_INSTALLER_VERSION to force a rebuild +ARG CLAUDE_INSTALLER_VERSION=latest +RUN curl -fsSL https://claude.ai/install.sh | sh + +WORKDIR /workspace diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000..92fe27f17 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,135 @@ +# Devcontainer Setup + +The devcontainer provides a full-stack development environment with Go, Node.js, +PostgreSQL, Redis, and all build tools. It runs on **Podman** and works with both +**Cursor** and **Claude Code**. + +## Prerequisites + +- [Podman](https://podman.io/) v4+ with `podman machine` running +- [devcontainer CLI](https://github.com/devcontainers/cli) — see the [official install instructions](https://github.com/devcontainers/cli#installation) (`npm install -g @devcontainers/cli`, or `brew install devcontainer` on macOS) +- For Cursor: set `"dev.containers.dockerPath": "podman"` in user settings (`Cmd+Shift+P` > "Preferences: Open User Settings (JSON)") + +## First-Time Setup + +1. Copy the env file template and fill in your values: + + ```bash + cp .devcontainer/.env.example .devcontainer/.env + # Edit .devcontainer/.env with your credentials + ``` + +2. Start the container: + + ```bash + devcontainer up --workspace-folder . --docker-path podman + ``` + + This automatically starts PostgreSQL and Redis via `init-services.sh`. + +3. Exec into the container and authenticate with GCP: + + ```bash + podman exec -it sippy-dev bash + gcloud auth application-default login + ``` + +## Starting the Container + +### For Claude Code + +```bash +# Start the container (if not already running) +devcontainer up --workspace-folder . --docker-path podman + +# Exec in and run Claude Code +podman exec -it sippy-dev bash +claude +``` + +Claude Code picks up the MCP server from `.mcp.json` at the repo root. Tool list +and usage: **[mcp/README.md](../mcp/README.md)**. + +#### Claude Code environment variables + +Claude Code uses Vertex AI for authentication. The following env vars must be set +in `.devcontainer/.env`: + +| Variable | Description | +| ----------------------------- | ------------------------------ | +| `CLAUDE_CODE_USE_VERTEX` | Set to `1` to enable Vertex AI | +| `ANTHROPIC_VERTEX_PROJECT_ID` | Your GCP project ID | +| `CLOUD_ML_REGION` | GCP region (e.g., `global`) | + +You must also run `gcloud auth application-default login` inside the container +on first use. + +### For Cursor + +```text +Cmd+Shift+P > "Dev Containers: Attach to Running Container" > sippy-dev +``` + +If the container isn't running yet, use `"Dev Containers: Reopen in Container"` +to build and start it. Cursor reads `.cursor/mcp.json` for MCP; see +**[mcp/README.md](../mcp/README.md)** for tools, logs, and credentials. + +## Services + +| Service | Container | Port | Access from devcontainer | +| ---------------- | ------------------- | ---- | ------------------------ | +| PostgreSQL | `sippy-postgres` | 5432 | `$SIPPY_DATABASE_DSN` | +| Redis | `sippy-redis` | 6379 | `$REDIS_URL` | +| Sippy API | inside devcontainer | 8080 | `http://localhost:8080` | +| React dev server | inside devcontainer | 3000 | `http://localhost:3000` | + +Ports 8080 and 3000 are published to the host, so you can access them in your +browser at `http://localhost:8080` and `http://localhost:3000`. + +## Lint and e2e inside the devcontainer + +**`make lint`** (the Go path via `hack/go-lint.sh` without `CI=true`) and **`make e2e`** +(`scripts/e2e.sh`) are **not expected to work** when you run them only inside this +devcontainer today. + +**Why:** `go-lint.sh` runs `golangci-lint` in a separate container image via +Podman/Docker. `e2e.sh` starts short-lived Postgres and Redis containers the same +way. That is **nested** container use. In many devcontainer setups (including +typical Cursor/cloud workspaces), rootless Podman inside the outer container cannot +get a working user-space network stack: **`/dev/net/tun`** is missing, so +slirp4netns/pasta fail, and alternatives such as **`--network host`** often hit +**`proc` mount / netlink** restrictions. Without nested networking, those helper +containers never start, so lint and e2e fail. + +**What works today:** run **`make lint`** / **`make e2e`** on the **host** (or any +environment where Podman or Docker can run sibling containers normally), or set +**`CI=true`** for the Go part of lint so `hack/go-lint.sh` uses a host-installed +`golangci-lint` instead of spawning an inner container. + +### TODO + +- [ ] Make **`make lint`** reliable inside the devcontainer without nested Podman + (for example: always use host `golangci-lint` when present, or document + `CI=true` as the supported in-container path). +- [ ] Make **`make e2e`** reliable inside the devcontainer without nested Podman + (for example: optional mode that uses the compose **`sippy-postgres`** / + **`sippy-redis`** services with an isolated DB name and Redis logical DB, plus + client tools in the image, or document running e2e only on the host). + +## Rebuilding + +If you change the Dockerfile or devcontainer config: + +```bash +podman rm -f sippy-dev 2>/dev/null +devcontainer up --workspace-folder . --docker-path podman --remove-existing-container +``` + +Or from Cursor: `Cmd+Shift+P` > "Dev Containers: Rebuild Container Without Cache". + +## Cleanup + +```bash +podman rm -f sippy-dev sippy-postgres sippy-redis 2>/dev/null +podman network rm sippy-net 2>/dev/null +``` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..e27b3e32c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,53 @@ +// Podman users: +// 1. In Cursor/VS Code settings, set: "dev.containers.dockerPath": "podman" +// 2. Ensure "podman machine" is running (macOS/Windows) +{ + "name": "Sippy", + "build": { + "dockerfile": "Dockerfile" + }, + "runArgs": ["--name", "sippy-dev", "--network", "sippy-net", "--env-file", ".devcontainer/.env", "-p", "8080:8080", "-p", "3000:3000"], + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + "workspaceFolder": "/workspace", + "remoteUser": "vscode", + "initializeCommand": ".devcontainer/init-services.sh", + "forwardPorts": [8080, 3000, 5432, 6379], + "portsAttributes": { + "8080": { "label": "Sippy API" }, + "3000": { "label": "React Dev Server" }, + "5432": { "label": "PostgreSQL" }, + "6379": { "label": "Redis" } + }, + "containerEnv": { + "SIPPY_DATABASE_DSN": "postgresql://postgres:password@sippy-postgres:5432/postgres", + "REDIS_URL": "redis://sippy-redis:6379" + }, + "postCreateCommand": "bash .devcontainer/post-create.sh", + "customizations": { + "vscode": { + "extensions": [ + "golang.go", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-azuretools.vscode-docker", + "redhat.vscode-yaml" + ], + "settings": { + "go.gopath": "/home/vscode/go", + "go.lintTool": "golangci-lint", + "go.lintFlags": ["--fast"], + "editor.formatOnSave": true, + "[go]": { + "editor.defaultFormatter": "golang.go" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "eslint.workingDirectories": ["sippy-ng"] + } + } + } +} diff --git a/.devcontainer/init-services.sh b/.devcontainer/init-services.sh new file mode 100755 index 000000000..d2bbe95c9 --- /dev/null +++ b/.devcontainer/init-services.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Starts PostgreSQL and Redis as standalone Podman containers (runs on the host before the devcontainer starts) + +podman network create sippy-net 2>/dev/null + +podman start sippy-postgres 2>/dev/null || \ + podman run -d --name sippy-postgres \ + --network sippy-net \ + -e POSTGRES_PASSWORD=password \ + -e POSTGRES_HOST_AUTH_METHOD=trust \ + -p 127.0.0.1:5432:5432 \ + quay.io/enterprisedb/postgresql \ + -c listen_addresses='*' + +podman start sippy-redis 2>/dev/null || \ + podman run -d --name sippy-redis \ + --network sippy-net \ + -p 127.0.0.1:6379:6379 \ + redis:7-alpine + +echo "Waiting for PostgreSQL..." +pg_ready=false +for i in $(seq 1 30); do + if podman exec sippy-postgres pg_isready -U postgres >/dev/null 2>&1; then + pg_ready=true + break + fi + sleep 1 +done +if [ "$pg_ready" = false ]; then + echo "ERROR: PostgreSQL did not become ready within 30 seconds." + exit 1 +fi + +echo "Waiting for Redis..." +redis_ready=false +for i in $(seq 1 15); do + if podman exec sippy-redis redis-cli -p 6379 PING >/dev/null 2>&1; then + redis_ready=true + break + fi + sleep 1 +done +if [ "$redis_ready" = false ]; then + echo "ERROR: Redis did not become ready within 15 seconds." + exit 1 +fi + +echo "Services ready." diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 000000000..7dbb942c0 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +echo "==> Installing Go IDE tools..." +go install golang.org/x/tools/gopls@latest +go install github.com/go-delve/delve/cmd/dlv@latest +go install honnef.co/go/tools/cmd/staticcheck@latest + +echo "==> Downloading Go module dependencies..." +go mod download + +echo "==> Installing frontend dependencies..." +cd sippy-ng +npm install --ignore-scripts +cd .. + +echo "==> Setting up MCP server venv..." +python3.12 -m venv mcp/.venv +mcp/.venv/bin/python -m pip install --upgrade pip +mcp/.venv/bin/python -m pip install -r mcp/requirements.txt + +echo "==> Checking GCP auth..." +if command -v gcloud >/dev/null 2>&1; then + if ! gcloud auth application-default print-access-token >/dev/null 2>&1; then + echo " GCP credentials not found. Run 'gcloud auth application-default login' to authenticate." + fi +else + echo " gcloud not found — skipping auth check." +fi + +echo "==> Dev environment ready." diff --git a/.gitattributes b/.gitattributes index 764c88c4a..0ae837d58 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,11 @@ .claude/** linguist-generated=true -.cursor/** linguist-generated=true +.cursor/rules/** linguist-generated=true +.cursor/mcp.json linguist-generated=false +.cursor/skills/** linguist-generated=false .gemini/** linguist-generated=true .opencode/** linguist-generated=true AGENTS.md linguist-generated=true CLAUDE.md linguist-generated=true GEMINI.md linguist-generated=true +mcp/AGENTS.md linguist-generated=true +mcp/CLAUDE.md linguist-generated=true diff --git a/.gitignore b/.gitignore index 1a04b31db..4036be693 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,12 @@ report.sh .idea/ /sippy-ng/build/* .env +.devcontainer/.env *.log +sippy-dev-logs/ +mcp/.venv/ +__pycache__/ +*.py[cod] e2e-coverage* diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 161cca07f..5de747cfb 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,3 +1,16 @@ +## Devcontainer (Recommended) + +The easiest way to get a full development environment is the +[devcontainer](.devcontainer/README.md). It provides a pre-configured +UBI9 container with Go, Node.js, PostgreSQL, Redis, and all build tools +— no manual dependency installation required. It works with both +**Cursor** and **Claude Code**, and includes a shared MCP server for +AI-callable dev tasks (migrations, loaders, local servers, tests). See +**[mcp/README.md](mcp/README.md)** for tools, configuration, and logs. + +See [.devcontainer/README.md](.devcontainer/README.md) for setup +instructions. + ## Building Sippy Running `make` will build an all-in-one binary that contains both the go app and frontend. diff --git a/Makefile b/Makefile index 33152ac4f..4585b821f 100644 --- a/Makefile +++ b/Makefile @@ -65,9 +65,9 @@ apm: uvx --from apm-cli@0.10.0 apm compile verify-apm: apm - @if ! git diff --quiet HEAD -- .claude .cursor .gemini .opencode AGENTS.md CLAUDE.md GEMINI.md sippy-ng/AGENTS.md sippy-ng/CLAUDE.md; then \ + @if ! git diff --quiet HEAD -- .claude .cursor .gemini .opencode AGENTS.md CLAUDE.md GEMINI.md sippy-ng/AGENTS.md sippy-ng/CLAUDE.md mcp/AGENTS.md mcp/CLAUDE.md; then \ echo "ERROR: Generated APM files are out of date. Run 'make apm' and commit the results."; \ - git diff --stat HEAD -- .claude .cursor .gemini .opencode AGENTS.md CLAUDE.md GEMINI.md sippy-ng/AGENTS.md sippy-ng/CLAUDE.md; \ + git diff --stat HEAD -- .claude .cursor .gemini .opencode AGENTS.md CLAUDE.md GEMINI.md sippy-ng/AGENTS.md sippy-ng/CLAUDE.md mcp/AGENTS.md mcp/CLAUDE.md; \ exit 1; \ fi diff --git a/apm.lock.yaml b/apm.lock.yaml index 7cb398789..3a2b98af1 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -9,10 +9,12 @@ local_deployed_files: - .claude/rules/backend.md - .claude/rules/frontend.md - .claude/rules/general.md +- .claude/rules/mcp.md - .claude/rules/testing.md - .cursor/rules/backend.mdc - .cursor/rules/frontend.mdc - .cursor/rules/general.mdc +- .cursor/rules/mcp.mdc - .cursor/rules/testing.mdc - .gemini/commands/sippy-generate-release-views.toml - .gemini/commands/sippy-update-ga-release-views.toml @@ -27,10 +29,12 @@ local_deployed_file_hashes: .claude/rules/backend.md: sha256:44d4119d9234bdaf026b615bc91c386cfb6930783e3b676e31ea2ec8e2ea930a .claude/rules/frontend.md: sha256:ff22046c5b951769218bbdf36499e67c70896811b8ef161ca6d3729a3423997d .claude/rules/general.md: sha256:9238d053e2d6a1703728c2bb165de93658b47c3152dfac16169ff9db9c6f716c + .claude/rules/mcp.md: sha256:73735078b3b1a8597b91b49821ba05cdb338144ea00c1eea9e9f4de38d0241a3 .claude/rules/testing.md: sha256:0a4c2f1b2484a70c1973315821ba777e7236dc187c6511973503075e0df0617d .cursor/rules/backend.mdc: sha256:b79264fe113886feaf6dc7eb7e37c31d753dba5989b34a7948fd9cdb05d4d363 .cursor/rules/frontend.mdc: sha256:497f39372724f1ae127181fe3dac9ea9a95a51c532b68ccfee6080832cf9c556 .cursor/rules/general.mdc: sha256:671e94d8251783ecfb70d16e6e8d60dc42752ecfbe90919006b908b8a751595d + .cursor/rules/mcp.mdc: sha256:268557cc3bdd9c8f7401f9ea80ac342ff2688f1d563b10880177ca586d7d30bb .cursor/rules/testing.mdc: sha256:e177fa738c0b3b607e947986ba9b0a902a9483bf34d621289f615416084b2509 .gemini/commands/sippy-generate-release-views.toml: sha256:4759f4547c05631d2e83a5859172421ea3d6ec794dd887756a90bbe0fd2732c1 .gemini/commands/sippy-update-ga-release-views.toml: sha256:71a60dfe068bdc59645b8d0069c93c65f2df30c5172da3389df2a80d86410ae9 diff --git a/mcp/AGENTS.md b/mcp/AGENTS.md new file mode 100644 index 000000000..15551ead8 --- /dev/null +++ b/mcp/AGENTS.md @@ -0,0 +1,16 @@ +# AGENTS.md + + + + + +## Files matching `mcp/**` + + +Shared MCP server for AI-callable dev tasks (migrate, serve, lint, test, e2e). Configuration, tool list, logs, and extension notes: **[README.md](../../mcp/README.md)**. + +When adding or modifying MCP tools, follow existing patterns in `server.py` (subprocess, `_repo_path`, `_ensure_dev_log_dir`, credentials helpers). Restart the MCP server after changes. + +--- +*This file was generated by APM CLI. Do not edit manually.* +*To regenerate: `specify apm compile`* diff --git a/mcp/CLAUDE.md b/mcp/CLAUDE.md new file mode 100644 index 000000000..ea4681c2c --- /dev/null +++ b/mcp/CLAUDE.md @@ -0,0 +1,17 @@ +# CLAUDE.md + + + + +# Project Standards + +## Files matching `mcp/**` + + +Shared MCP server for AI-callable dev tasks (migrate, serve, lint, test, e2e). Configuration, tool list, logs, and extension notes: **[README.md](../../mcp/README.md)**. + +When adding or modifying MCP tools, follow existing patterns in `server.py` (subprocess, `_repo_path`, `_ensure_dev_log_dir`, credentials helpers). Restart the MCP server after changes. + +--- +*This file was generated by APM CLI. Do not edit manually.* +*To regenerate: `apm compile`* diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 000000000..61456f4dd --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,76 @@ +# Sippy dev MCP server + +Python [FastMCP](https://github.com/jlowin/fastmcp) server (`server.py`) that exposes common Sippy development commands to **Cursor** and **Claude Code**. + +## Setup + +- **Virtualenv**: the devcontainer `post-create` script creates `mcp/.venv` and installs `requirements.txt`. +- **Python**: **3.10+** required (`fastmcp`). The devcontainer image installs **Python 3.12** for this venv. +- **Manual install** (from repo root): + + ```bash + python3.12 -m venv mcp/.venv + mcp/.venv/bin/python -m pip install --upgrade pip + mcp/.venv/bin/python -m pip install -r mcp/requirements.txt + ``` + +## Editor configuration + +| Client | Config file | +| ----------- | ----------------------- | +| Cursor | `.cursor/mcp.json` | +| Claude Code | `.mcp.json` (repo root) | + +Both use the same shape: run `mcp/.venv/bin/python` with argument `mcp/server.py`. The workspace folder should be the Sippy repo root so paths resolve. + +## Server id in Cursor + +The MCP server key in config is **`sippy-dev`**. Cursor may expose tools under a **prefixed** server name (e.g. `project-0-workspace-sippy-dev`). Use the server id your client lists when calling tools. + +## Tools + +Commands use the **repo root** as working directory unless noted. Most long outputs go to **`sippy-dev-logs/`** (see `.gitignore`). + +| Tool | What it runs | Default log | +| ------------------ | -------------------------------------------------------------------------------------- | ------------------------------------- | +| `migrate_db` | `go run ./cmd/sippy migrate` | `sippy-dev-logs/migrate_db.log` | +| `regression_cache` | `go run ./cmd/sippy load --loader regression-cache` (BigQuery + Redis + DB) | `sippy-dev-logs/regression_cache.log` | +| `sippy_serve` | Background `go run ./cmd/sippy serve` (API/UI, typically port **8080**) | `sippy-dev-logs/sippy_serve.log` | +| `sippy_ng_start` | Background `npm start` in `sippy-ng/` (typically port **3000**) | `sippy-dev-logs/sippy_ng_start.log` | +| `run_lint` | `make lint` (`CI=true` so `hack/go-lint.sh` runs local `golangci-lint` without Podman) | `sippy-dev-logs/run_lint.log` | +| `run_test` | `make test` (Go `gotestsum` + `sippy-ng` Jest) | `sippy-dev-logs/run_test.log` | +| `run_e2e` | `make e2e` | `sippy-dev-logs/run_e2e.log` | + +Optional parameters (timeouts, paths, DSNs, etc.) are documented on each function in **`server.py`**. + +> **Cost caution:** `run_e2e` and `regression_cache` issue BigQuery queries that cost real money. Run them only when explicitly needed and never more than once per request. + +### Credentials and environment + +- **Service account JSON** (BigQuery / GCS): pass `bigquery_credentials_file` where supported, or set **`SIPPY_BIGQUERY_CREDENTIALS_FILE`** or **`GOOGLE_APPLICATION_CREDENTIALS`** to an existing file path. Typical local file: `sippy-bigquery-job-importer-key.json` at repo root. +- **`run_e2e`** sets **`GCS_SA_JSON_PATH`** for `scripts/e2e.sh` from that same resolution. +- **Postgres / Redis**: `SIPPY_DATABASE_DSN`, `REDIS_URL`, or per-tool arguments; see `server.py` for defaults. + +### E2E containers + +`scripts/e2e.sh` uses **`DOCKER`** if set; otherwise **Podman** if on `PATH`, else **Docker**. Install one of them, or set `DOCKER` to the CLI you use. + +### Background processes + +`sippy_serve` and `sippy_ng_start` spawn detached processes. A second start is refused if a matching process is already running (see `server.py` for detection logic). + +## Cursor skills + +Agent-oriented shortcuts live under **`.cursor/skills/`**, for example: + +- `sippy-dev-migrate`, `sippy-dev-regression-cache`, `sippy-dev-serve`, `sippy-dev-frontend` +- `sippy-dev-app` (backend + frontend) +- `sippy-dev-tests` (order: `run_lint` → `run_test` → `run_e2e`) + +## Changing the server + +After editing **`server.py`**, restart the **sippy-dev** MCP server (or reload the editor) so tool lists stay in sync. + +## Adding tools + +Add `@mcp.tool()` handlers in `server.py`, mirror existing patterns (`subprocess`, `_repo_path`, `_ensure_dev_log_dir`, credentials helpers), then restart MCP. diff --git a/mcp/requirements.txt b/mcp/requirements.txt new file mode 100644 index 000000000..f8981d59b --- /dev/null +++ b/mcp/requirements.txt @@ -0,0 +1 @@ +fastmcp>=3.2.4 diff --git a/mcp/server.py b/mcp/server.py new file mode 100644 index 000000000..f7629a8dc --- /dev/null +++ b/mcp/server.py @@ -0,0 +1,583 @@ +import os +import signal +import subprocess +import sys +import time +from pathlib import Path + +from fastmcp import FastMCP + +mcp = FastMCP("sippy-dev") + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEV_LOG_DIR = REPO_ROOT / "sippy-dev-logs" + +_MAX_TOOL_CHARS = 28000 + + +def _ensure_dev_log_dir() -> None: + DEV_LOG_DIR.mkdir(parents=True, exist_ok=True) + + +def _repo_path(p: str) -> Path: + """Resolve *p* relative to REPO_ROOT and reject paths that escape it.""" + root = str(REPO_ROOT.resolve()) + sanitized = os.path.normpath(os.path.expanduser(p)) + if os.path.isabs(sanitized): + resolved = Path(sanitized).resolve() + else: + if sanitized.startswith(".."): + raise ValueError(f"path escapes repo root: {p}") + resolved = Path(os.path.normpath(os.path.join(root, sanitized))).resolve() + if not str(resolved).startswith(root + os.sep) and str(resolved) != root: + raise ValueError(f"path escapes repo root: {resolved}") + return resolved + + +def _trim(s: str, max_len: int = _MAX_TOOL_CHARS) -> str: + if len(s) <= max_len: + return s + head = max_len // 2 + tail = max_len - head - 120 + omitted = len(s) - head - tail + return f"{s[:head]}\n... [{omitted} characters omitted] ...\n{s[-tail:]}" + + +def _default_database_dsn() -> str: + return os.environ.get( + "SIPPY_DATABASE_DSN", + "postgresql://postgres:password@localhost:5432/postgres", + ) + + +def _resolve_bigquery_creds(explicit: str | None) -> tuple[Path | None, str | None]: + if explicit: + p = Path(explicit).expanduser().resolve() + if not p.is_file(): + return None, f"BigQuery credentials file not found: {p}" + return p, None + for key in ("SIPPY_BIGQUERY_CREDENTIALS_FILE", "GOOGLE_APPLICATION_CREDENTIALS"): + v = os.environ.get(key) + if v: + p = Path(v).expanduser().resolve() + if p.is_file(): + return p, None + return None, f"{key} is set but file not found: {p}" + return ( + None, + "Set bigquery_credentials_file to your GCP service account JSON path " + "(e.g. sippy-bigquery-job-importer-key.json), or set SIPPY_BIGQUERY_CREDENTIALS_FILE " + "or GOOGLE_APPLICATION_CREDENTIALS.", + ) + + +@mcp.tool() +def migrate_db(database_dsn: str | None = None) -> str: + """Run Sippy PostgreSQL migrations (``go run ./cmd/sippy migrate``). + + Uses ``database_dsn`` when provided; otherwise ``SIPPY_DATABASE_DSN`` or a localhost dev default. + Full output is written to ``sippy-dev-logs/migrate_db.log``. + """ + dsn = database_dsn or _default_database_dsn() + _ensure_dev_log_dir() + log_path = DEV_LOG_DIR / "migrate_db.log" + try: + r = subprocess.run( + ["go", "run", "./cmd/sippy", "migrate", "--database-dsn", dsn], + cwd=REPO_ROOT, + env=os.environ.copy(), + capture_output=True, + text=True, + timeout=600, + ) + except subprocess.TimeoutExpired as e: + partial = ((e.stdout or "") + (e.stderr or "")).strip() + log_path.write_text( + partial + ("\n" if partial and not partial.endswith("\n") else ""), + encoding="utf-8", + ) + out = _trim(partial) if partial else "(no output captured)" + return f"migrate timed out after 600s. log: {log_path}\n{out}" + raw = ((r.stdout or "") + (r.stderr or "")).strip() + log_path.write_text(raw + ("\n" if raw and not raw.endswith("\n") else ""), encoding="utf-8") + out = _trim(raw) + if r.returncode != 0: + return f"migrate failed (exit {r.returncode}). log: {log_path}\n{out}" + return f"migrate succeeded (exit 0). log: {log_path}\n{out}" + + +@mcp.tool() +def regression_cache( + bigquery_credentials_file: str | None = None, + database_dsn: str | None = None, + redis_url: str | None = None, + views_file: str = "config/views.yaml", + config_file: str = "config/openshift.yaml", + log_file: str = "sippy-dev-logs/regression_cache.log", + skip_matview_refresh: bool = True, + timeout_seconds: int = 7200, +) -> str: + """Run the regression-cache loader (BigQuery + Redis + DB). + + Equivalent to a line-buffered ``go run ./cmd/sippy load --loader regression-cache`` with + logging merged to ``log_file``. Typical duration is many minutes. + + ``bigquery_credentials_file`` should be a JSON key with BigQuery job permissions (e.g. + ``sippy-bigquery-job-importer-key.json``). Relative paths are resolved from the repo root. + If omitted, ``SIPPY_BIGQUERY_CREDENTIALS_FILE`` or ``GOOGLE_APPLICATION_CREDENTIALS`` must + point to an existing file. + + Other settings default from arguments or ``SIPPY_DATABASE_DSN`` / ``REDIS_URL`` environment + variables with sensible dev fallbacks. + """ + creds_path, err = _resolve_bigquery_creds(bigquery_credentials_file) + if err: + return err + + dsn = database_dsn or _default_database_dsn() + redis = redis_url or os.environ.get("REDIS_URL", "redis://localhost:6379") + try: + views = _repo_path(views_file) + config = _repo_path(config_file) + log_path = _repo_path(log_file) + except ValueError as e: + return str(e) + + for label, p in ("views", views), ("config", config): + if not p.is_file(): + return f"{label} file not found: {p}" + + args = [ + "stdbuf", + "-oL", + "-eL", + "go", + "run", + "./cmd/sippy", + "load", + "--loader", + "regression-cache", + "--views", + str(views), + "--database-dsn", + dsn, + "--redis-url", + redis, + "--config", + str(config), + "--google-service-account-credential-file", + str(creds_path), + ] + if skip_matview_refresh: + args.append("--skip-matview-refresh") + + _ensure_dev_log_dir() + log_path.parent.mkdir(parents=True, exist_ok=True) + try: + with open(log_path, "w", encoding="utf-8") as logf: + r = subprocess.run( + args, + cwd=REPO_ROOT, + env=os.environ.copy(), + stdout=logf, + stderr=subprocess.STDOUT, + timeout=timeout_seconds if timeout_seconds > 0 else None, + ) + except subprocess.TimeoutExpired: + return ( + f"regression_cache timed out after {timeout_seconds}s. " + f"Partial log: {log_path}\n" + "Increase timeout_seconds or check BigQuery / network." + ) + + try: + log_text = log_path.read_text(encoding="utf-8", errors="replace") + except OSError as e: + log_text = f"(could not read log file: {e})" + + tail_lines = log_text.splitlines()[-40:] + tail = "\n".join(tail_lines) + status = "succeeded" if r.returncode == 0 else "failed" + return ( + f"regression_cache {status} (exit {r.returncode}). " + f"Full log: {log_path}\n--- last {len(tail_lines)} lines ---\n{tail}" + ) + + +def _proc_cmdline(pid_dir: Path) -> str: + raw = (pid_dir / "cmdline").read_bytes() + return raw.replace(b"\0", b" ").decode(errors="replace") + + +def _proc_cwd(pid_dir: Path) -> Path | None: + try: + return (pid_dir / "cwd").resolve() + except OSError: + return None + + +def _pgrep_pids(pattern: str) -> list[int]: + try: + r = subprocess.run( + ["pgrep", "-f", pattern], + capture_output=True, + text=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode != 0: + return [] + return [int(x) for x in r.stdout.split() if x.strip().isdigit()] + + +def _filter_pids_by_cwd(pids: list[int], expected_cwd: Path) -> list[int]: + """Filter PIDs to those whose working directory matches expected_cwd (macOS/lsof fallback).""" + if not pids: + return [] + filtered: list[int] = [] + for pid in pids: + try: + r = subprocess.run( + ["lsof", "-a", "-p", str(pid), "-d", "cwd", "-Fn"], + capture_output=True, + text=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return pids + for line in r.stdout.splitlines(): + if line.startswith("n") and Path(line[1:]).resolve() == expected_cwd: + filtered.append(pid) + break + return filtered + + +def _pids_sippy_serve() -> list[int]: + """Processes that look like ``go run ./cmd/sippy serve`` or ``.../sippy serve`` from this repo.""" + root = REPO_ROOT.resolve() + found: list[int] = [] + if sys.platform.startswith("linux"): + for pid_dir in Path("/proc").iterdir(): + if not pid_dir.name.isdigit(): + continue + try: + if _proc_cwd(pid_dir) != root: + continue + cmd = _proc_cmdline(pid_dir) + except OSError: + continue + if " migrate" in cmd or " load" in cmd: + continue + if " serve" not in cmd and not cmd.rstrip().endswith(" serve"): + continue + if "cmd/sippy" in cmd or "exe/sippy" in cmd or "/sippy serve" in cmd: + found.append(int(pid_dir.name)) + if found: + return sorted(set(found)) + for pat in ("./cmd/sippy serve", "cmd/sippy serve", "exe/sippy serve"): + p = _filter_pids_by_cwd(_pgrep_pids(pat), root) + if p: + return sorted(set(p)) + return [] + + +def _pids_sippy_ng_dev() -> list[int]: + """Processes running CRA dev server from ``sippy-ng`` (this repo).""" + ng = (REPO_ROOT / "sippy-ng").resolve() + found: list[int] = [] + if sys.platform.startswith("linux"): + for pid_dir in Path("/proc").iterdir(): + if not pid_dir.name.isdigit(): + continue + try: + if _proc_cwd(pid_dir) != ng: + continue + cmd = _proc_cmdline(pid_dir) + except OSError: + continue + if "react-scripts" in cmd or "npm start" in cmd: + found.append(int(pid_dir.name)) + if found: + return sorted(set(found)) + p = _filter_pids_by_cwd(_pgrep_pids("react-scripts/scripts/start.js"), ng) + return sorted(set(p)) + + +@mcp.tool() +def sippy_serve( + bigquery_credentials_file: str | None = None, + database_dsn: str | None = None, + redis_url: str | None = None, + views_file: str = "config/views.yaml", + config_file: str | None = None, + log_file: str = "sippy-dev-logs/sippy_serve.log", + log_level: str = "debug", + mode: str = "ocp", + listen: str = ":8080", + enable_write_endpoints: bool = True, +) -> str: + """Start the Sippy HTTP server (``go run ./cmd/sippy serve``) in the background. + + Long-running: returns after spawn with PID, log path, and listen address. Uses the same + credential and DSN conventions as ``regression_cache``. Skips starting if a matching + ``sippy serve`` process is already running (cwd + cmdline on Linux, ``pgrep -f`` fallback). + """ + creds_path, err = _resolve_bigquery_creds(bigquery_credentials_file) + if err: + return err + + dsn = database_dsn or _default_database_dsn() + redis = redis_url or os.environ.get("REDIS_URL", "redis://localhost:6379") + try: + views = _repo_path(views_file) + log_path = _repo_path(log_file) + except ValueError as e: + return str(e) + + if not views.is_file(): + return f"views file not found: {views}" + + existing = _pids_sippy_serve() + if existing: + host_hint = f"http://127.0.0.1{listen}" if listen.startswith(":") else listen + pids = ", ".join(str(p) for p in existing) + return ( + f"sippy_serve already running (pid(s) {pids}). Listen: {host_hint} " + f"log: {log_path}" + ) + + args = [ + "stdbuf", + "-oL", + "-eL", + "go", + "run", + "./cmd/sippy", + "serve", + "--views", + str(views), + "--log-level", + log_level, + "--database-dsn", + dsn, + "--mode", + mode, + "--google-service-account-credential-file", + str(creds_path), + "--redis-url", + redis, + "--listen", + listen, + ] + if enable_write_endpoints: + args.append("--enable-write-endpoints") + if config_file: + try: + cfg = _repo_path(config_file) + except ValueError as e: + return str(e) + if not cfg.is_file(): + return f"config file not found: {cfg}" + args.extend(["--config", str(cfg)]) + + _ensure_dev_log_dir() + log_path.parent.mkdir(parents=True, exist_ok=True) + logf = open(log_path, "a", encoding="utf-8") + try: + proc = subprocess.Popen( + args, + cwd=REPO_ROOT, + env=os.environ.copy(), + stdout=logf, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + except OSError as e: + logf.close() + return f"sippy_serve failed to start: {e}" + + logf.close() + time.sleep(0.75) + code = proc.poll() + if code is not None: + try: + tail = log_path.read_text(encoding="utf-8", errors="replace")[-4000:] + except OSError: + tail = "(no log output)" + return f"sippy_serve exited immediately (exit {code}). Log: {log_path}\n--- tail ---\n{tail}" + + host_hint = f"http://127.0.0.1{listen}" if listen.startswith(":") else listen + return f"sippy_serve started (pid {proc.pid}). Listen: {host_hint} log: {log_path}" + + +@mcp.tool() +def sippy_ng_start( + log_file: str = "sippy-dev-logs/sippy_ng_start.log", + open_browser: bool = False, +) -> str: + """Start the React dev server (``npm start`` in ``sippy-ng``) in the background. + + CRA defaults to port 3000. ``log_file`` is resolved relative to the repo root; + absolute paths outside the repo are rejected. Skips starting if a matching + ``npm start`` / react-scripts process is already running for this ``sippy-ng`` tree. + """ + ng_dir = REPO_ROOT / "sippy-ng" + if not (ng_dir / "package.json").is_file(): + return f"sippy-ng not found or missing package.json: {ng_dir}" + + try: + log_path = _repo_path(log_file) + except ValueError as e: + return str(e) + + existing = _pids_sippy_ng_dev() + if existing: + pids = ", ".join(str(p) for p in existing) + return ( + f"sippy_ng_start already running (pid(s) {pids}). " + f"Typical URL: http://127.0.0.1:3000 log: {log_path}" + ) + + env = os.environ.copy() + if not open_browser: + env["BROWSER"] = "none" + + _ensure_dev_log_dir() + log_path.parent.mkdir(parents=True, exist_ok=True) + logf = open(log_path, "a", encoding="utf-8") + try: + proc = subprocess.Popen( + ["stdbuf", "-oL", "-eL", "npm", "start"], + cwd=ng_dir, + env=env, + stdout=logf, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + except OSError as e: + logf.close() + return f"sippy_ng_start failed to start: {e}" + + logf.close() + time.sleep(0.75) + code = proc.poll() + if code is not None: + try: + tail = log_path.read_text(encoding="utf-8", errors="replace")[-4000:] + except OSError: + tail = "(no log output)" + return f"sippy_ng_start exited immediately (exit {code}). Log: {log_path}\n--- tail ---\n{tail}" + + return ( + f"sippy_ng_start started (pid {proc.pid}). Typical URL: http://127.0.0.1:3000 " + f"log: {log_path}" + ) + + +def _tail_file(path: Path, max_lines: int) -> str: + try: + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError as e: + return f"(could not read log: {e})" + return "\n".join(lines[-max_lines:]) + + +def _run_make_phase( + tool_label: str, + make_target: str, + log_filename: str, + timeout_seconds: int, + env_extra: dict[str, str] | None = None, +) -> str: + """Run ``make ``; log to ``sippy-dev-logs/``.""" + _ensure_dev_log_dir() + log_path = DEV_LOG_DIR / log_filename + run_env = os.environ.copy() + if env_extra: + run_env.update(env_extra) + tout = None if timeout_seconds <= 0 else timeout_seconds + with open(log_path, "w", encoding="utf-8") as logf: + proc = subprocess.Popen( + ["make", make_target], + cwd=REPO_ROOT, + env=run_env, + stdout=logf, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + try: + returncode = proc.wait(timeout=tout) + except subprocess.TimeoutExpired: + os.killpg(proc.pid, signal.SIGTERM) + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(proc.pid, signal.SIGKILL) + proc.wait() + tail = _tail_file(log_path, 80) + return ( + f"{tool_label} timed out after {timeout_seconds}s. log: {log_path}\n" + f"--- tail ---\n{tail}" + ) + if returncode != 0: + tail = _tail_file(log_path, 80) + return ( + f"{tool_label} failed (exit {returncode}). log: {log_path}\n" + f"--- tail ---\n{tail}" + ) + tail = _tail_file(log_path, 40) + return f"{tool_label} succeeded (exit 0). log: {log_path}\n--- last lines ---\n{tail}" + + +@mcp.tool() +def run_lint(timeout_seconds: int = 1800) -> str: + """Run ``make lint`` (``CI=true`` so ``hack/go-lint.sh`` uses local ``golangci-lint``). + + Log: ``sippy-dev-logs/run_lint.log``. Use ``timeout_seconds=0`` for no limit. + """ + return _run_make_phase( + "run_lint", + "lint", + "run_lint.log", + timeout_seconds, + {"CI": "true"}, + ) + + +@mcp.tool() +def run_test(timeout_seconds: int = 7200) -> str: + """Run ``make test`` (Go packages via gotestsum and ``sippy-ng`` Jest). + + Log: ``sippy-dev-logs/run_test.log``. Use ``timeout_seconds=0`` for no limit. + """ + return _run_make_phase("run_test", "test", "run_test.log", timeout_seconds, None) + + +@mcp.tool() +def run_e2e( + bigquery_credentials_file: str | None = None, + timeout_seconds: int = 7200, +) -> str: + """Run ``make e2e`` (sets ``GCS_SA_JSON_PATH`` from the same SA JSON as other MCP tools). + + Log: ``sippy-dev-logs/run_e2e.log``. E2e is slow and uses BigQuery; use ``timeout_seconds=0`` + for no limit. + """ + creds_path, err = _resolve_bigquery_creds(bigquery_credentials_file) + if err: + return err + return _run_make_phase( + "run_e2e", + "e2e", + "run_e2e.log", + timeout_seconds, + {"GCS_SA_JSON_PATH": str(creds_path)}, + ) + + +if __name__ == "__main__": + mcp.run() From c78c9a37e379251ed217da1de31174a6a108a883 Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Wed, 29 Apr 2026 10:23:07 -0400 Subject: [PATCH 2/8] migrate cursor skills to apm --- .../instructions/dev-commands.instructions.md | 22 +++++++ .apm/prompts/sippy-dev-app.prompt.md | 12 ++++ .apm/prompts/sippy-dev-frontend.prompt.md | 11 ++++ .apm/prompts/sippy-dev-migrate.prompt.md | 17 ++++++ .../sippy-dev-regression-cache.prompt.md | 11 ++++ .apm/prompts/sippy-dev-serve.prompt.md | 11 ++++ .apm/prompts/sippy-dev-tests.prompt.md | 25 ++++++++ .claude/commands/sippy-dev-app.md | 12 ++++ .claude/commands/sippy-dev-frontend.md | 11 ++++ .claude/commands/sippy-dev-migrate.md | 17 ++++++ .../commands/sippy-dev-regression-cache.md | 11 ++++ .claude/commands/sippy-dev-serve.md | 11 ++++ .claude/commands/sippy-dev-tests.md | 25 ++++++++ .claude/rules/dev-commands.md | 22 +++++++ .cursor/rules/dev-commands.mdc | 22 +++++++ .cursor/skills/sippy-dev-app/SKILL.md | 17 ------ .cursor/skills/sippy-dev-frontend/SKILL.md | 14 ----- .cursor/skills/sippy-dev-migrate/SKILL.md | 14 ----- .../sippy-dev-regression-cache/SKILL.md | 16 ----- .cursor/skills/sippy-dev-serve/SKILL.md | 13 ---- .cursor/skills/sippy-dev-tests/SKILL.md | 17 ------ .gemini/commands/sippy-dev-app.toml | 2 + .gemini/commands/sippy-dev-frontend.toml | 2 + .gemini/commands/sippy-dev-migrate.toml | 2 + .../commands/sippy-dev-regression-cache.toml | 2 + .gemini/commands/sippy-dev-serve.toml | 2 + .gemini/commands/sippy-dev-tests.toml | 2 + .opencode/commands/sippy-dev-app.md | 12 ++++ .opencode/commands/sippy-dev-frontend.md | 11 ++++ .opencode/commands/sippy-dev-migrate.md | 17 ++++++ .../commands/sippy-dev-regression-cache.md | 11 ++++ .opencode/commands/sippy-dev-serve.md | 11 ++++ .opencode/commands/sippy-dev-tests.md | 25 ++++++++ AGENTS.md | 19 ++++++ CLAUDE.md | 19 ++++++ apm.lock.yaml | 40 +++++++++++++ mcp/README.md | 21 ++++--- mcp/server.py | 59 ------------------- 38 files changed, 427 insertions(+), 161 deletions(-) create mode 100644 .apm/instructions/dev-commands.instructions.md create mode 100644 .apm/prompts/sippy-dev-app.prompt.md create mode 100644 .apm/prompts/sippy-dev-frontend.prompt.md create mode 100644 .apm/prompts/sippy-dev-migrate.prompt.md create mode 100644 .apm/prompts/sippy-dev-regression-cache.prompt.md create mode 100644 .apm/prompts/sippy-dev-serve.prompt.md create mode 100644 .apm/prompts/sippy-dev-tests.prompt.md create mode 100644 .claude/commands/sippy-dev-app.md create mode 100644 .claude/commands/sippy-dev-frontend.md create mode 100644 .claude/commands/sippy-dev-migrate.md create mode 100644 .claude/commands/sippy-dev-regression-cache.md create mode 100644 .claude/commands/sippy-dev-serve.md create mode 100644 .claude/commands/sippy-dev-tests.md create mode 100644 .claude/rules/dev-commands.md create mode 100644 .cursor/rules/dev-commands.mdc delete mode 100644 .cursor/skills/sippy-dev-app/SKILL.md delete mode 100644 .cursor/skills/sippy-dev-frontend/SKILL.md delete mode 100644 .cursor/skills/sippy-dev-migrate/SKILL.md delete mode 100644 .cursor/skills/sippy-dev-regression-cache/SKILL.md delete mode 100644 .cursor/skills/sippy-dev-serve/SKILL.md delete mode 100644 .cursor/skills/sippy-dev-tests/SKILL.md create mode 100644 .gemini/commands/sippy-dev-app.toml create mode 100644 .gemini/commands/sippy-dev-frontend.toml create mode 100644 .gemini/commands/sippy-dev-migrate.toml create mode 100644 .gemini/commands/sippy-dev-regression-cache.toml create mode 100644 .gemini/commands/sippy-dev-serve.toml create mode 100644 .gemini/commands/sippy-dev-tests.toml create mode 100644 .opencode/commands/sippy-dev-app.md create mode 100644 .opencode/commands/sippy-dev-frontend.md create mode 100644 .opencode/commands/sippy-dev-migrate.md create mode 100644 .opencode/commands/sippy-dev-regression-cache.md create mode 100644 .opencode/commands/sippy-dev-serve.md create mode 100644 .opencode/commands/sippy-dev-tests.md diff --git a/.apm/instructions/dev-commands.instructions.md b/.apm/instructions/dev-commands.instructions.md new file mode 100644 index 000000000..af1cafd40 --- /dev/null +++ b/.apm/instructions/dev-commands.instructions.md @@ -0,0 +1,22 @@ +--- +description: "Common dev commands for database migration, linting, and testing" +applyTo: "**" +--- + +### Database migration + +Run migrations: `go run ./cmd/sippy migrate --database-dsn $SIPPY_DATABASE_DSN` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: `postgresql://postgres:password@localhost:5432/postgres` + +### Linting + +Run lint: `CI=true make lint` + +`CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +### Testing + +Run unit tests: `make test` + +This runs Go tests via gotestsum and sippy-ng Jest tests. diff --git a/.apm/prompts/sippy-dev-app.prompt.md b/.apm/prompts/sippy-dev-app.prompt.md new file mode 100644 index 000000000..096bc0010 --- /dev/null +++ b/.apm/prompts/sippy-dev-app.prompt.md @@ -0,0 +1,12 @@ +--- +description: "Start both Sippy backend and frontend dev servers" +--- + +# Sippy dev — backend + frontend + +Start the full local Sippy stack using two MCP tool calls (server: **`sippy-dev`**). Run them in order — backend first, then frontend. + +1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set. +2. **`sippy_ng_start`** + +Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is. diff --git a/.apm/prompts/sippy-dev-frontend.prompt.md b/.apm/prompts/sippy-dev-frontend.prompt.md new file mode 100644 index 000000000..074176f59 --- /dev/null +++ b/.apm/prompts/sippy-dev-frontend.prompt.md @@ -0,0 +1,11 @@ +--- +description: "Start the sippy-ng React dev server via the sippy-dev MCP tool" +--- + +# Sippy dev — frontend (sippy-ng) + +Use the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm start` in `sippy-ng` manually — the MCP tool handles background process management, log routing, and duplicate detection. + +**`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**. + +See **`mcp/server.py`** for all parameters. diff --git a/.apm/prompts/sippy-dev-migrate.prompt.md b/.apm/prompts/sippy-dev-migrate.prompt.md new file mode 100644 index 000000000..6798f8cf7 --- /dev/null +++ b/.apm/prompts/sippy-dev-migrate.prompt.md @@ -0,0 +1,17 @@ +--- +description: "Run Sippy PostgreSQL schema migration" +--- + +# Sippy dev — migrate + +Run the migration command directly: + +```bash +go run ./cmd/sippy migrate --database-dsn "$SIPPY_DATABASE_DSN" +``` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: + +```bash +go run ./cmd/sippy migrate --database-dsn "postgresql://postgres:password@localhost:5432/postgres" +``` diff --git a/.apm/prompts/sippy-dev-regression-cache.prompt.md b/.apm/prompts/sippy-dev-regression-cache.prompt.md new file mode 100644 index 000000000..243d8f9ba --- /dev/null +++ b/.apm/prompts/sippy-dev-regression-cache.prompt.md @@ -0,0 +1,11 @@ +--- +description: "Run the Sippy regression-cache loader (BigQuery + Redis + DB)" +--- + +# Sippy dev — regression-cache + +Use the **`regression_cache`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy load --loader regression-cache` manually — the MCP tool handles credentials, logging, and timeouts. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. + +See **`mcp/server.py`** for all parameters. Log: **`sippy-dev-logs/regression_cache.log`**. Typical duration is many minutes. diff --git a/.apm/prompts/sippy-dev-serve.prompt.md b/.apm/prompts/sippy-dev-serve.prompt.md new file mode 100644 index 000000000..3e9c8748d --- /dev/null +++ b/.apm/prompts/sippy-dev-serve.prompt.md @@ -0,0 +1,11 @@ +--- +description: "Start the Sippy HTTP API server via the sippy-dev MCP tool" +--- + +# Sippy dev — serve + +Use the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy serve` manually — the MCP tool handles background process management, log routing, and duplicate detection. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. + +See **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**. diff --git a/.apm/prompts/sippy-dev-tests.prompt.md b/.apm/prompts/sippy-dev-tests.prompt.md new file mode 100644 index 000000000..b24b50c7e --- /dev/null +++ b/.apm/prompts/sippy-dev-tests.prompt.md @@ -0,0 +1,25 @@ +--- +description: "Run lint, unit tests, and e2e in order (full local CI suite)" +--- + +# Sippy dev — full test suite + +Run these three steps in order. Stop if any step fails. + +1. **Lint** — run directly: + + ```bash + CI=true make lint + ``` + + `CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +2. **Unit tests** — run directly: + + ```bash + make test + ``` + +3. **E2e** — use the **`run_e2e`** MCP tool (server: **`sippy-dev`**). Pass **`bigquery_credentials_file`** when cred env vars are unset. E2e requires Podman/Docker and **does not work inside the devcontainer** — run it on the host. + +E2e log: **`sippy-dev-logs/run_e2e.log`**. Timeouts: **`mcp/server.py`**. diff --git a/.claude/commands/sippy-dev-app.md b/.claude/commands/sippy-dev-app.md new file mode 100644 index 000000000..2cb9d712e --- /dev/null +++ b/.claude/commands/sippy-dev-app.md @@ -0,0 +1,12 @@ +--- +description: Start both Sippy backend and frontend dev servers +--- + +# Sippy dev — backend + frontend + +Start the full local Sippy stack using two MCP tool calls (server: **`sippy-dev`**). Run them in order — backend first, then frontend. + +1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set. +2. **`sippy_ng_start`** + +Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is. \ No newline at end of file diff --git a/.claude/commands/sippy-dev-frontend.md b/.claude/commands/sippy-dev-frontend.md new file mode 100644 index 000000000..1f8912469 --- /dev/null +++ b/.claude/commands/sippy-dev-frontend.md @@ -0,0 +1,11 @@ +--- +description: Start the sippy-ng React dev server via the sippy-dev MCP tool +--- + +# Sippy dev — frontend (sippy-ng) + +Use the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm start` in `sippy-ng` manually — the MCP tool handles background process management, log routing, and duplicate detection. + +**`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**. + +See **`mcp/server.py`** for all parameters. \ No newline at end of file diff --git a/.claude/commands/sippy-dev-migrate.md b/.claude/commands/sippy-dev-migrate.md new file mode 100644 index 000000000..2430ba319 --- /dev/null +++ b/.claude/commands/sippy-dev-migrate.md @@ -0,0 +1,17 @@ +--- +description: Run Sippy PostgreSQL schema migration +--- + +# Sippy dev — migrate + +Run the migration command directly: + +```bash +go run ./cmd/sippy migrate --database-dsn "$SIPPY_DATABASE_DSN" +``` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: + +```bash +go run ./cmd/sippy migrate --database-dsn "postgresql://postgres:password@localhost:5432/postgres" +``` \ No newline at end of file diff --git a/.claude/commands/sippy-dev-regression-cache.md b/.claude/commands/sippy-dev-regression-cache.md new file mode 100644 index 000000000..0ca7f4d75 --- /dev/null +++ b/.claude/commands/sippy-dev-regression-cache.md @@ -0,0 +1,11 @@ +--- +description: Run the Sippy regression-cache loader (BigQuery + Redis + DB) +--- + +# Sippy dev — regression-cache + +Use the **`regression_cache`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy load --loader regression-cache` manually — the MCP tool handles credentials, logging, and timeouts. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. + +See **`mcp/server.py`** for all parameters. Log: **`sippy-dev-logs/regression_cache.log`**. Typical duration is many minutes. \ No newline at end of file diff --git a/.claude/commands/sippy-dev-serve.md b/.claude/commands/sippy-dev-serve.md new file mode 100644 index 000000000..680343d81 --- /dev/null +++ b/.claude/commands/sippy-dev-serve.md @@ -0,0 +1,11 @@ +--- +description: Start the Sippy HTTP API server via the sippy-dev MCP tool +--- + +# Sippy dev — serve + +Use the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy serve` manually — the MCP tool handles background process management, log routing, and duplicate detection. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. + +See **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**. \ No newline at end of file diff --git a/.claude/commands/sippy-dev-tests.md b/.claude/commands/sippy-dev-tests.md new file mode 100644 index 000000000..46eaf431b --- /dev/null +++ b/.claude/commands/sippy-dev-tests.md @@ -0,0 +1,25 @@ +--- +description: Run lint, unit tests, and e2e in order (full local CI suite) +--- + +# Sippy dev — full test suite + +Run these three steps in order. Stop if any step fails. + +1. **Lint** — run directly: + + ```bash + CI=true make lint + ``` + + `CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +2. **Unit tests** — run directly: + + ```bash + make test + ``` + +3. **E2e** — use the **`run_e2e`** MCP tool (server: **`sippy-dev`**). Pass **`bigquery_credentials_file`** when cred env vars are unset. E2e requires Podman/Docker and **does not work inside the devcontainer** — run it on the host. + +E2e log: **`sippy-dev-logs/run_e2e.log`**. Timeouts: **`mcp/server.py`**. \ No newline at end of file diff --git a/.claude/rules/dev-commands.md b/.claude/rules/dev-commands.md new file mode 100644 index 000000000..9f5e1eb2a --- /dev/null +++ b/.claude/rules/dev-commands.md @@ -0,0 +1,22 @@ +--- +paths: + - "**" +--- + +### Database migration + +Run migrations: `go run ./cmd/sippy migrate --database-dsn $SIPPY_DATABASE_DSN` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: `postgresql://postgres:password@localhost:5432/postgres` + +### Linting + +Run lint: `CI=true make lint` + +`CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +### Testing + +Run unit tests: `make test` + +This runs Go tests via gotestsum and sippy-ng Jest tests. diff --git a/.cursor/rules/dev-commands.mdc b/.cursor/rules/dev-commands.mdc new file mode 100644 index 000000000..68583ab44 --- /dev/null +++ b/.cursor/rules/dev-commands.mdc @@ -0,0 +1,22 @@ +--- +description: Common dev commands for database migration, linting, and testing +globs: "**" +--- + +### Database migration + +Run migrations: `go run ./cmd/sippy migrate --database-dsn $SIPPY_DATABASE_DSN` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: `postgresql://postgres:password@localhost:5432/postgres` + +### Linting + +Run lint: `CI=true make lint` + +`CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +### Testing + +Run unit tests: `make test` + +This runs Go tests via gotestsum and sippy-ng Jest tests. diff --git a/.cursor/skills/sippy-dev-app/SKILL.md b/.cursor/skills/sippy-dev-app/SKILL.md deleted file mode 100644 index 97bda60ef..000000000 --- a/.cursor/skills/sippy-dev-app/SKILL.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: sippy-dev-app -description: >- - Starts the local Sippy stack by calling sippy-dev MCP tools sippy_serve then - sippy_ng_start (backend then frontend). Use when the user wants both API/UI dev - servers, full local Sippy, or a single workflow instead of separate serve and - frontend steps. ---- - -# Sippy dev MCP — backend + frontend - -Two **`call_mcp_tool`** calls, same server (**`sippy-dev`** or prefixed, e.g. **`project-0-workspace-sippy-dev`**). Do not use shell **`go run ./cmd/sippy serve`** or **`npm start`**. - -1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set (see **`mcp/server.py`** / **sippy-dev-serve**). -2. **`sippy_ng_start`** - -Backend first, then frontend. Each tool returns listen hints (typically **8080** / **3000**) and log/pid paths; if a tool says already running, leave that process as-is. diff --git a/.cursor/skills/sippy-dev-frontend/SKILL.md b/.cursor/skills/sippy-dev-frontend/SKILL.md deleted file mode 100644 index e87467dd0..000000000 --- a/.cursor/skills/sippy-dev-frontend/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: sippy-dev-frontend -description: >- - Starts the sippy-ng React dev server via the sippy-dev MCP tool sippy_ng_start - (background npm start in sippy-ng). Use when running the Sippy UI against a local - API or when the user mentions sippy_ng_start, sippy-ng dev server, or npm start - for the frontend. ---- - -# Sippy dev MCP — frontend (sippy-ng) - -**`call_mcp_tool`**: tool **`sippy_ng_start`**. Server: **`sippy-dev`** or prefixed (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `npm start` in `sippy-ng` instead. - -**`open_browser`** defaults to **`false`** (no browser launched). See **`mcp/server.py`** for all parameters. diff --git a/.cursor/skills/sippy-dev-migrate/SKILL.md b/.cursor/skills/sippy-dev-migrate/SKILL.md deleted file mode 100644 index 77345a55f..000000000 --- a/.cursor/skills/sippy-dev-migrate/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: sippy-dev-migrate -description: >- - Runs Sippy PostgreSQL schema migration via the sippy-dev MCP tool migrate_db. - Use when initializing or updating the local Sippy database, fixing missing - tables (e.g. test_regressions), or when the user mentions migrate_db, sippy - migrate, or DB schema for Sippy. ---- - -# Sippy dev MCP — migrate - -**`call_mcp_tool`**: tool **`migrate_db`**. Server: **`sippy-dev`** (from **`.cursor/mcp.json`**) or Cursor’s prefixed id (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `go run ./cmd/sippy migrate` instead. - -Optional: **`database_dsn`**. Otherwise the server uses `SIPPY_DATABASE_DSN` or its default (see **`mcp/server.py`**). diff --git a/.cursor/skills/sippy-dev-regression-cache/SKILL.md b/.cursor/skills/sippy-dev-regression-cache/SKILL.md deleted file mode 100644 index 63f023023..000000000 --- a/.cursor/skills/sippy-dev-regression-cache/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: sippy-dev-regression-cache -description: >- - Runs the Sippy regression-cache loader via the sippy-dev MCP tool regression_cache - (BigQuery, Redis, component readiness cache). Use when priming regression cache, - component readiness cache, rerunning regression-cache logs under sippy-dev-logs, - or when the user mentions regression_cache or regression-cache loader for Sippy. ---- - -# Sippy dev MCP — regression-cache - -**`call_mcp_tool`**: tool **`regression_cache`**. Server: **`sippy-dev`** or prefixed (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `go run ./cmd/sippy load --loader regression-cache` instead. - -Run **`migrate_db`** first if the DB is new. - -**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. See **`mcp/server.py`** for all parameters. diff --git a/.cursor/skills/sippy-dev-serve/SKILL.md b/.cursor/skills/sippy-dev-serve/SKILL.md deleted file mode 100644 index f8ba0533e..000000000 --- a/.cursor/skills/sippy-dev-serve/SKILL.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: sippy-dev-serve -description: >- - Starts the Sippy HTTP API/UI via the sippy-dev MCP tool sippy_serve (background - go run ./cmd/sippy serve). Use when running Sippy locally for debugging, component - readiness UI, or when the user mentions sippy_serve, sippy serve, or local Sippy server. ---- - -# Sippy dev MCP — serve - -**`call_mcp_tool`**: tool **`sippy_serve`**. Server: **`sippy-dev`** or prefixed (e.g. **`project-0-workspace-sippy-dev`**). Do not use shell `go run ./cmd/sippy serve` instead. - -**`bigquery_credentials_file`**: same as regression-cache; optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. See **`mcp/server.py`** for all parameters. diff --git a/.cursor/skills/sippy-dev-tests/SKILL.md b/.cursor/skills/sippy-dev-tests/SKILL.md deleted file mode 100644 index c65aca949..000000000 --- a/.cursor/skills/sippy-dev-tests/SKILL.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: sippy-dev-tests -description: >- - Runs make lint, make test, and make e2e via three sippy-dev MCP tools (run_lint, - run_test, run_e2e) in that order. Use for the full local CI suite or when the user - mentions lint plus unit tests plus e2e for Sippy. ---- - -# Sippy dev MCP — full test suite - -Three **`call_mcp_tool`** calls, same server (**`sippy-dev`** or prefixed, e.g. **`project-0-workspace-sippy-dev`**). Order: **`run_lint`** → **`run_test`** → **`run_e2e`**. Stop if any step fails. Do not run **`make lint` / `make test` / `make e2e`** manually for this workflow. - -1. **`run_lint`** — inside the devcontainer, set **`CI=true`** (the MCP tool does this automatically) so `hack/go-lint.sh` uses the host-installed `golangci-lint` instead of spawning a nested container. -2. **`run_test`** -3. **`run_e2e`** — pass **`bigquery_credentials_file`** when cred env vars are unset (same SA JSON as other tools; sets **`GCS_SA_JSON_PATH`**). **Note:** e2e requires Podman/Docker-in-Docker support and **does not work inside the devcontainer** — run it on the host instead. - -Logs: **`sippy-dev-logs/run_lint.log`**, **`sippy-dev-logs/run_test.log`**, **`sippy-dev-logs/run_e2e.log`**. Timeouts: **`mcp/server.py`**. diff --git a/.gemini/commands/sippy-dev-app.toml b/.gemini/commands/sippy-dev-app.toml new file mode 100644 index 000000000..da2ec81dd --- /dev/null +++ b/.gemini/commands/sippy-dev-app.toml @@ -0,0 +1,2 @@ +description = "Start both Sippy backend and frontend dev servers" +prompt = "# Sippy dev — backend + frontend\n\nStart the full local Sippy stack using two MCP tool calls (server: **`sippy-dev`**). Run them in order — backend first, then frontend.\n\n1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set.\n2. **`sippy_ng_start`**\n\nEach tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is." diff --git a/.gemini/commands/sippy-dev-frontend.toml b/.gemini/commands/sippy-dev-frontend.toml new file mode 100644 index 000000000..fe676fe77 --- /dev/null +++ b/.gemini/commands/sippy-dev-frontend.toml @@ -0,0 +1,2 @@ +description = "Start the sippy-ng React dev server via the sippy-dev MCP tool" +prompt = "# Sippy dev — frontend (sippy-ng)\n\nUse the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm start` in `sippy-ng` manually — the MCP tool handles background process management, log routing, and duplicate detection.\n\n**`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**.\n\nSee **`mcp/server.py`** for all parameters." diff --git a/.gemini/commands/sippy-dev-migrate.toml b/.gemini/commands/sippy-dev-migrate.toml new file mode 100644 index 000000000..2000275c3 --- /dev/null +++ b/.gemini/commands/sippy-dev-migrate.toml @@ -0,0 +1,2 @@ +description = "Run Sippy PostgreSQL schema migration" +prompt = "# Sippy dev — migrate\n\nRun the migration command directly:\n\n```bash\ngo run ./cmd/sippy migrate --database-dsn \"$SIPPY_DATABASE_DSN\"\n```\n\nIf `SIPPY_DATABASE_DSN` is not set, use the dev default:\n\n```bash\ngo run ./cmd/sippy migrate --database-dsn \"postgresql://postgres:password@localhost:5432/postgres\"\n```" diff --git a/.gemini/commands/sippy-dev-regression-cache.toml b/.gemini/commands/sippy-dev-regression-cache.toml new file mode 100644 index 000000000..9a1fc6af9 --- /dev/null +++ b/.gemini/commands/sippy-dev-regression-cache.toml @@ -0,0 +1,2 @@ +description = "Run the Sippy regression-cache loader (BigQuery + Redis + DB)" +prompt = "# Sippy dev — regression-cache\n\nUse the **`regression_cache`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy load --loader regression-cache` manually — the MCP tool handles credentials, logging, and timeouts.\n\n**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set.\n\nSee **`mcp/server.py`** for all parameters. Log: **`sippy-dev-logs/regression_cache.log`**. Typical duration is many minutes." diff --git a/.gemini/commands/sippy-dev-serve.toml b/.gemini/commands/sippy-dev-serve.toml new file mode 100644 index 000000000..72a296fff --- /dev/null +++ b/.gemini/commands/sippy-dev-serve.toml @@ -0,0 +1,2 @@ +description = "Start the Sippy HTTP API server via the sippy-dev MCP tool" +prompt = "# Sippy dev — serve\n\nUse the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy serve` manually — the MCP tool handles background process management, log routing, and duplicate detection.\n\n**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set.\n\nSee **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**." diff --git a/.gemini/commands/sippy-dev-tests.toml b/.gemini/commands/sippy-dev-tests.toml new file mode 100644 index 000000000..c78fcc213 --- /dev/null +++ b/.gemini/commands/sippy-dev-tests.toml @@ -0,0 +1,2 @@ +description = "Run lint, unit tests, and e2e in order (full local CI suite)" +prompt = "# Sippy dev — full test suite\n\nRun these three steps in order. Stop if any step fails.\n\n1. **Lint** — run directly:\n\n ```bash\n CI=true make lint\n ```\n\n `CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container.\n\n2. **Unit tests** — run directly:\n\n ```bash\n make test\n ```\n\n3. **E2e** — use the **`run_e2e`** MCP tool (server: **`sippy-dev`**). Pass **`bigquery_credentials_file`** when cred env vars are unset. E2e requires Podman/Docker and **does not work inside the devcontainer** — run it on the host.\n\nE2e log: **`sippy-dev-logs/run_e2e.log`**. Timeouts: **`mcp/server.py`**." diff --git a/.opencode/commands/sippy-dev-app.md b/.opencode/commands/sippy-dev-app.md new file mode 100644 index 000000000..2cb9d712e --- /dev/null +++ b/.opencode/commands/sippy-dev-app.md @@ -0,0 +1,12 @@ +--- +description: Start both Sippy backend and frontend dev servers +--- + +# Sippy dev — backend + frontend + +Start the full local Sippy stack using two MCP tool calls (server: **`sippy-dev`**). Run them in order — backend first, then frontend. + +1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set. +2. **`sippy_ng_start`** + +Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is. \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-frontend.md b/.opencode/commands/sippy-dev-frontend.md new file mode 100644 index 000000000..1f8912469 --- /dev/null +++ b/.opencode/commands/sippy-dev-frontend.md @@ -0,0 +1,11 @@ +--- +description: Start the sippy-ng React dev server via the sippy-dev MCP tool +--- + +# Sippy dev — frontend (sippy-ng) + +Use the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm start` in `sippy-ng` manually — the MCP tool handles background process management, log routing, and duplicate detection. + +**`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**. + +See **`mcp/server.py`** for all parameters. \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-migrate.md b/.opencode/commands/sippy-dev-migrate.md new file mode 100644 index 000000000..2430ba319 --- /dev/null +++ b/.opencode/commands/sippy-dev-migrate.md @@ -0,0 +1,17 @@ +--- +description: Run Sippy PostgreSQL schema migration +--- + +# Sippy dev — migrate + +Run the migration command directly: + +```bash +go run ./cmd/sippy migrate --database-dsn "$SIPPY_DATABASE_DSN" +``` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: + +```bash +go run ./cmd/sippy migrate --database-dsn "postgresql://postgres:password@localhost:5432/postgres" +``` \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-regression-cache.md b/.opencode/commands/sippy-dev-regression-cache.md new file mode 100644 index 000000000..0ca7f4d75 --- /dev/null +++ b/.opencode/commands/sippy-dev-regression-cache.md @@ -0,0 +1,11 @@ +--- +description: Run the Sippy regression-cache loader (BigQuery + Redis + DB) +--- + +# Sippy dev — regression-cache + +Use the **`regression_cache`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy load --loader regression-cache` manually — the MCP tool handles credentials, logging, and timeouts. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. + +See **`mcp/server.py`** for all parameters. Log: **`sippy-dev-logs/regression_cache.log`**. Typical duration is many minutes. \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-serve.md b/.opencode/commands/sippy-dev-serve.md new file mode 100644 index 000000000..680343d81 --- /dev/null +++ b/.opencode/commands/sippy-dev-serve.md @@ -0,0 +1,11 @@ +--- +description: Start the Sippy HTTP API server via the sippy-dev MCP tool +--- + +# Sippy dev — serve + +Use the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy serve` manually — the MCP tool handles background process management, log routing, and duplicate detection. + +**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. + +See **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**. \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-tests.md b/.opencode/commands/sippy-dev-tests.md new file mode 100644 index 000000000..46eaf431b --- /dev/null +++ b/.opencode/commands/sippy-dev-tests.md @@ -0,0 +1,25 @@ +--- +description: Run lint, unit tests, and e2e in order (full local CI suite) +--- + +# Sippy dev — full test suite + +Run these three steps in order. Stop if any step fails. + +1. **Lint** — run directly: + + ```bash + CI=true make lint + ``` + + `CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +2. **Unit tests** — run directly: + + ```bash + make test + ``` + +3. **E2e** — use the **`run_e2e`** MCP tool (server: **`sippy-dev`**). Pass **`bigquery_credentials_file`** when cred env vars are unset. E2e requires Podman/Docker and **does not work inside the devcontainer** — run it on the host. + +E2e log: **`sippy-dev-logs/run_e2e.log`**. Timeouts: **`mcp/server.py`**. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 33c159e76..d6c2ddbbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,25 @@ ## Files matching `**` + +### Database migration + +Run migrations: `go run ./cmd/sippy migrate --database-dsn $SIPPY_DATABASE_DSN` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: `postgresql://postgres:password@localhost:5432/postgres` + +### Linting + +Run lint: `CI=true make lint` + +`CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +### Testing + +Run unit tests: `make test` + +This runs Go tests via gotestsum and sippy-ng Jest tests. + **Sippy (CIPI - Continuous Integration Private Investigator)** is a tool used within the OpenShift engineering organization to analyze CI job results. Its primary goals are to: diff --git a/CLAUDE.md b/CLAUDE.md index 03536901c..452020de3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,25 @@ ## Files matching `**` + +### Database migration + +Run migrations: `go run ./cmd/sippy migrate --database-dsn $SIPPY_DATABASE_DSN` + +If `SIPPY_DATABASE_DSN` is not set, use the dev default: `postgresql://postgres:password@localhost:5432/postgres` + +### Linting + +Run lint: `CI=true make lint` + +`CI=true` makes `hack/go-lint.sh` use the locally installed `golangci-lint` instead of spawning a container. + +### Testing + +Run unit tests: `make test` + +This runs Go tests via gotestsum and sippy-ng Jest tests. + **Sippy (CIPI - Continuous Integration Private Investigator)** is a tool used within the OpenShift engineering organization to analyze CI job results. Its primary goals are to: diff --git a/apm.lock.yaml b/apm.lock.yaml index 3a2b98af1..438a29952 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -3,42 +3,82 @@ generated_at: '2026-04-27T13:58:06.737227+00:00' apm_version: 0.10.0 dependencies: [] local_deployed_files: +- .claude/commands/sippy-dev-app.md +- .claude/commands/sippy-dev-frontend.md +- .claude/commands/sippy-dev-migrate.md +- .claude/commands/sippy-dev-regression-cache.md +- .claude/commands/sippy-dev-serve.md +- .claude/commands/sippy-dev-tests.md - .claude/commands/sippy-generate-release-views.md - .claude/commands/sippy-update-ga-release-views.md - .claude/commands/sippy-update-job-variant.md - .claude/rules/backend.md +- .claude/rules/dev-commands.md - .claude/rules/frontend.md - .claude/rules/general.md - .claude/rules/mcp.md - .claude/rules/testing.md - .cursor/rules/backend.mdc +- .cursor/rules/dev-commands.mdc - .cursor/rules/frontend.mdc - .cursor/rules/general.mdc - .cursor/rules/mcp.mdc - .cursor/rules/testing.mdc +- .gemini/commands/sippy-dev-app.toml +- .gemini/commands/sippy-dev-frontend.toml +- .gemini/commands/sippy-dev-migrate.toml +- .gemini/commands/sippy-dev-regression-cache.toml +- .gemini/commands/sippy-dev-serve.toml +- .gemini/commands/sippy-dev-tests.toml - .gemini/commands/sippy-generate-release-views.toml - .gemini/commands/sippy-update-ga-release-views.toml - .gemini/commands/sippy-update-job-variant.toml +- .opencode/commands/sippy-dev-app.md +- .opencode/commands/sippy-dev-frontend.md +- .opencode/commands/sippy-dev-migrate.md +- .opencode/commands/sippy-dev-regression-cache.md +- .opencode/commands/sippy-dev-serve.md +- .opencode/commands/sippy-dev-tests.md - .opencode/commands/sippy-generate-release-views.md - .opencode/commands/sippy-update-ga-release-views.md - .opencode/commands/sippy-update-job-variant.md local_deployed_file_hashes: + .claude/commands/sippy-dev-app.md: sha256:0b4837a545b7633d8b53f891aea808b59ddf49815835c6ed978212635bbc7670 + .claude/commands/sippy-dev-frontend.md: sha256:67ff3997d3f9728e318bc8d35d187b922fcb979c6bb2723bf4bf2a0462c9975e + .claude/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b + .claude/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 + .claude/commands/sippy-dev-serve.md: sha256:bb7f6d87d147b437e761e4f7be854e5c7f825d741472b3aee48812e2379e20ca + .claude/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .claude/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .claude/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 .claude/commands/sippy-update-job-variant.md: sha256:ef00ec3a3b01556b33243d3e17ab1ace23879a8ef10dfd2e501a0fda2291fb97 .claude/rules/backend.md: sha256:44d4119d9234bdaf026b615bc91c386cfb6930783e3b676e31ea2ec8e2ea930a + .claude/rules/dev-commands.md: sha256:4508eaad3d835305b68106ed5b270ced97c93e4a42ee94d1bcac5ea7b056bc5d .claude/rules/frontend.md: sha256:ff22046c5b951769218bbdf36499e67c70896811b8ef161ca6d3729a3423997d .claude/rules/general.md: sha256:9238d053e2d6a1703728c2bb165de93658b47c3152dfac16169ff9db9c6f716c .claude/rules/mcp.md: sha256:73735078b3b1a8597b91b49821ba05cdb338144ea00c1eea9e9f4de38d0241a3 .claude/rules/testing.md: sha256:0a4c2f1b2484a70c1973315821ba777e7236dc187c6511973503075e0df0617d .cursor/rules/backend.mdc: sha256:b79264fe113886feaf6dc7eb7e37c31d753dba5989b34a7948fd9cdb05d4d363 + .cursor/rules/dev-commands.mdc: sha256:c69ca93db9b6c68f9aeb6567289dc5baef805520e8636a1e3e848793f67450e6 .cursor/rules/frontend.mdc: sha256:497f39372724f1ae127181fe3dac9ea9a95a51c532b68ccfee6080832cf9c556 .cursor/rules/general.mdc: sha256:671e94d8251783ecfb70d16e6e8d60dc42752ecfbe90919006b908b8a751595d .cursor/rules/mcp.mdc: sha256:268557cc3bdd9c8f7401f9ea80ac342ff2688f1d563b10880177ca586d7d30bb .cursor/rules/testing.mdc: sha256:e177fa738c0b3b607e947986ba9b0a902a9483bf34d621289f615416084b2509 + .gemini/commands/sippy-dev-app.toml: sha256:c893af083c3fc78fc05b5866edc6cc9ccae61183c1c56bf7e99fbd347c276387 + .gemini/commands/sippy-dev-frontend.toml: sha256:e6c6bf80fe01e2422aaee53f2b1e61d889277e8db0274c8e881e8061be53f97d + .gemini/commands/sippy-dev-migrate.toml: sha256:e699558eb27c294327a092bfcd3e76b576087ad29d9a3a9b000cfd9e7c6b4774 + .gemini/commands/sippy-dev-regression-cache.toml: sha256:cd2c9017e5e976750839c712b42d10046d1d04fabaa749ffda6fd1abaf776d7a + .gemini/commands/sippy-dev-serve.toml: sha256:56b555609652edf23982c8154f8edb42e643ee5c114580ff571134fddb1df72e + .gemini/commands/sippy-dev-tests.toml: sha256:0d19aaff911d478859fc948650a92e46142071a8c5d1a29c5b40c25d1d1e495b .gemini/commands/sippy-generate-release-views.toml: sha256:4759f4547c05631d2e83a5859172421ea3d6ec794dd887756a90bbe0fd2732c1 .gemini/commands/sippy-update-ga-release-views.toml: sha256:71a60dfe068bdc59645b8d0069c93c65f2df30c5172da3389df2a80d86410ae9 .gemini/commands/sippy-update-job-variant.toml: sha256:71c9c7548a1a4b829e970d76504ef981ea05fecf8052cdb86859d96ad52cb2b2 + .opencode/commands/sippy-dev-app.md: sha256:0b4837a545b7633d8b53f891aea808b59ddf49815835c6ed978212635bbc7670 + .opencode/commands/sippy-dev-frontend.md: sha256:67ff3997d3f9728e318bc8d35d187b922fcb979c6bb2723bf4bf2a0462c9975e + .opencode/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b + .opencode/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 + .opencode/commands/sippy-dev-serve.md: sha256:bb7f6d87d147b437e761e4f7be854e5c7f825d741472b3aee48812e2379e20ca + .opencode/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .opencode/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .opencode/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 .opencode/commands/sippy-update-job-variant.md: sha256:ef00ec3a3b01556b33243d3e17ab1ace23879a8ef10dfd2e501a0fda2291fb97 diff --git a/mcp/README.md b/mcp/README.md index 61456f4dd..e1bb1f846 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -1,6 +1,8 @@ # Sippy dev MCP server -Python [FastMCP](https://github.com/jlowin/fastmcp) server (`server.py`) that exposes common Sippy development commands to **Cursor** and **Claude Code**. +Python [FastMCP](https://github.com/jlowin/fastmcp) server (`server.py`) that exposes Sippy development commands requiring process management to **Cursor** and **Claude Code**. + +Simple commands (migrate, lint, test) are documented in project instructions via [APM](../apm.yml) and can be run directly — see `.apm/instructions/dev-commands.instructions.md`. ## Setup @@ -31,15 +33,12 @@ The MCP server key in config is **`sippy-dev`**. Cursor may expose tools under a Commands use the **repo root** as working directory unless noted. Most long outputs go to **`sippy-dev-logs/`** (see `.gitignore`). -| Tool | What it runs | Default log | -| ------------------ | -------------------------------------------------------------------------------------- | ------------------------------------- | -| `migrate_db` | `go run ./cmd/sippy migrate` | `sippy-dev-logs/migrate_db.log` | -| `regression_cache` | `go run ./cmd/sippy load --loader regression-cache` (BigQuery + Redis + DB) | `sippy-dev-logs/regression_cache.log` | -| `sippy_serve` | Background `go run ./cmd/sippy serve` (API/UI, typically port **8080**) | `sippy-dev-logs/sippy_serve.log` | -| `sippy_ng_start` | Background `npm start` in `sippy-ng/` (typically port **3000**) | `sippy-dev-logs/sippy_ng_start.log` | -| `run_lint` | `make lint` (`CI=true` so `hack/go-lint.sh` runs local `golangci-lint` without Podman) | `sippy-dev-logs/run_lint.log` | -| `run_test` | `make test` (Go `gotestsum` + `sippy-ng` Jest) | `sippy-dev-logs/run_test.log` | -| `run_e2e` | `make e2e` | `sippy-dev-logs/run_e2e.log` | +| Tool | What it runs | Default log | +| ------------------ | --------------------------------------------------------------------------- | ------------------------------------- | +| `regression_cache` | `go run ./cmd/sippy load --loader regression-cache` (BigQuery + Redis + DB) | `sippy-dev-logs/regression_cache.log` | +| `sippy_serve` | Background `go run ./cmd/sippy serve` (API/UI, typically port **8080**) | `sippy-dev-logs/sippy_serve.log` | +| `sippy_ng_start` | Background `npm start` in `sippy-ng/` (typically port **3000**) | `sippy-dev-logs/sippy_ng_start.log` | +| `run_e2e` | `make e2e` | `sippy-dev-logs/run_e2e.log` | Optional parameters (timeouts, paths, DSNs, etc.) are documented on each function in **`server.py`**. @@ -65,7 +64,7 @@ Agent-oriented shortcuts live under **`.cursor/skills/`**, for example: - `sippy-dev-migrate`, `sippy-dev-regression-cache`, `sippy-dev-serve`, `sippy-dev-frontend` - `sippy-dev-app` (backend + frontend) -- `sippy-dev-tests` (order: `run_lint` → `run_test` → `run_e2e`) +- `sippy-dev-tests` (order: `CI=true make lint` → `make test` → `run_e2e`) ## Changing the server diff --git a/mcp/server.py b/mcp/server.py index f7629a8dc..1fdcbbcc5 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -71,41 +71,6 @@ def _resolve_bigquery_creds(explicit: str | None) -> tuple[Path | None, str | No ) -@mcp.tool() -def migrate_db(database_dsn: str | None = None) -> str: - """Run Sippy PostgreSQL migrations (``go run ./cmd/sippy migrate``). - - Uses ``database_dsn`` when provided; otherwise ``SIPPY_DATABASE_DSN`` or a localhost dev default. - Full output is written to ``sippy-dev-logs/migrate_db.log``. - """ - dsn = database_dsn or _default_database_dsn() - _ensure_dev_log_dir() - log_path = DEV_LOG_DIR / "migrate_db.log" - try: - r = subprocess.run( - ["go", "run", "./cmd/sippy", "migrate", "--database-dsn", dsn], - cwd=REPO_ROOT, - env=os.environ.copy(), - capture_output=True, - text=True, - timeout=600, - ) - except subprocess.TimeoutExpired as e: - partial = ((e.stdout or "") + (e.stderr or "")).strip() - log_path.write_text( - partial + ("\n" if partial and not partial.endswith("\n") else ""), - encoding="utf-8", - ) - out = _trim(partial) if partial else "(no output captured)" - return f"migrate timed out after 600s. log: {log_path}\n{out}" - raw = ((r.stdout or "") + (r.stderr or "")).strip() - log_path.write_text(raw + ("\n" if raw and not raw.endswith("\n") else ""), encoding="utf-8") - out = _trim(raw) - if r.returncode != 0: - return f"migrate failed (exit {r.returncode}). log: {log_path}\n{out}" - return f"migrate succeeded (exit 0). log: {log_path}\n{out}" - - @mcp.tool() def regression_cache( bigquery_credentials_file: str | None = None, @@ -533,30 +498,6 @@ def _run_make_phase( return f"{tool_label} succeeded (exit 0). log: {log_path}\n--- last lines ---\n{tail}" -@mcp.tool() -def run_lint(timeout_seconds: int = 1800) -> str: - """Run ``make lint`` (``CI=true`` so ``hack/go-lint.sh`` uses local ``golangci-lint``). - - Log: ``sippy-dev-logs/run_lint.log``. Use ``timeout_seconds=0`` for no limit. - """ - return _run_make_phase( - "run_lint", - "lint", - "run_lint.log", - timeout_seconds, - {"CI": "true"}, - ) - - -@mcp.tool() -def run_test(timeout_seconds: int = 7200) -> str: - """Run ``make test`` (Go packages via gotestsum and ``sippy-ng`` Jest). - - Log: ``sippy-dev-logs/run_test.log``. Use ``timeout_seconds=0`` for no limit. - """ - return _run_make_phase("run_test", "test", "run_test.log", timeout_seconds, None) - - @mcp.tool() def run_e2e( bigquery_credentials_file: str | None = None, From d222eac2f28ae219e6d62265ddcc076416ddee4e Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Wed, 29 Apr 2026 10:26:51 -0400 Subject: [PATCH 3/8] fix gitattributes --- .gitattributes | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0ae837d58..c0746c587 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,5 @@ .claude/** linguist-generated=true -.cursor/rules/** linguist-generated=true -.cursor/mcp.json linguist-generated=false -.cursor/skills/** linguist-generated=false +.cursor/** linguist-generated=true .gemini/** linguist-generated=true .opencode/** linguist-generated=true AGENTS.md linguist-generated=true From 6b739419bf78fabce9da4793ca9829c1a8d630eb Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Thu, 30 Apr 2026 10:13:47 -0400 Subject: [PATCH 4/8] additional fixes and improvements to mcp --- .apm/prompts/sippy-dev-app.prompt.md | 2 +- .apm/prompts/sippy-dev-frontend.prompt.md | 2 + .apm/prompts/sippy-dev-serve.prompt.md | 2 + .claude/commands/sippy-dev-app.md | 2 +- .claude/commands/sippy-dev-frontend.md | 2 + .claude/commands/sippy-dev-serve.md | 2 + .cursor/mcp.json | 4 +- .devcontainer/README.md | 51 ++++- .devcontainer/init-services.sh | 3 +- .devcontainer/post-create.sh | 6 +- .gemini/commands/sippy-dev-app.toml | 2 +- .gemini/commands/sippy-dev-frontend.toml | 2 +- .gemini/commands/sippy-dev-serve.toml | 2 +- .opencode/commands/sippy-dev-app.md | 2 +- .opencode/commands/sippy-dev-frontend.md | 2 + .opencode/commands/sippy-dev-serve.md | 2 + apm.lock.yaml | 18 +- mcp/README.md | 13 +- mcp/run.sh | 14 ++ mcp/server.py | 238 ++++++++++++++-------- mcp/test_server.py | 191 +++++++++++++++++ 21 files changed, 440 insertions(+), 122 deletions(-) create mode 100755 mcp/run.sh create mode 100644 mcp/test_server.py diff --git a/.apm/prompts/sippy-dev-app.prompt.md b/.apm/prompts/sippy-dev-app.prompt.md index 096bc0010..4046af10e 100644 --- a/.apm/prompts/sippy-dev-app.prompt.md +++ b/.apm/prompts/sippy-dev-app.prompt.md @@ -9,4 +9,4 @@ Start the full local Sippy stack using two MCP tool calls (server: **`sippy-dev` 1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set. 2. **`sippy_ng_start`** -Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is. +Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, ask the user if they want to restart it. If yes, call the tool again with **`restart=True`**. diff --git a/.apm/prompts/sippy-dev-frontend.prompt.md b/.apm/prompts/sippy-dev-frontend.prompt.md index 074176f59..3574586dd 100644 --- a/.apm/prompts/sippy-dev-frontend.prompt.md +++ b/.apm/prompts/sippy-dev-frontend.prompt.md @@ -8,4 +8,6 @@ Use the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm **`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**. +If the dev server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**. + See **`mcp/server.py`** for all parameters. diff --git a/.apm/prompts/sippy-dev-serve.prompt.md b/.apm/prompts/sippy-dev-serve.prompt.md index 3e9c8748d..1a875d768 100644 --- a/.apm/prompts/sippy-dev-serve.prompt.md +++ b/.apm/prompts/sippy-dev-serve.prompt.md @@ -8,4 +8,6 @@ Use the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run **`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. +If the server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**. + See **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**. diff --git a/.claude/commands/sippy-dev-app.md b/.claude/commands/sippy-dev-app.md index 2cb9d712e..85d2be678 100644 --- a/.claude/commands/sippy-dev-app.md +++ b/.claude/commands/sippy-dev-app.md @@ -9,4 +9,4 @@ Start the full local Sippy stack using two MCP tool calls (server: **`sippy-dev` 1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set. 2. **`sippy_ng_start`** -Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is. \ No newline at end of file +Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, ask the user if they want to restart it. If yes, call the tool again with **`restart=True`**. \ No newline at end of file diff --git a/.claude/commands/sippy-dev-frontend.md b/.claude/commands/sippy-dev-frontend.md index 1f8912469..53b16d603 100644 --- a/.claude/commands/sippy-dev-frontend.md +++ b/.claude/commands/sippy-dev-frontend.md @@ -8,4 +8,6 @@ Use the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm **`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**. +If the dev server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**. + See **`mcp/server.py`** for all parameters. \ No newline at end of file diff --git a/.claude/commands/sippy-dev-serve.md b/.claude/commands/sippy-dev-serve.md index 680343d81..14c7eb8cd 100644 --- a/.claude/commands/sippy-dev-serve.md +++ b/.claude/commands/sippy-dev-serve.md @@ -8,4 +8,6 @@ Use the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run **`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. +If the server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**. + See **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**. \ No newline at end of file diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 31d365017..924cb0693 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -1,8 +1,8 @@ { "mcpServers": { "sippy-dev": { - "command": "mcp/.venv/bin/python", - "args": ["mcp/server.py"] + "command": "mcp/run.sh", + "args": [] } } } diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 92fe27f17..0c59d3657 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -6,10 +6,39 @@ PostgreSQL, Redis, and all build tools. It runs on **Podman** and works with bot ## Prerequisites -- [Podman](https://podman.io/) v4+ with `podman machine` running -- [devcontainer CLI](https://github.com/devcontainers/cli) — see the [official install instructions](https://github.com/devcontainers/cli#installation) (`npm install -g @devcontainers/cli`, or `brew install devcontainer` on macOS) +- [Podman](https://podman.io/) v4+ +- [devcontainer CLI](https://github.com/devcontainers/cli) — `npm install -g @devcontainers/cli` + +### macOS + +- Start `podman machine` before using the devcontainer: + + ```bash + podman machine init # first time only + podman machine start + ``` + - For Cursor: set `"dev.containers.dockerPath": "podman"` in user settings (`Cmd+Shift+P` > "Preferences: Open User Settings (JSON)") +### Linux + +- Podman runs natively — no machine required. Ensure the Podman socket is active: + + ```bash + systemctl --user enable --now podman.socket + ``` + +- Install `podman-docker` to provide the `docker` CLI alias — this lets devcontainer CLI and Cursor work without `--docker-path podman`: + + ```bash + # Fedora/RHEL + sudo dnf install podman-docker + # Debian/Ubuntu + sudo apt install podman-docker + ``` + +- For Cursor (if not using `podman-docker`): set `"dev.containers.dockerPath": "podman"` in user settings (`Ctrl+Shift+P` > "Preferences: Open User Settings (JSON)") + ## First-Time Setup 1. Copy the env file template and fill in your values: @@ -22,7 +51,7 @@ PostgreSQL, Redis, and all build tools. It runs on **Podman** and works with bot 2. Start the container: ```bash - devcontainer up --workspace-folder . --docker-path podman + devcontainer up --workspace-folder . # add --docker-path podman on macOS ``` This automatically starts PostgreSQL and Redis via `init-services.sh`. @@ -40,7 +69,7 @@ PostgreSQL, Redis, and all build tools. It runs on **Podman** and works with bot ```bash # Start the container (if not already running) -devcontainer up --workspace-folder . --docker-path podman +devcontainer up --workspace-folder . # add --docker-path podman on macOS # Exec in and run Claude Code podman exec -it sippy-dev bash @@ -66,12 +95,12 @@ on first use. ### For Cursor -```text -Cmd+Shift+P > "Dev Containers: Attach to Running Container" > sippy-dev -``` +Open the command palette (`Cmd+Shift+P` on macOS, `Ctrl+Shift+P` on Linux): + +- **Container already running:** "Dev Containers: Attach to Running Container" > `sippy-dev` +- **Container not running:** "Dev Containers: Reopen in Container" to build and start it -If the container isn't running yet, use `"Dev Containers: Reopen in Container"` -to build and start it. Cursor reads `.cursor/mcp.json` for MCP; see +Cursor reads `.cursor/mcp.json` for MCP; see **[mcp/README.md](../mcp/README.md)** for tools, logs, and credentials. ## Services @@ -122,10 +151,10 @@ If you change the Dockerfile or devcontainer config: ```bash podman rm -f sippy-dev 2>/dev/null -devcontainer up --workspace-folder . --docker-path podman --remove-existing-container +devcontainer up --workspace-folder . # add --docker-path podman on macOS --remove-existing-container ``` -Or from Cursor: `Cmd+Shift+P` > "Dev Containers: Rebuild Container Without Cache". +Or from Cursor: open the command palette and run "Dev Containers: Rebuild Container Without Cache". ## Cleanup diff --git a/.devcontainer/init-services.sh b/.devcontainer/init-services.sh index d2bbe95c9..9bdf70a5e 100755 --- a/.devcontainer/init-services.sh +++ b/.devcontainer/init-services.sh @@ -1,7 +1,8 @@ #!/bin/sh +set -eu # Starts PostgreSQL and Redis as standalone Podman containers (runs on the host before the devcontainer starts) -podman network create sippy-net 2>/dev/null +podman network create sippy-net 2>/dev/null || true podman start sippy-postgres 2>/dev/null || \ podman run -d --name sippy-postgres \ diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 7dbb942c0..dc64ca126 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eu echo "==> Installing Go IDE tools..." go install golang.org/x/tools/gopls@latest @@ -16,8 +16,8 @@ cd .. echo "==> Setting up MCP server venv..." python3.12 -m venv mcp/.venv -mcp/.venv/bin/python -m pip install --upgrade pip -mcp/.venv/bin/python -m pip install -r mcp/requirements.txt +mcp/.venv/bin/pip install --upgrade pip -q +mcp/.venv/bin/pip install -r mcp/requirements.txt -q echo "==> Checking GCP auth..." if command -v gcloud >/dev/null 2>&1; then diff --git a/.gemini/commands/sippy-dev-app.toml b/.gemini/commands/sippy-dev-app.toml index da2ec81dd..ad1be5411 100644 --- a/.gemini/commands/sippy-dev-app.toml +++ b/.gemini/commands/sippy-dev-app.toml @@ -1,2 +1,2 @@ description = "Start both Sippy backend and frontend dev servers" -prompt = "# Sippy dev — backend + frontend\n\nStart the full local Sippy stack using two MCP tool calls (server: **`sippy-dev`**). Run them in order — backend first, then frontend.\n\n1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set.\n2. **`sippy_ng_start`**\n\nEach tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is." +prompt = "# Sippy dev — backend + frontend\n\nStart the full local Sippy stack using two MCP tool calls (server: **`sippy-dev`**). Run them in order — backend first, then frontend.\n\n1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set.\n2. **`sippy_ng_start`**\n\nEach tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, ask the user if they want to restart it. If yes, call the tool again with **`restart=True`**." diff --git a/.gemini/commands/sippy-dev-frontend.toml b/.gemini/commands/sippy-dev-frontend.toml index fe676fe77..1af041ac8 100644 --- a/.gemini/commands/sippy-dev-frontend.toml +++ b/.gemini/commands/sippy-dev-frontend.toml @@ -1,2 +1,2 @@ description = "Start the sippy-ng React dev server via the sippy-dev MCP tool" -prompt = "# Sippy dev — frontend (sippy-ng)\n\nUse the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm start` in `sippy-ng` manually — the MCP tool handles background process management, log routing, and duplicate detection.\n\n**`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**.\n\nSee **`mcp/server.py`** for all parameters." +prompt = "# Sippy dev — frontend (sippy-ng)\n\nUse the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm start` in `sippy-ng` manually — the MCP tool handles background process management, log routing, and duplicate detection.\n\n**`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**.\n\nIf the dev server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**.\n\nSee **`mcp/server.py`** for all parameters." diff --git a/.gemini/commands/sippy-dev-serve.toml b/.gemini/commands/sippy-dev-serve.toml index 72a296fff..3258c5fbd 100644 --- a/.gemini/commands/sippy-dev-serve.toml +++ b/.gemini/commands/sippy-dev-serve.toml @@ -1,2 +1,2 @@ description = "Start the Sippy HTTP API server via the sippy-dev MCP tool" -prompt = "# Sippy dev — serve\n\nUse the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy serve` manually — the MCP tool handles background process management, log routing, and duplicate detection.\n\n**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set.\n\nSee **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**." +prompt = "# Sippy dev — serve\n\nUse the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run ./cmd/sippy serve` manually — the MCP tool handles background process management, log routing, and duplicate detection.\n\n**`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set.\n\nIf the server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**.\n\nSee **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**." diff --git a/.opencode/commands/sippy-dev-app.md b/.opencode/commands/sippy-dev-app.md index 2cb9d712e..85d2be678 100644 --- a/.opencode/commands/sippy-dev-app.md +++ b/.opencode/commands/sippy-dev-app.md @@ -9,4 +9,4 @@ Start the full local Sippy stack using two MCP tool calls (server: **`sippy-dev` 1. **`sippy_serve`** — pass **`bigquery_credentials_file`** when `SIPPY_BIGQUERY_CREDENTIALS_FILE` / `GOOGLE_APPLICATION_CREDENTIALS` are not set. 2. **`sippy_ng_start`** -Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, leave that process as-is. \ No newline at end of file +Each tool returns listen hints (typically **8080** / **3000**) and log paths. If a tool reports already running, ask the user if they want to restart it. If yes, call the tool again with **`restart=True`**. \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-frontend.md b/.opencode/commands/sippy-dev-frontend.md index 1f8912469..53b16d603 100644 --- a/.opencode/commands/sippy-dev-frontend.md +++ b/.opencode/commands/sippy-dev-frontend.md @@ -8,4 +8,6 @@ Use the **`sippy_ng_start`** MCP tool (server: **`sippy-dev`**). Do not run `npm **`open_browser`** defaults to **`false`**. Typical URL: **`http://127.0.0.1:3000`**. Log: **`sippy-dev-logs/sippy_ng_start.log`**. +If the dev server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**. + See **`mcp/server.py`** for all parameters. \ No newline at end of file diff --git a/.opencode/commands/sippy-dev-serve.md b/.opencode/commands/sippy-dev-serve.md index 680343d81..14c7eb8cd 100644 --- a/.opencode/commands/sippy-dev-serve.md +++ b/.opencode/commands/sippy-dev-serve.md @@ -8,4 +8,6 @@ Use the **`sippy_serve`** MCP tool (server: **`sippy-dev`**). Do not run `go run **`bigquery_credentials_file`**: path to BigQuery-capable SA JSON (e.g. `sippy-bigquery-job-importer-key.json`); optional if `SIPPY_BIGQUERY_CREDENTIALS_FILE` or `GOOGLE_APPLICATION_CREDENTIALS` is set. +If the server is already running, the tool will report it. Ask the user if they want to restart, and if so call again with **`restart=True`**. + See **`mcp/server.py`** for all parameters. Typical listen address: **`:8080`**. Log: **`sippy-dev-logs/sippy_serve.log`**. \ No newline at end of file diff --git a/apm.lock.yaml b/apm.lock.yaml index 438a29952..2a76004e0 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -43,11 +43,11 @@ local_deployed_files: - .opencode/commands/sippy-update-ga-release-views.md - .opencode/commands/sippy-update-job-variant.md local_deployed_file_hashes: - .claude/commands/sippy-dev-app.md: sha256:0b4837a545b7633d8b53f891aea808b59ddf49815835c6ed978212635bbc7670 - .claude/commands/sippy-dev-frontend.md: sha256:67ff3997d3f9728e318bc8d35d187b922fcb979c6bb2723bf4bf2a0462c9975e + .claude/commands/sippy-dev-app.md: sha256:44a111c7cbd34d2480658b39f9b01a18cb80e0f50b4a652cae4c255d123595a4 + .claude/commands/sippy-dev-frontend.md: sha256:42eae4b3bc610c9fcb43533a6fed229a6d1c409d279f3d6f93672986ede62e3a .claude/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b .claude/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 - .claude/commands/sippy-dev-serve.md: sha256:bb7f6d87d147b437e761e4f7be854e5c7f825d741472b3aee48812e2379e20ca + .claude/commands/sippy-dev-serve.md: sha256:0571085d7f4017334c08a6feadd94ab7ba016cce0556dbd9999c93b5449f420a .claude/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .claude/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .claude/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 @@ -64,20 +64,20 @@ local_deployed_file_hashes: .cursor/rules/general.mdc: sha256:671e94d8251783ecfb70d16e6e8d60dc42752ecfbe90919006b908b8a751595d .cursor/rules/mcp.mdc: sha256:268557cc3bdd9c8f7401f9ea80ac342ff2688f1d563b10880177ca586d7d30bb .cursor/rules/testing.mdc: sha256:e177fa738c0b3b607e947986ba9b0a902a9483bf34d621289f615416084b2509 - .gemini/commands/sippy-dev-app.toml: sha256:c893af083c3fc78fc05b5866edc6cc9ccae61183c1c56bf7e99fbd347c276387 - .gemini/commands/sippy-dev-frontend.toml: sha256:e6c6bf80fe01e2422aaee53f2b1e61d889277e8db0274c8e881e8061be53f97d + .gemini/commands/sippy-dev-app.toml: sha256:afc6fe75647832aa571c63d8229d151820c54535cdb695b96f8400c1d7b3bd16 + .gemini/commands/sippy-dev-frontend.toml: sha256:ec4ab5e1fb7581f09473e33b3ed4f53ce40509f23aac581e3b100ad1f59de5e5 .gemini/commands/sippy-dev-migrate.toml: sha256:e699558eb27c294327a092bfcd3e76b576087ad29d9a3a9b000cfd9e7c6b4774 .gemini/commands/sippy-dev-regression-cache.toml: sha256:cd2c9017e5e976750839c712b42d10046d1d04fabaa749ffda6fd1abaf776d7a - .gemini/commands/sippy-dev-serve.toml: sha256:56b555609652edf23982c8154f8edb42e643ee5c114580ff571134fddb1df72e + .gemini/commands/sippy-dev-serve.toml: sha256:774bedf16a0ac5c2c464a29c8264d2588c48eb2a1d8380e694e22f09c6a95715 .gemini/commands/sippy-dev-tests.toml: sha256:0d19aaff911d478859fc948650a92e46142071a8c5d1a29c5b40c25d1d1e495b .gemini/commands/sippy-generate-release-views.toml: sha256:4759f4547c05631d2e83a5859172421ea3d6ec794dd887756a90bbe0fd2732c1 .gemini/commands/sippy-update-ga-release-views.toml: sha256:71a60dfe068bdc59645b8d0069c93c65f2df30c5172da3389df2a80d86410ae9 .gemini/commands/sippy-update-job-variant.toml: sha256:71c9c7548a1a4b829e970d76504ef981ea05fecf8052cdb86859d96ad52cb2b2 - .opencode/commands/sippy-dev-app.md: sha256:0b4837a545b7633d8b53f891aea808b59ddf49815835c6ed978212635bbc7670 - .opencode/commands/sippy-dev-frontend.md: sha256:67ff3997d3f9728e318bc8d35d187b922fcb979c6bb2723bf4bf2a0462c9975e + .opencode/commands/sippy-dev-app.md: sha256:44a111c7cbd34d2480658b39f9b01a18cb80e0f50b4a652cae4c255d123595a4 + .opencode/commands/sippy-dev-frontend.md: sha256:42eae4b3bc610c9fcb43533a6fed229a6d1c409d279f3d6f93672986ede62e3a .opencode/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b .opencode/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 - .opencode/commands/sippy-dev-serve.md: sha256:bb7f6d87d147b437e761e4f7be854e5c7f825d741472b3aee48812e2379e20ca + .opencode/commands/sippy-dev-serve.md: sha256:0571085d7f4017334c08a6feadd94ab7ba016cce0556dbd9999c93b5449f420a .opencode/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .opencode/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .opencode/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 diff --git a/mcp/README.md b/mcp/README.md index e1bb1f846..6ce74b21b 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -6,14 +6,15 @@ Simple commands (migrate, lint, test) are documented in project instructions via ## Setup -- **Virtualenv**: the devcontainer `post-create` script creates `mcp/.venv` and installs `requirements.txt`. -- **Python**: **3.10+** required (`fastmcp`). The devcontainer image installs **Python 3.12** for this venv. -- **Manual install** (from repo root): +**`mcp/run.sh`** is the entry point. It auto-creates the virtualenv (`mcp/.venv`) and installs dependencies on first run — no manual setup required. The devcontainer's `post-create.sh` pre-warms the venv during build. + +- **Python 3.12** required. The devcontainer image installs it; on macOS use `brew install python@3.12`. +- **Manual venv setup** (if you prefer not to use `run.sh`): ```bash python3.12 -m venv mcp/.venv - mcp/.venv/bin/python -m pip install --upgrade pip - mcp/.venv/bin/python -m pip install -r mcp/requirements.txt + mcp/.venv/bin/pip install --upgrade pip + mcp/.venv/bin/pip install -r mcp/requirements.txt ``` ## Editor configuration @@ -23,7 +24,7 @@ Simple commands (migrate, lint, test) are documented in project instructions via | Cursor | `.cursor/mcp.json` | | Claude Code | `.mcp.json` (repo root) | -Both use the same shape: run `mcp/.venv/bin/python` with argument `mcp/server.py`. The workspace folder should be the Sippy repo root so paths resolve. +Both point to `mcp/run.sh`. This works on the host and inside the devcontainer without path changes. ## Server id in Cursor diff --git a/mcp/run.sh b/mcp/run.sh new file mode 100755 index 000000000..50a84e767 --- /dev/null +++ b/mcp/run.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +VENV_DIR="$SCRIPT_DIR/.venv" + +if [ ! -x "$VENV_DIR/bin/python" ]; then + rm -rf "$VENV_DIR" + python3.12 -m venv "$VENV_DIR" + "$VENV_DIR/bin/pip" install --upgrade pip -q + "$VENV_DIR/bin/pip" install -r "$SCRIPT_DIR/requirements.txt" -q +fi + +exec "$VENV_DIR/bin/python" "$SCRIPT_DIR/server.py" "$@" diff --git a/mcp/server.py b/mcp/server.py index 1fdcbbcc5..287bda65c 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -3,6 +3,8 @@ import subprocess import sys import time +import urllib.request +from collections.abc import Callable from pathlib import Path from fastmcp import FastMCP @@ -50,6 +52,10 @@ def _default_database_dsn() -> str: ) +def _default_redis_url() -> str: + return os.environ.get("REDIS_URL", "redis://localhost:6379") + + def _resolve_bigquery_creds(explicit: str | None) -> tuple[Path | None, str | None]: if explicit: p = Path(explicit).expanduser().resolve() @@ -100,7 +106,7 @@ def regression_cache( return err dsn = database_dsn or _default_database_dsn() - redis = redis_url or os.environ.get("REDIS_URL", "redis://localhost:6379") + redis = redis_url or _default_redis_url() try: views = _repo_path(views_file) config = _repo_path(config_file) @@ -218,55 +224,80 @@ def _filter_pids_by_cwd(pids: list[int], expected_cwd: Path) -> list[int]: return filtered -def _pids_sippy_serve() -> list[int]: - """Processes that look like ``go run ./cmd/sippy serve`` or ``.../sippy serve`` from this repo.""" - root = REPO_ROOT.resolve() +def _find_pids( + expected_cwd: Path, + cmdline_match: Callable[[str], bool], + pgrep_patterns: list[str], +) -> list[int]: + """Find PIDs with a given cwd whose cmdline passes *cmdline_match*. + + On Linux, scans /proc directly. Falls back to pgrep + lsof filtering. + """ found: list[int] = [] if sys.platform.startswith("linux"): for pid_dir in Path("/proc").iterdir(): if not pid_dir.name.isdigit(): continue try: - if _proc_cwd(pid_dir) != root: + if _proc_cwd(pid_dir) != expected_cwd: continue cmd = _proc_cmdline(pid_dir) except OSError: continue - if " migrate" in cmd or " load" in cmd: - continue - if " serve" not in cmd and not cmd.rstrip().endswith(" serve"): - continue - if "cmd/sippy" in cmd or "exe/sippy" in cmd or "/sippy serve" in cmd: + if cmdline_match(cmd): found.append(int(pid_dir.name)) if found: return sorted(set(found)) - for pat in ("./cmd/sippy serve", "cmd/sippy serve", "exe/sippy serve"): - p = _filter_pids_by_cwd(_pgrep_pids(pat), root) + for pat in pgrep_patterns: + p = _filter_pids_by_cwd(_pgrep_pids(pat), expected_cwd) if p: return sorted(set(p)) return [] +def _pids_sippy_serve() -> list[int]: + def _match(cmd: str) -> bool: + if " migrate" in cmd or " load" in cmd: + return False + if " serve" not in cmd and not cmd.rstrip().endswith(" serve"): + return False + return "cmd/sippy" in cmd or "exe/sippy" in cmd or "/sippy serve" in cmd + + return _find_pids( + REPO_ROOT.resolve(), + _match, + ["./cmd/sippy serve", "cmd/sippy serve", "exe/sippy serve"], + ) + + def _pids_sippy_ng_dev() -> list[int]: - """Processes running CRA dev server from ``sippy-ng`` (this repo).""" - ng = (REPO_ROOT / "sippy-ng").resolve() - found: list[int] = [] - if sys.platform.startswith("linux"): - for pid_dir in Path("/proc").iterdir(): - if not pid_dir.name.isdigit(): - continue - try: - if _proc_cwd(pid_dir) != ng: - continue - cmd = _proc_cmdline(pid_dir) - except OSError: - continue - if "react-scripts" in cmd or "npm start" in cmd: - found.append(int(pid_dir.name)) - if found: - return sorted(set(found)) - p = _filter_pids_by_cwd(_pgrep_pids("react-scripts/scripts/start.js"), ng) - return sorted(set(p)) + def _match(cmd: str) -> bool: + return "react-scripts" in cmd or "npm start" in cmd + + return _find_pids( + (REPO_ROOT / "sippy-ng").resolve(), + _match, + ["react-scripts/scripts/start.js"], + ) + + +def _stop_pids(pids: list[int]) -> str: + """Send SIGTERM then SIGKILL to each PID. Returns a summary.""" + for pid in pids: + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + pass + time.sleep(1) + killed = [] + for pid in pids: + try: + os.kill(pid, 0) + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + killed.append(pid) + return ", ".join(str(p) for p in killed) @mcp.tool() @@ -281,19 +312,20 @@ def sippy_serve( mode: str = "ocp", listen: str = ":8080", enable_write_endpoints: bool = True, + restart: bool = False, ) -> str: """Start the Sippy HTTP server (``go run ./cmd/sippy serve``) in the background. Long-running: returns after spawn with PID, log path, and listen address. Uses the same credential and DSN conventions as ``regression_cache``. Skips starting if a matching - ``sippy serve`` process is already running (cwd + cmdline on Linux, ``pgrep -f`` fallback). + ``sippy serve`` process is already running, unless ``restart`` is True. """ creds_path, err = _resolve_bigquery_creds(bigquery_credentials_file) if err: return err dsn = database_dsn or _default_database_dsn() - redis = redis_url or os.environ.get("REDIS_URL", "redis://localhost:6379") + redis = redis_url or _default_redis_url() try: views = _repo_path(views_file) log_path = _repo_path(log_file) @@ -305,12 +337,14 @@ def sippy_serve( existing = _pids_sippy_serve() if existing: - host_hint = f"http://127.0.0.1{listen}" if listen.startswith(":") else listen - pids = ", ".join(str(p) for p in existing) - return ( - f"sippy_serve already running (pid(s) {pids}). Listen: {host_hint} " - f"log: {log_path}" - ) + if not restart: + host_hint = f"http://127.0.0.1{listen}" if listen.startswith(":") else listen + pids = ", ".join(str(p) for p in existing) + return ( + f"sippy_serve already running (pid(s) {pids}). Listen: {host_hint} " + f"log: {log_path}. Call with restart=True to restart." + ) + _stop_pids(existing) args = [ "stdbuf", @@ -346,47 +380,27 @@ def sippy_serve( return f"config file not found: {cfg}" args.extend(["--config", str(cfg)]) - _ensure_dev_log_dir() - log_path.parent.mkdir(parents=True, exist_ok=True) - logf = open(log_path, "a", encoding="utf-8") - try: - proc = subprocess.Popen( - args, - cwd=REPO_ROOT, - env=os.environ.copy(), - stdout=logf, - stderr=subprocess.STDOUT, - stdin=subprocess.DEVNULL, - start_new_session=True, - ) - except OSError as e: - logf.close() - return f"sippy_serve failed to start: {e}" - - logf.close() - time.sleep(0.75) - code = proc.poll() - if code is not None: - try: - tail = log_path.read_text(encoding="utf-8", errors="replace")[-4000:] - except OSError: - tail = "(no log output)" - return f"sippy_serve exited immediately (exit {code}). Log: {log_path}\n--- tail ---\n{tail}" - host_hint = f"http://127.0.0.1{listen}" if listen.startswith(":") else listen - return f"sippy_serve started (pid {proc.pid}). Listen: {host_hint} log: {log_path}" + pid_or_err = _spawn_background( + label="sippy_serve", args=args, cwd=REPO_ROOT, log_path=log_path, + ready_url=host_hint, + ) + if isinstance(pid_or_err, str): + return pid_or_err + return f"sippy_serve started and ready (pid {pid_or_err}). Listen: {host_hint} log: {log_path}" @mcp.tool() def sippy_ng_start( log_file: str = "sippy-dev-logs/sippy_ng_start.log", open_browser: bool = False, + restart: bool = False, ) -> str: """Start the React dev server (``npm start`` in ``sippy-ng``) in the background. CRA defaults to port 3000. ``log_file`` is resolved relative to the repo root; absolute paths outside the repo are rejected. Skips starting if a matching - ``npm start`` / react-scripts process is already running for this ``sippy-ng`` tree. + ``npm start`` / react-scripts process is already running, unless ``restart`` is True. """ ng_dir = REPO_ROOT / "sippy-ng" if not (ng_dir / "package.json").is_file(): @@ -399,24 +413,71 @@ def sippy_ng_start( existing = _pids_sippy_ng_dev() if existing: - pids = ", ".join(str(p) for p in existing) - return ( - f"sippy_ng_start already running (pid(s) {pids}). " - f"Typical URL: http://127.0.0.1:3000 log: {log_path}" - ) + if not restart: + pids = ", ".join(str(p) for p in existing) + return ( + f"sippy_ng_start already running (pid(s) {pids}). " + f"Typical URL: http://127.0.0.1:3000 log: {log_path}. " + f"Call with restart=True to restart." + ) + _stop_pids(existing) env = os.environ.copy() if not open_browser: env["BROWSER"] = "none" + pid_or_err = _spawn_background( + label="sippy_ng_start", + args=["stdbuf", "-oL", "-eL", "npm", "start"], + cwd=ng_dir, + log_path=log_path, + env=env, + ready_url="http://127.0.0.1:3000", + ) + if isinstance(pid_or_err, str): + return pid_or_err + return ( + f"sippy_ng_start started and ready (pid {pid_or_err}). URL: http://127.0.0.1:3000 " + f"log: {log_path}" + ) + + +def _wait_for_ready(url: str, timeout: int, proc: subprocess.Popen) -> str | None: + """Poll *url* until it responds or *timeout* seconds elapse. Returns an error string or None.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + code = proc.poll() + if code is not None: + return f"process exited (exit {code}) while waiting for readiness" + try: + urllib.request.urlopen(url, timeout=2) + return None + except Exception: + time.sleep(1) + return f"not ready after {timeout}s (checked {url})" + + +def _spawn_background( + label: str, + args: list[str], + cwd: Path, + log_path: Path, + env: dict[str, str] | None = None, + ready_url: str | None = None, + ready_timeout: int = 120, +) -> int | str: + """Spawn a detached process, returning its PID or an error string. + + If *ready_url* is set, polls it until it responds (up to *ready_timeout* seconds). + """ _ensure_dev_log_dir() log_path.parent.mkdir(parents=True, exist_ok=True) logf = open(log_path, "a", encoding="utf-8") try: proc = subprocess.Popen( - ["stdbuf", "-oL", "-eL", "npm", "start"], - cwd=ng_dir, - env=env, + args, + cwd=cwd, + env=env or os.environ.copy(), stdout=logf, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, @@ -424,7 +485,7 @@ def sippy_ng_start( ) except OSError as e: logf.close() - return f"sippy_ng_start failed to start: {e}" + return f"{label} failed to start: {e}" logf.close() time.sleep(0.75) @@ -434,12 +495,15 @@ def sippy_ng_start( tail = log_path.read_text(encoding="utf-8", errors="replace")[-4000:] except OSError: tail = "(no log output)" - return f"sippy_ng_start exited immediately (exit {code}). Log: {log_path}\n--- tail ---\n{tail}" + return f"{label} exited immediately (exit {code}). Log: {log_path}\n--- tail ---\n{tail}" - return ( - f"sippy_ng_start started (pid {proc.pid}). Typical URL: http://127.0.0.1:3000 " - f"log: {log_path}" - ) + if ready_url: + err = _wait_for_ready(ready_url, ready_timeout, proc) + if err: + tail = _tail_file(log_path, 40) + return f"{label} started (pid {proc.pid}) but {err}. Log: {log_path}\n--- tail ---\n{tail}" + + return proc.pid def _tail_file(path: Path, max_lines: int) -> str: @@ -477,11 +541,17 @@ def _run_make_phase( try: returncode = proc.wait(timeout=tout) except subprocess.TimeoutExpired: - os.killpg(proc.pid, signal.SIGTERM) + try: + os.killpg(proc.pid, signal.SIGTERM) + except ProcessLookupError: + pass try: proc.wait(timeout=5) except subprocess.TimeoutExpired: - os.killpg(proc.pid, signal.SIGKILL) + try: + os.killpg(proc.pid, signal.SIGKILL) + except ProcessLookupError: + pass proc.wait() tail = _tail_file(log_path, 80) return ( diff --git a/mcp/test_server.py b/mcp/test_server.py new file mode 100644 index 000000000..d45ed78af --- /dev/null +++ b/mcp/test_server.py @@ -0,0 +1,191 @@ +import os +import tempfile +from pathlib import Path +from unittest import mock + +import pytest + +from server import ( + REPO_ROOT, + _default_database_dsn, + _default_redis_url, + _repo_path, + _resolve_bigquery_creds, + _trim, +) + + +class TestRepoPath: + def test_relative_path(self): + result = _repo_path("config/views.yaml") + assert result == (REPO_ROOT / "config" / "views.yaml").resolve() + + def test_nested_relative_path(self): + result = _repo_path("a/b/c") + assert result == (REPO_ROOT / "a" / "b" / "c").resolve() + + def test_dot_relative(self): + result = _repo_path("./config/views.yaml") + assert result == (REPO_ROOT / "config" / "views.yaml").resolve() + + def test_rejects_dotdot_escape(self): + with pytest.raises(ValueError, match="path escapes repo root"): + _repo_path("../etc/passwd") + + def test_rejects_nested_dotdot_escape(self): + with pytest.raises(ValueError, match="path escapes repo root"): + _repo_path("foo/../../..") + + def test_rejects_absolute_outside_repo(self): + with pytest.raises(ValueError, match="path escapes repo root"): + _repo_path("/etc/passwd") + + def test_absolute_inside_repo(self): + inner = str(REPO_ROOT / "config" / "views.yaml") + result = _repo_path(inner) + assert result == Path(inner).resolve() + + def test_tilde_expansion_outside_repo(self): + with pytest.raises(ValueError, match="path escapes repo root"): + _repo_path("~/something") + + def test_repo_root_itself(self): + result = _repo_path(str(REPO_ROOT)) + assert result == REPO_ROOT.resolve() + + def test_normalizes_redundant_slashes(self): + result = _repo_path("config///views.yaml") + assert result == (REPO_ROOT / "config" / "views.yaml").resolve() + + def test_normalizes_inner_dotdot(self): + result = _repo_path("config/../config/views.yaml") + assert result == (REPO_ROOT / "config" / "views.yaml").resolve() + + +class TestTrim: + def test_short_string_unchanged(self): + assert _trim("hello") == "hello" + + def test_at_limit_unchanged(self): + s = "x" * 100 + assert _trim(s, max_len=100) == s + + def test_over_limit_truncated(self): + s = "A" * 30000 + result = _trim(s, max_len=1000) + assert len(result) < len(s) + assert "characters omitted" in result + head = 500 + tail = 1000 - 500 - 120 + assert result.startswith("A" * head) + assert result.endswith("A" * tail) + + def test_omission_count_correct(self): + s = "x" * 30000 + result = _trim(s, max_len=1000) + head = 500 + tail = 1000 - 500 - 120 + expected_omitted = 30000 - head - tail + assert str(expected_omitted) in result + + +class TestResolveBigqueryCreds: + def test_explicit_path_exists(self): + with tempfile.NamedTemporaryFile(suffix=".json") as f: + path, err = _resolve_bigquery_creds(f.name) + assert err is None + assert path == Path(f.name).resolve() + + def test_explicit_path_missing(self): + path, err = _resolve_bigquery_creds("/nonexistent/creds.json") + assert path is None + assert "not found" in err + + def test_env_var_fallback(self): + with tempfile.NamedTemporaryFile(suffix=".json") as f: + with mock.patch.dict( + os.environ, + {"SIPPY_BIGQUERY_CREDENTIALS_FILE": f.name}, + clear=False, + ): + path, err = _resolve_bigquery_creds(None) + assert err is None + assert path == Path(f.name).resolve() + + def test_env_var_second_fallback(self): + with tempfile.NamedTemporaryFile(suffix=".json") as f: + env = { + "GOOGLE_APPLICATION_CREDENTIALS": f.name, + } + with mock.patch.dict(os.environ, env, clear=False): + os.environ.pop("SIPPY_BIGQUERY_CREDENTIALS_FILE", None) + path, err = _resolve_bigquery_creds(None) + assert err is None + assert path == Path(f.name).resolve() + + def test_env_var_file_missing(self): + with mock.patch.dict( + os.environ, + {"SIPPY_BIGQUERY_CREDENTIALS_FILE": "/nonexistent/sa.json"}, + clear=False, + ): + path, err = _resolve_bigquery_creds(None) + assert path is None + assert "SIPPY_BIGQUERY_CREDENTIALS_FILE" in err + assert "not found" in err + + def test_no_creds_anywhere(self): + with mock.patch.dict(os.environ, clear=False): + os.environ.pop("SIPPY_BIGQUERY_CREDENTIALS_FILE", None) + os.environ.pop("GOOGLE_APPLICATION_CREDENTIALS", None) + path, err = _resolve_bigquery_creds(None) + assert path is None + assert "Set bigquery_credentials_file" in err + + def test_explicit_overrides_env(self): + with tempfile.NamedTemporaryFile(suffix=".json") as explicit: + with tempfile.NamedTemporaryFile(suffix=".json") as env_file: + with mock.patch.dict( + os.environ, + {"SIPPY_BIGQUERY_CREDENTIALS_FILE": env_file.name}, + clear=False, + ): + path, err = _resolve_bigquery_creds(explicit.name) + assert err is None + assert path == Path(explicit.name).resolve() + + def test_first_env_var_takes_priority(self): + with tempfile.NamedTemporaryFile(suffix=".json") as first: + with tempfile.NamedTemporaryFile(suffix=".json") as second: + env = { + "SIPPY_BIGQUERY_CREDENTIALS_FILE": first.name, + "GOOGLE_APPLICATION_CREDENTIALS": second.name, + } + with mock.patch.dict(os.environ, env, clear=False): + path, err = _resolve_bigquery_creds(None) + assert err is None + assert path == Path(first.name).resolve() + + +class TestDefaults: + def test_database_dsn_default(self): + with mock.patch.dict(os.environ, clear=False): + os.environ.pop("SIPPY_DATABASE_DSN", None) + assert _default_database_dsn() == "postgresql://postgres:password@localhost:5432/postgres" + + def test_database_dsn_from_env(self): + with mock.patch.dict( + os.environ, {"SIPPY_DATABASE_DSN": "postgresql://other:5433/db"}, clear=False + ): + assert _default_database_dsn() == "postgresql://other:5433/db" + + def test_redis_url_default(self): + with mock.patch.dict(os.environ, clear=False): + os.environ.pop("REDIS_URL", None) + assert _default_redis_url() == "redis://localhost:6379" + + def test_redis_url_from_env(self): + with mock.patch.dict( + os.environ, {"REDIS_URL": "redis://other:6380"}, clear=False + ): + assert _default_redis_url() == "redis://other:6380" From b8ec23181c75c7e5bc178c94521072341ac5f986 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Thu, 30 Apr 2026 10:55:11 -0400 Subject: [PATCH 5/8] update apm to 0.11.0 and mark generated files in gitattributes Co-Authored-By: Claude Opus 4.6 --- .gitattributes | 3 +++ AGENTS.md | 4 ++-- CLAUDE.md | 2 +- GEMINI.md | 2 +- Makefile | 4 ++-- mcp/AGENTS.md | 4 ++-- mcp/CLAUDE.md | 2 +- sippy-ng/AGENTS.md | 4 ++-- sippy-ng/CLAUDE.md | 2 +- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.gitattributes b/.gitattributes index c0746c587..2b740f8fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,6 @@ CLAUDE.md linguist-generated=true GEMINI.md linguist-generated=true mcp/AGENTS.md linguist-generated=true mcp/CLAUDE.md linguist-generated=true +sippy-ng/AGENTS.md linguist-generated=true +sippy-ng/CLAUDE.md linguist-generated=true +.mcp.json linguist-generated=true diff --git a/AGENTS.md b/AGENTS.md index d6c2ddbbe..856934fe6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md - + ## Files matching `**` @@ -56,4 +56,4 @@ Favor clarity and maintainability over cleverness. Comments should be minimal, h --- *This file was generated by APM CLI. Do not edit manually.* -*To regenerate: `specify apm compile`* +*To regenerate: `apm compile`* diff --git a/CLAUDE.md b/CLAUDE.md index 452020de3..131da6b9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # CLAUDE.md - + # Project Standards diff --git a/GEMINI.md b/GEMINI.md index fe1730f9c..123ba5eb3 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,6 +1,6 @@ # GEMINI.md - + @./AGENTS.md diff --git a/Makefile b/Makefile index 4585b821f..05a0f8386 100644 --- a/Makefile +++ b/Makefile @@ -61,8 +61,8 @@ clean: rm -rf sippy-ng/node_modules apm: - uvx --from apm-cli@0.10.0 apm install - uvx --from apm-cli@0.10.0 apm compile + uvx --from apm-cli@0.11.0 apm install + uvx --from apm-cli@0.11.0 apm compile verify-apm: apm @if ! git diff --quiet HEAD -- .claude .cursor .gemini .opencode AGENTS.md CLAUDE.md GEMINI.md sippy-ng/AGENTS.md sippy-ng/CLAUDE.md mcp/AGENTS.md mcp/CLAUDE.md; then \ diff --git a/mcp/AGENTS.md b/mcp/AGENTS.md index 15551ead8..6b1ea53f2 100644 --- a/mcp/AGENTS.md +++ b/mcp/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md - + ## Files matching `mcp/**` @@ -13,4 +13,4 @@ When adding or modifying MCP tools, follow existing patterns in `server.py` (sub --- *This file was generated by APM CLI. Do not edit manually.* -*To regenerate: `specify apm compile`* +*To regenerate: `apm compile`* diff --git a/mcp/CLAUDE.md b/mcp/CLAUDE.md index ea4681c2c..11b6b2fa2 100644 --- a/mcp/CLAUDE.md +++ b/mcp/CLAUDE.md @@ -1,7 +1,7 @@ # CLAUDE.md - + # Project Standards diff --git a/sippy-ng/AGENTS.md b/sippy-ng/AGENTS.md index 7cb53b962..8cc348b84 100644 --- a/sippy-ng/AGENTS.md +++ b/sippy-ng/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md - + ## Files matching `sippy-ng/**` @@ -21,4 +21,4 @@ The frontend uses `npm`. If you must install or update any dependencies, always --- *This file was generated by APM CLI. Do not edit manually.* -*To regenerate: `specify apm compile`* +*To regenerate: `apm compile`* diff --git a/sippy-ng/CLAUDE.md b/sippy-ng/CLAUDE.md index d9d63e658..97779f3d4 100644 --- a/sippy-ng/CLAUDE.md +++ b/sippy-ng/CLAUDE.md @@ -1,7 +1,7 @@ # CLAUDE.md - + # Project Standards From 36b006fedf4d00866ecddc4b60b3f9e957f4760f Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Thu, 30 Apr 2026 11:28:54 -0400 Subject: [PATCH 6/8] Add sippy-dev-setup command --- .apm/prompts/sippy-dev-setup.prompt.md | 113 ++++++++++++++++++ .claude/commands/sippy-dev-setup.md | 113 ++++++++++++++++++ .devcontainer/README.md | 153 ++++++------------------- .gemini/commands/sippy-dev-setup.toml | 2 + .opencode/commands/sippy-dev-setup.md | 113 ++++++++++++++++++ apm.lock.yaml | 6 + 6 files changed, 382 insertions(+), 118 deletions(-) create mode 100644 .apm/prompts/sippy-dev-setup.prompt.md create mode 100644 .claude/commands/sippy-dev-setup.md create mode 100644 .gemini/commands/sippy-dev-setup.toml create mode 100644 .opencode/commands/sippy-dev-setup.md diff --git a/.apm/prompts/sippy-dev-setup.prompt.md b/.apm/prompts/sippy-dev-setup.prompt.md new file mode 100644 index 000000000..f4bcd6c07 --- /dev/null +++ b/.apm/prompts/sippy-dev-setup.prompt.md @@ -0,0 +1,113 @@ +--- +description: "Set up the Sippy devcontainer (Podman, env, GCP auth)" +--- + +# Sippy dev — devcontainer setup + +Interactive setup for the Sippy devcontainer. Automates what can be detected, prompts for what can't. + +## Workflow + +### 1. Detect OS + +```bash +uname -s +``` + +- **Darwin** = macOS +- **Linux** = Linux + +### 2. Check prerequisites + +Verify each tool is installed. Report any that are missing and stop. + +```bash +command -v podman +command -v devcontainer +``` + +If `devcontainer` is missing, tell the user: `npm install -g @devcontainers/cli` + +### 3. macOS: Podman machine + +If macOS, check if podman machine is running: + +```bash +podman machine info +``` + +If not initialized or not running, run: + +```bash +podman machine init # only if no machine exists +podman machine start +``` + +### 4. Linux: Podman socket + +If Linux, check if the socket is active: + +```bash +systemctl --user is-active podman.socket +``` + +If not active, run: + +```bash +systemctl --user enable --now podman.socket +``` + +Also check for `podman-docker`: + +```bash +command -v docker +``` + +If missing, suggest installing `podman-docker` (dnf or apt depending on `/etc/os-release`). + +### 5. Environment file + +Check if `.devcontainer/.env` exists: + +```bash +test -f .devcontainer/.env +``` + +If missing, copy from the example: + +```bash +cp .devcontainer/.env.example .devcontainer/.env +``` + +Then read `.devcontainer/.env` and check for empty required values. If any are blank (e.g. `ANTHROPIC_VERTEX_PROJECT_ID`), tell the user which values need to be filled in and ask them to edit `.devcontainer/.env` and let you know when they're done. **Do not** ask for the values directly or write to the file yourself — the user should edit it. Wait for them to confirm before continuing. + +### 6. Start the container + +Determine the right command based on OS: + +- **macOS**: `devcontainer up --workspace-folder . --docker-path podman` +- **Linux** (with `podman-docker`): `devcontainer up --workspace-folder .` +- **Linux** (without `podman-docker`): `devcontainer up --workspace-folder . --docker-path podman` + +Run it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.sh` (Go tools, npm, MCP venv). + +### 7. GCP authentication + +Ask the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run: + +``` +! podman exec -it sippy-dev bash -c "gcloud auth application-default login" +``` + +This must be done interactively by the user — it opens a browser for OAuth. + +### 8. Summary + +Print a summary of what was set up: + +- Container status +- PostgreSQL: `localhost:5432` +- Redis: `localhost:6379` +- API server: `localhost:8080` (start with `/sippy-dev-serve`) +- React dev server: `localhost:3000` (start with `/sippy-dev-frontend`) +- GCP auth status diff --git a/.claude/commands/sippy-dev-setup.md b/.claude/commands/sippy-dev-setup.md new file mode 100644 index 000000000..4e2cb90a2 --- /dev/null +++ b/.claude/commands/sippy-dev-setup.md @@ -0,0 +1,113 @@ +--- +description: Set up the Sippy devcontainer (Podman, env, GCP auth) +--- + +# Sippy dev — devcontainer setup + +Interactive setup for the Sippy devcontainer. Automates what can be detected, prompts for what can't. + +## Workflow + +### 1. Detect OS + +```bash +uname -s +``` + +- **Darwin** = macOS +- **Linux** = Linux + +### 2. Check prerequisites + +Verify each tool is installed. Report any that are missing and stop. + +```bash +command -v podman +command -v devcontainer +``` + +If `devcontainer` is missing, tell the user: `npm install -g @devcontainers/cli` + +### 3. macOS: Podman machine + +If macOS, check if podman machine is running: + +```bash +podman machine info +``` + +If not initialized or not running, run: + +```bash +podman machine init # only if no machine exists +podman machine start +``` + +### 4. Linux: Podman socket + +If Linux, check if the socket is active: + +```bash +systemctl --user is-active podman.socket +``` + +If not active, run: + +```bash +systemctl --user enable --now podman.socket +``` + +Also check for `podman-docker`: + +```bash +command -v docker +``` + +If missing, suggest installing `podman-docker` (dnf or apt depending on `/etc/os-release`). + +### 5. Environment file + +Check if `.devcontainer/.env` exists: + +```bash +test -f .devcontainer/.env +``` + +If missing, copy from the example: + +```bash +cp .devcontainer/.env.example .devcontainer/.env +``` + +Then read `.devcontainer/.env` and check for empty required values. If any are blank (e.g. `ANTHROPIC_VERTEX_PROJECT_ID`), tell the user which values need to be filled in and ask them to edit `.devcontainer/.env` and let you know when they're done. **Do not** ask for the values directly or write to the file yourself — the user should edit it. Wait for them to confirm before continuing. + +### 6. Start the container + +Determine the right command based on OS: + +- **macOS**: `devcontainer up --workspace-folder . --docker-path podman` +- **Linux** (with `podman-docker`): `devcontainer up --workspace-folder .` +- **Linux** (without `podman-docker`): `devcontainer up --workspace-folder . --docker-path podman` + +Run it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.sh` (Go tools, npm, MCP venv). + +### 7. GCP authentication + +Ask the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run: + +``` +! podman exec -it sippy-dev bash -c "gcloud auth application-default login" +``` + +This must be done interactively by the user — it opens a browser for OAuth. + +### 8. Summary + +Print a summary of what was set up: + +- Container status +- PostgreSQL: `localhost:5432` +- Redis: `localhost:6379` +- API server: `localhost:8080` (start with `/sippy-dev-serve`) +- React dev server: `localhost:3000` (start with `/sippy-dev-frontend`) +- GCP auth status \ No newline at end of file diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 0c59d3657..44e7a688d 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -4,157 +4,74 @@ The devcontainer provides a full-stack development environment with Go, Node.js, PostgreSQL, Redis, and all build tools. It runs on **Podman** and works with both **Cursor** and **Claude Code**. -## Prerequisites +## Quick start -- [Podman](https://podman.io/) v4+ -- [devcontainer CLI](https://github.com/devcontainers/cli) — `npm install -g @devcontainers/cli` +Run the `/sippy-dev-setup` slash command in Claude Code or Cursor. It detects your +OS, checks prerequisites, creates the `.env` file, starts the container, and walks +you through GCP authentication. -### macOS +## What you'll need to provide -- Start `podman machine` before using the devcontainer: +- **GCP project ID** (`ANTHROPIC_VERTEX_PROJECT_ID`) — for Claude Code via Vertex AI +- **GCP auth** — the setup will prompt you to run `gcloud auth application-default login` - ```bash - podman machine init # first time only - podman machine start - ``` +## Manual setup -- For Cursor: set `"dev.containers.dockerPath": "podman"` in user settings (`Cmd+Shift+P` > "Preferences: Open User Settings (JSON)") +If you prefer to set up manually: -### Linux +1. Install [Podman](https://podman.io/) v4+ and [devcontainer CLI](https://github.com/devcontainers/cli) (`npm install -g @devcontainers/cli`) +2. **macOS**: run `podman machine init && podman machine start` +3. **Linux**: run `systemctl --user enable --now podman.socket` and install `podman-docker` +4. Copy `.devcontainer/.env.example` to `.devcontainer/.env` and fill in your values +5. Run `devcontainer up --workspace-folder .` (add `--docker-path podman` on macOS) +6. Exec in: `podman exec -it sippy-dev bash` and run `gcloud auth application-default login` -- Podman runs natively — no machine required. Ensure the Podman socket is active: - - ```bash - systemctl --user enable --now podman.socket - ``` - -- Install `podman-docker` to provide the `docker` CLI alias — this lets devcontainer CLI and Cursor work without `--docker-path podman`: - - ```bash - # Fedora/RHEL - sudo dnf install podman-docker - # Debian/Ubuntu - sudo apt install podman-docker - ``` - -- For Cursor (if not using `podman-docker`): set `"dev.containers.dockerPath": "podman"` in user settings (`Ctrl+Shift+P` > "Preferences: Open User Settings (JSON)") - -## First-Time Setup - -1. Copy the env file template and fill in your values: - - ```bash - cp .devcontainer/.env.example .devcontainer/.env - # Edit .devcontainer/.env with your credentials - ``` - -2. Start the container: - - ```bash - devcontainer up --workspace-folder . # add --docker-path podman on macOS - ``` - - This automatically starts PostgreSQL and Redis via `init-services.sh`. +## Services -3. Exec into the container and authenticate with GCP: +| Service | Container | Port | Access from devcontainer | +| ---------------- | ------------------- | ---- | ------------------------ | +| PostgreSQL | `sippy-postgres` | 5432 | `$SIPPY_DATABASE_DSN` | +| Redis | `sippy-redis` | 6379 | `$REDIS_URL` | +| Sippy API | inside devcontainer | 8080 | `http://localhost:8080` | +| React dev server | inside devcontainer | 3000 | `http://localhost:3000` | - ```bash - podman exec -it sippy-dev bash - gcloud auth application-default login - ``` +Ports 8080 and 3000 are published to the host. -## Starting the Container +## Starting the container -### For Claude Code +### Claude Code ```bash -# Start the container (if not already running) devcontainer up --workspace-folder . # add --docker-path podman on macOS - -# Exec in and run Claude Code podman exec -it sippy-dev bash claude ``` -Claude Code picks up the MCP server from `.mcp.json` at the repo root. Tool list -and usage: **[mcp/README.md](../mcp/README.md)**. - -#### Claude Code environment variables +### Cursor -Claude Code uses Vertex AI for authentication. The following env vars must be set -in `.devcontainer/.env`: +Open the command palette and run "Dev Containers: Attach to Running Container" > `sippy-dev`. -| Variable | Description | -| ----------------------------- | ------------------------------ | -| `CLAUDE_CODE_USE_VERTEX` | Set to `1` to enable Vertex AI | -| `ANTHROPIC_VERTEX_PROJECT_ID` | Your GCP project ID | -| `CLOUD_ML_REGION` | GCP region (e.g., `global`) | +## Lint and e2e -You must also run `gcloud auth application-default login` inside the container -on first use. - -### For Cursor - -Open the command palette (`Cmd+Shift+P` on macOS, `Ctrl+Shift+P` on Linux): - -- **Container already running:** "Dev Containers: Attach to Running Container" > `sippy-dev` -- **Container not running:** "Dev Containers: Reopen in Container" to build and start it - -Cursor reads `.cursor/mcp.json` for MCP; see -**[mcp/README.md](../mcp/README.md)** for tools, logs, and credentials. - -## Services - -| Service | Container | Port | Access from devcontainer | -| ---------------- | ------------------- | ---- | ------------------------ | -| PostgreSQL | `sippy-postgres` | 5432 | `$SIPPY_DATABASE_DSN` | -| Redis | `sippy-redis` | 6379 | `$REDIS_URL` | -| Sippy API | inside devcontainer | 8080 | `http://localhost:8080` | -| React dev server | inside devcontainer | 3000 | `http://localhost:3000` | - -Ports 8080 and 3000 are published to the host, so you can access them in your -browser at `http://localhost:8080` and `http://localhost:3000`. - -## Lint and e2e inside the devcontainer - -**`make lint`** (the Go path via `hack/go-lint.sh` without `CI=true`) and **`make e2e`** -(`scripts/e2e.sh`) are **not expected to work** when you run them only inside this -devcontainer today. - -**Why:** `go-lint.sh` runs `golangci-lint` in a separate container image via -Podman/Docker. `e2e.sh` starts short-lived Postgres and Redis containers the same -way. That is **nested** container use. In many devcontainer setups (including -typical Cursor/cloud workspaces), rootless Podman inside the outer container cannot -get a working user-space network stack: **`/dev/net/tun`** is missing, so -slirp4netns/pasta fail, and alternatives such as **`--network host`** often hit -**`proc` mount / netlink** restrictions. Without nested networking, those helper -containers never start, so lint and e2e fail. - -**What works today:** run **`make lint`** / **`make e2e`** on the **host** (or any -environment where Podman or Docker can run sibling containers normally), or set -**`CI=true`** for the Go part of lint so `hack/go-lint.sh` uses a host-installed -`golangci-lint` instead of spawning an inner container. +`make lint` (without `CI=true`) and `make e2e` require nested containers and +**do not work inside the devcontainer**. Run them on the host, or use `CI=true make lint` +inside the container for the Go linter. ### TODO - [ ] Make **`make lint`** reliable inside the devcontainer without nested Podman - (for example: always use host `golangci-lint` when present, or document - `CI=true` as the supported in-container path). + (e.g. always use host `golangci-lint` when present, or default to `CI=true`). - [ ] Make **`make e2e`** reliable inside the devcontainer without nested Podman - (for example: optional mode that uses the compose **`sippy-postgres`** / - **`sippy-redis`** services with an isolated DB name and Redis logical DB, plus - client tools in the image, or document running e2e only on the host). + (e.g. reuse the `sippy-postgres` / `sippy-redis` services instead of spawning new containers). ## Rebuilding -If you change the Dockerfile or devcontainer config: - ```bash podman rm -f sippy-dev 2>/dev/null -devcontainer up --workspace-folder . # add --docker-path podman on macOS --remove-existing-container +devcontainer up --workspace-folder . --remove-existing-container # add --docker-path podman on macOS ``` -Or from Cursor: open the command palette and run "Dev Containers: Rebuild Container Without Cache". +Or from Cursor: "Dev Containers: Rebuild Container Without Cache". ## Cleanup diff --git a/.gemini/commands/sippy-dev-setup.toml b/.gemini/commands/sippy-dev-setup.toml new file mode 100644 index 000000000..dbe7b7c97 --- /dev/null +++ b/.gemini/commands/sippy-dev-setup.toml @@ -0,0 +1,2 @@ +description = "Set up the Sippy devcontainer (Podman, env, GCP auth)" +prompt = "# Sippy dev — devcontainer setup\n\nInteractive setup for the Sippy devcontainer. Automates what can be detected, prompts for what can't.\n\n## Workflow\n\n### 1. Detect OS\n\n```bash\nuname -s\n```\n\n- **Darwin** = macOS\n- **Linux** = Linux\n\n### 2. Check prerequisites\n\nVerify each tool is installed. Report any that are missing and stop.\n\n```bash\ncommand -v podman\ncommand -v devcontainer\n```\n\nIf `devcontainer` is missing, tell the user: `npm install -g @devcontainers/cli`\n\n### 3. macOS: Podman machine\n\nIf macOS, check if podman machine is running:\n\n```bash\npodman machine info\n```\n\nIf not initialized or not running, run:\n\n```bash\npodman machine init # only if no machine exists\npodman machine start\n```\n\n### 4. Linux: Podman socket\n\nIf Linux, check if the socket is active:\n\n```bash\nsystemctl --user is-active podman.socket\n```\n\nIf not active, run:\n\n```bash\nsystemctl --user enable --now podman.socket\n```\n\nAlso check for `podman-docker`:\n\n```bash\ncommand -v docker\n```\n\nIf missing, suggest installing `podman-docker` (dnf or apt depending on `/etc/os-release`).\n\n### 5. Environment file\n\nCheck if `.devcontainer/.env` exists:\n\n```bash\ntest -f .devcontainer/.env\n```\n\nIf missing, copy from the example:\n\n```bash\ncp .devcontainer/.env.example .devcontainer/.env\n```\n\nThen read `.devcontainer/.env` and check for empty required values. If any are blank (e.g. `ANTHROPIC_VERTEX_PROJECT_ID`), tell the user which values need to be filled in and ask them to edit `.devcontainer/.env` and let you know when they're done. **Do not** ask for the values directly or write to the file yourself — the user should edit it. Wait for them to confirm before continuing.\n\n### 6. Start the container\n\nDetermine the right command based on OS:\n\n- **macOS**: `devcontainer up --workspace-folder . --docker-path podman`\n- **Linux** (with `podman-docker`): `devcontainer up --workspace-folder .`\n- **Linux** (without `podman-docker`): `devcontainer up --workspace-folder . --docker-path podman`\n\nRun it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.sh` (Go tools, npm, MCP venv).\n\n### 7. GCP authentication\n\nAsk the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run:\n\n```\n! podman exec -it sippy-dev bash -c \"gcloud auth application-default login\"\n```\n\nThis must be done interactively by the user — it opens a browser for OAuth.\n\n### 8. Summary\n\nPrint a summary of what was set up:\n\n- Container status\n- PostgreSQL: `localhost:5432`\n- Redis: `localhost:6379`\n- API server: `localhost:8080` (start with `/sippy-dev-serve`)\n- React dev server: `localhost:3000` (start with `/sippy-dev-frontend`)\n- GCP auth status" diff --git a/.opencode/commands/sippy-dev-setup.md b/.opencode/commands/sippy-dev-setup.md new file mode 100644 index 000000000..4e2cb90a2 --- /dev/null +++ b/.opencode/commands/sippy-dev-setup.md @@ -0,0 +1,113 @@ +--- +description: Set up the Sippy devcontainer (Podman, env, GCP auth) +--- + +# Sippy dev — devcontainer setup + +Interactive setup for the Sippy devcontainer. Automates what can be detected, prompts for what can't. + +## Workflow + +### 1. Detect OS + +```bash +uname -s +``` + +- **Darwin** = macOS +- **Linux** = Linux + +### 2. Check prerequisites + +Verify each tool is installed. Report any that are missing and stop. + +```bash +command -v podman +command -v devcontainer +``` + +If `devcontainer` is missing, tell the user: `npm install -g @devcontainers/cli` + +### 3. macOS: Podman machine + +If macOS, check if podman machine is running: + +```bash +podman machine info +``` + +If not initialized or not running, run: + +```bash +podman machine init # only if no machine exists +podman machine start +``` + +### 4. Linux: Podman socket + +If Linux, check if the socket is active: + +```bash +systemctl --user is-active podman.socket +``` + +If not active, run: + +```bash +systemctl --user enable --now podman.socket +``` + +Also check for `podman-docker`: + +```bash +command -v docker +``` + +If missing, suggest installing `podman-docker` (dnf or apt depending on `/etc/os-release`). + +### 5. Environment file + +Check if `.devcontainer/.env` exists: + +```bash +test -f .devcontainer/.env +``` + +If missing, copy from the example: + +```bash +cp .devcontainer/.env.example .devcontainer/.env +``` + +Then read `.devcontainer/.env` and check for empty required values. If any are blank (e.g. `ANTHROPIC_VERTEX_PROJECT_ID`), tell the user which values need to be filled in and ask them to edit `.devcontainer/.env` and let you know when they're done. **Do not** ask for the values directly or write to the file yourself — the user should edit it. Wait for them to confirm before continuing. + +### 6. Start the container + +Determine the right command based on OS: + +- **macOS**: `devcontainer up --workspace-folder . --docker-path podman` +- **Linux** (with `podman-docker`): `devcontainer up --workspace-folder .` +- **Linux** (without `podman-docker`): `devcontainer up --workspace-folder . --docker-path podman` + +Run it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.sh` (Go tools, npm, MCP venv). + +### 7. GCP authentication + +Ask the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run: + +``` +! podman exec -it sippy-dev bash -c "gcloud auth application-default login" +``` + +This must be done interactively by the user — it opens a browser for OAuth. + +### 8. Summary + +Print a summary of what was set up: + +- Container status +- PostgreSQL: `localhost:5432` +- Redis: `localhost:6379` +- API server: `localhost:8080` (start with `/sippy-dev-serve`) +- React dev server: `localhost:3000` (start with `/sippy-dev-frontend`) +- GCP auth status \ No newline at end of file diff --git a/apm.lock.yaml b/apm.lock.yaml index 2a76004e0..447901362 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -8,6 +8,7 @@ local_deployed_files: - .claude/commands/sippy-dev-migrate.md - .claude/commands/sippy-dev-regression-cache.md - .claude/commands/sippy-dev-serve.md +- .claude/commands/sippy-dev-setup.md - .claude/commands/sippy-dev-tests.md - .claude/commands/sippy-generate-release-views.md - .claude/commands/sippy-update-ga-release-views.md @@ -29,6 +30,7 @@ local_deployed_files: - .gemini/commands/sippy-dev-migrate.toml - .gemini/commands/sippy-dev-regression-cache.toml - .gemini/commands/sippy-dev-serve.toml +- .gemini/commands/sippy-dev-setup.toml - .gemini/commands/sippy-dev-tests.toml - .gemini/commands/sippy-generate-release-views.toml - .gemini/commands/sippy-update-ga-release-views.toml @@ -38,6 +40,7 @@ local_deployed_files: - .opencode/commands/sippy-dev-migrate.md - .opencode/commands/sippy-dev-regression-cache.md - .opencode/commands/sippy-dev-serve.md +- .opencode/commands/sippy-dev-setup.md - .opencode/commands/sippy-dev-tests.md - .opencode/commands/sippy-generate-release-views.md - .opencode/commands/sippy-update-ga-release-views.md @@ -48,6 +51,7 @@ local_deployed_file_hashes: .claude/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b .claude/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 .claude/commands/sippy-dev-serve.md: sha256:0571085d7f4017334c08a6feadd94ab7ba016cce0556dbd9999c93b5449f420a + .claude/commands/sippy-dev-setup.md: sha256:613a5d3c0ba2cebb3d4fd5b9d12863ccf1c4c22adf0f724cfdddc53191a63b76 .claude/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .claude/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .claude/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 @@ -69,6 +73,7 @@ local_deployed_file_hashes: .gemini/commands/sippy-dev-migrate.toml: sha256:e699558eb27c294327a092bfcd3e76b576087ad29d9a3a9b000cfd9e7c6b4774 .gemini/commands/sippy-dev-regression-cache.toml: sha256:cd2c9017e5e976750839c712b42d10046d1d04fabaa749ffda6fd1abaf776d7a .gemini/commands/sippy-dev-serve.toml: sha256:774bedf16a0ac5c2c464a29c8264d2588c48eb2a1d8380e694e22f09c6a95715 + .gemini/commands/sippy-dev-setup.toml: sha256:f99c93d6570c06612c90c21c84b1b335a640e9fb4e586c474e8f38f98f513013 .gemini/commands/sippy-dev-tests.toml: sha256:0d19aaff911d478859fc948650a92e46142071a8c5d1a29c5b40c25d1d1e495b .gemini/commands/sippy-generate-release-views.toml: sha256:4759f4547c05631d2e83a5859172421ea3d6ec794dd887756a90bbe0fd2732c1 .gemini/commands/sippy-update-ga-release-views.toml: sha256:71a60dfe068bdc59645b8d0069c93c65f2df30c5172da3389df2a80d86410ae9 @@ -78,6 +83,7 @@ local_deployed_file_hashes: .opencode/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b .opencode/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 .opencode/commands/sippy-dev-serve.md: sha256:0571085d7f4017334c08a6feadd94ab7ba016cce0556dbd9999c93b5449f420a + .opencode/commands/sippy-dev-setup.md: sha256:613a5d3c0ba2cebb3d4fd5b9d12863ccf1c4c22adf0f724cfdddc53191a63b76 .opencode/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .opencode/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .opencode/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 From 509df9a731adc04d24ce06e7461033959b8bc8ff Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Thu, 30 Apr 2026 11:32:29 -0400 Subject: [PATCH 7/8] use frontend make target --- .devcontainer/post-create.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index dc64ca126..3a3588ba8 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -10,9 +10,7 @@ echo "==> Downloading Go module dependencies..." go mod download echo "==> Installing frontend dependencies..." -cd sippy-ng -npm install --ignore-scripts -cd .. +make npm echo "==> Setting up MCP server venv..." python3.12 -m venv mcp/.venv From abac961f2b7d679d20716a2e2d02d4b90a1b8120 Mon Sep 17 00:00:00 2001 From: Stephen Goeddel Date: Thu, 30 Apr 2026 12:13:52 -0400 Subject: [PATCH 8/8] fix gcloud auth and add default model for claude --- .apm/prompts/sippy-dev-setup.prompt.md | 8 ++++---- .claude/commands/sippy-dev-setup.md | 8 ++++---- .claude/settings.json | 3 +++ .devcontainer/README.md | 2 +- .devcontainer/devcontainer.json | 3 +++ .devcontainer/post-create.sh | 9 --------- .gemini/commands/sippy-dev-setup.toml | 2 +- .opencode/commands/sippy-dev-setup.md | 8 ++++---- apm.lock.yaml | 6 +++--- 9 files changed, 23 insertions(+), 26 deletions(-) create mode 100644 .claude/settings.json diff --git a/.apm/prompts/sippy-dev-setup.prompt.md b/.apm/prompts/sippy-dev-setup.prompt.md index f4bcd6c07..e9dfa9c8c 100644 --- a/.apm/prompts/sippy-dev-setup.prompt.md +++ b/.apm/prompts/sippy-dev-setup.prompt.md @@ -93,13 +93,13 @@ Run it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.s ### 7. GCP authentication -Ask the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run: +GCP credentials are mounted from the host's `~/.config/gcloud` directory. If the user hasn't authenticated on the host yet, tell them to run on the host (not inside the container): -``` -! podman exec -it sippy-dev bash -c "gcloud auth application-default login" +```bash +gcloud auth application-default login ``` -This must be done interactively by the user — it opens a browser for OAuth. +The credentials will be available inside the container automatically after restart. ### 8. Summary diff --git a/.claude/commands/sippy-dev-setup.md b/.claude/commands/sippy-dev-setup.md index 4e2cb90a2..0e4f11dd4 100644 --- a/.claude/commands/sippy-dev-setup.md +++ b/.claude/commands/sippy-dev-setup.md @@ -93,13 +93,13 @@ Run it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.s ### 7. GCP authentication -Ask the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run: +GCP credentials are mounted from the host's `~/.config/gcloud` directory. If the user hasn't authenticated on the host yet, tell them to run on the host (not inside the container): -``` -! podman exec -it sippy-dev bash -c "gcloud auth application-default login" +```bash +gcloud auth application-default login ``` -This must be done interactively by the user — it opens a browser for OAuth. +The credentials will be available inside the container automatically after restart. ### 8. Summary diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..55a5ff820 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,3 @@ +{ + "model": "claude-opus-4-6" +} diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 44e7a688d..e33a87fb8 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -13,7 +13,7 @@ you through GCP authentication. ## What you'll need to provide - **GCP project ID** (`ANTHROPIC_VERTEX_PROJECT_ID`) — for Claude Code via Vertex AI -- **GCP auth** — the setup will prompt you to run `gcloud auth application-default login` +- **GCP auth** — run `gcloud auth application-default login` on the host before starting the container (credentials are mounted read-only) ## Manual setup diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e27b3e32c..6c0ff96dd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,6 +7,9 @@ "dockerfile": "Dockerfile" }, "runArgs": ["--name", "sippy-dev", "--network", "sippy-net", "--env-file", ".devcontainer/.env", "-p", "8080:8080", "-p", "3000:3000"], + "mounts": [ + "source=${localEnv:HOME}/.config/gcloud,target=/home/vscode/.config/gcloud,type=bind,readonly" + ], "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", "workspaceFolder": "/workspace", "remoteUser": "vscode", diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 3a3588ba8..2745025d5 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -17,13 +17,4 @@ python3.12 -m venv mcp/.venv mcp/.venv/bin/pip install --upgrade pip -q mcp/.venv/bin/pip install -r mcp/requirements.txt -q -echo "==> Checking GCP auth..." -if command -v gcloud >/dev/null 2>&1; then - if ! gcloud auth application-default print-access-token >/dev/null 2>&1; then - echo " GCP credentials not found. Run 'gcloud auth application-default login' to authenticate." - fi -else - echo " gcloud not found — skipping auth check." -fi - echo "==> Dev environment ready." diff --git a/.gemini/commands/sippy-dev-setup.toml b/.gemini/commands/sippy-dev-setup.toml index dbe7b7c97..01497d026 100644 --- a/.gemini/commands/sippy-dev-setup.toml +++ b/.gemini/commands/sippy-dev-setup.toml @@ -1,2 +1,2 @@ description = "Set up the Sippy devcontainer (Podman, env, GCP auth)" -prompt = "# Sippy dev — devcontainer setup\n\nInteractive setup for the Sippy devcontainer. Automates what can be detected, prompts for what can't.\n\n## Workflow\n\n### 1. Detect OS\n\n```bash\nuname -s\n```\n\n- **Darwin** = macOS\n- **Linux** = Linux\n\n### 2. Check prerequisites\n\nVerify each tool is installed. Report any that are missing and stop.\n\n```bash\ncommand -v podman\ncommand -v devcontainer\n```\n\nIf `devcontainer` is missing, tell the user: `npm install -g @devcontainers/cli`\n\n### 3. macOS: Podman machine\n\nIf macOS, check if podman machine is running:\n\n```bash\npodman machine info\n```\n\nIf not initialized or not running, run:\n\n```bash\npodman machine init # only if no machine exists\npodman machine start\n```\n\n### 4. Linux: Podman socket\n\nIf Linux, check if the socket is active:\n\n```bash\nsystemctl --user is-active podman.socket\n```\n\nIf not active, run:\n\n```bash\nsystemctl --user enable --now podman.socket\n```\n\nAlso check for `podman-docker`:\n\n```bash\ncommand -v docker\n```\n\nIf missing, suggest installing `podman-docker` (dnf or apt depending on `/etc/os-release`).\n\n### 5. Environment file\n\nCheck if `.devcontainer/.env` exists:\n\n```bash\ntest -f .devcontainer/.env\n```\n\nIf missing, copy from the example:\n\n```bash\ncp .devcontainer/.env.example .devcontainer/.env\n```\n\nThen read `.devcontainer/.env` and check for empty required values. If any are blank (e.g. `ANTHROPIC_VERTEX_PROJECT_ID`), tell the user which values need to be filled in and ask them to edit `.devcontainer/.env` and let you know when they're done. **Do not** ask for the values directly or write to the file yourself — the user should edit it. Wait for them to confirm before continuing.\n\n### 6. Start the container\n\nDetermine the right command based on OS:\n\n- **macOS**: `devcontainer up --workspace-folder . --docker-path podman`\n- **Linux** (with `podman-docker`): `devcontainer up --workspace-folder .`\n- **Linux** (without `podman-docker`): `devcontainer up --workspace-folder . --docker-path podman`\n\nRun it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.sh` (Go tools, npm, MCP venv).\n\n### 7. GCP authentication\n\nAsk the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run:\n\n```\n! podman exec -it sippy-dev bash -c \"gcloud auth application-default login\"\n```\n\nThis must be done interactively by the user — it opens a browser for OAuth.\n\n### 8. Summary\n\nPrint a summary of what was set up:\n\n- Container status\n- PostgreSQL: `localhost:5432`\n- Redis: `localhost:6379`\n- API server: `localhost:8080` (start with `/sippy-dev-serve`)\n- React dev server: `localhost:3000` (start with `/sippy-dev-frontend`)\n- GCP auth status" +prompt = "# Sippy dev — devcontainer setup\n\nInteractive setup for the Sippy devcontainer. Automates what can be detected, prompts for what can't.\n\n## Workflow\n\n### 1. Detect OS\n\n```bash\nuname -s\n```\n\n- **Darwin** = macOS\n- **Linux** = Linux\n\n### 2. Check prerequisites\n\nVerify each tool is installed. Report any that are missing and stop.\n\n```bash\ncommand -v podman\ncommand -v devcontainer\n```\n\nIf `devcontainer` is missing, tell the user: `npm install -g @devcontainers/cli`\n\n### 3. macOS: Podman machine\n\nIf macOS, check if podman machine is running:\n\n```bash\npodman machine info\n```\n\nIf not initialized or not running, run:\n\n```bash\npodman machine init # only if no machine exists\npodman machine start\n```\n\n### 4. Linux: Podman socket\n\nIf Linux, check if the socket is active:\n\n```bash\nsystemctl --user is-active podman.socket\n```\n\nIf not active, run:\n\n```bash\nsystemctl --user enable --now podman.socket\n```\n\nAlso check for `podman-docker`:\n\n```bash\ncommand -v docker\n```\n\nIf missing, suggest installing `podman-docker` (dnf or apt depending on `/etc/os-release`).\n\n### 5. Environment file\n\nCheck if `.devcontainer/.env` exists:\n\n```bash\ntest -f .devcontainer/.env\n```\n\nIf missing, copy from the example:\n\n```bash\ncp .devcontainer/.env.example .devcontainer/.env\n```\n\nThen read `.devcontainer/.env` and check for empty required values. If any are blank (e.g. `ANTHROPIC_VERTEX_PROJECT_ID`), tell the user which values need to be filled in and ask them to edit `.devcontainer/.env` and let you know when they're done. **Do not** ask for the values directly or write to the file yourself — the user should edit it. Wait for them to confirm before continuing.\n\n### 6. Start the container\n\nDetermine the right command based on OS:\n\n- **macOS**: `devcontainer up --workspace-folder . --docker-path podman`\n- **Linux** (with `podman-docker`): `devcontainer up --workspace-folder .`\n- **Linux** (without `podman-docker`): `devcontainer up --workspace-folder . --docker-path podman`\n\nRun it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.sh` (Go tools, npm, MCP venv).\n\n### 7. GCP authentication\n\nGCP credentials are mounted from the host's `~/.config/gcloud` directory. If the user hasn't authenticated on the host yet, tell them to run on the host (not inside the container):\n\n```bash\ngcloud auth application-default login\n```\n\nThe credentials will be available inside the container automatically after restart.\n\n### 8. Summary\n\nPrint a summary of what was set up:\n\n- Container status\n- PostgreSQL: `localhost:5432`\n- Redis: `localhost:6379`\n- API server: `localhost:8080` (start with `/sippy-dev-serve`)\n- React dev server: `localhost:3000` (start with `/sippy-dev-frontend`)\n- GCP auth status" diff --git a/.opencode/commands/sippy-dev-setup.md b/.opencode/commands/sippy-dev-setup.md index 4e2cb90a2..0e4f11dd4 100644 --- a/.opencode/commands/sippy-dev-setup.md +++ b/.opencode/commands/sippy-dev-setup.md @@ -93,13 +93,13 @@ Run it. This triggers `init-services.sh` (PostgreSQL + Redis) and `post-create.s ### 7. GCP authentication -Ask the user if they need to authenticate with GCP (for BigQuery/GCS access). If yes, tell them to run: +GCP credentials are mounted from the host's `~/.config/gcloud` directory. If the user hasn't authenticated on the host yet, tell them to run on the host (not inside the container): -``` -! podman exec -it sippy-dev bash -c "gcloud auth application-default login" +```bash +gcloud auth application-default login ``` -This must be done interactively by the user — it opens a browser for OAuth. +The credentials will be available inside the container automatically after restart. ### 8. Summary diff --git a/apm.lock.yaml b/apm.lock.yaml index 447901362..d7ab533ab 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -51,7 +51,7 @@ local_deployed_file_hashes: .claude/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b .claude/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 .claude/commands/sippy-dev-serve.md: sha256:0571085d7f4017334c08a6feadd94ab7ba016cce0556dbd9999c93b5449f420a - .claude/commands/sippy-dev-setup.md: sha256:613a5d3c0ba2cebb3d4fd5b9d12863ccf1c4c22adf0f724cfdddc53191a63b76 + .claude/commands/sippy-dev-setup.md: sha256:276c708371bebcdbe8602df22ca482e59c2eab0bbc7ecacf1b49acd0a99d966f .claude/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .claude/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .claude/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0 @@ -73,7 +73,7 @@ local_deployed_file_hashes: .gemini/commands/sippy-dev-migrate.toml: sha256:e699558eb27c294327a092bfcd3e76b576087ad29d9a3a9b000cfd9e7c6b4774 .gemini/commands/sippy-dev-regression-cache.toml: sha256:cd2c9017e5e976750839c712b42d10046d1d04fabaa749ffda6fd1abaf776d7a .gemini/commands/sippy-dev-serve.toml: sha256:774bedf16a0ac5c2c464a29c8264d2588c48eb2a1d8380e694e22f09c6a95715 - .gemini/commands/sippy-dev-setup.toml: sha256:f99c93d6570c06612c90c21c84b1b335a640e9fb4e586c474e8f38f98f513013 + .gemini/commands/sippy-dev-setup.toml: sha256:bb56074029f5a73075f7d8b5e971ad2621d0517ea29a90bbb1e11f989bd6295c .gemini/commands/sippy-dev-tests.toml: sha256:0d19aaff911d478859fc948650a92e46142071a8c5d1a29c5b40c25d1d1e495b .gemini/commands/sippy-generate-release-views.toml: sha256:4759f4547c05631d2e83a5859172421ea3d6ec794dd887756a90bbe0fd2732c1 .gemini/commands/sippy-update-ga-release-views.toml: sha256:71a60dfe068bdc59645b8d0069c93c65f2df30c5172da3389df2a80d86410ae9 @@ -83,7 +83,7 @@ local_deployed_file_hashes: .opencode/commands/sippy-dev-migrate.md: sha256:5704945283027398bbe4a54eb4d97295059bd64a47e86e97a58665338ec62c8b .opencode/commands/sippy-dev-regression-cache.md: sha256:4e9fc3ed63927c84c30554929768a3b0e760264925f55c9dbbdafabf2361bee2 .opencode/commands/sippy-dev-serve.md: sha256:0571085d7f4017334c08a6feadd94ab7ba016cce0556dbd9999c93b5449f420a - .opencode/commands/sippy-dev-setup.md: sha256:613a5d3c0ba2cebb3d4fd5b9d12863ccf1c4c22adf0f724cfdddc53191a63b76 + .opencode/commands/sippy-dev-setup.md: sha256:276c708371bebcdbe8602df22ca482e59c2eab0bbc7ecacf1b49acd0a99d966f .opencode/commands/sippy-dev-tests.md: sha256:2f631ca56790cd9c4441e60ee1e1e2deff5a32bf7517c051d053b8e645242a64 .opencode/commands/sippy-generate-release-views.md: sha256:e8a283e43f84777e6da34c10f1b18de2c41baa4525a7e40317bddd0c61b75553 .opencode/commands/sippy-update-ga-release-views.md: sha256:c7adb0f649455fa7c12c94454220fc87f182d49b820a2683ca733401cbfeb9b0