diff --git a/.claude/skills/docs-start/SKILL.md b/.claude/skills/docs-start/SKILL.md new file mode 100644 index 000000000..44c04d013 --- /dev/null +++ b/.claude/skills/docs-start/SKILL.md @@ -0,0 +1,172 @@ +--- +name: docs-start +description: Start the La Suite Docs development stack with the least work possible. Use when asked to start, run, launch, boot or bring up Docs, the dev stack or the backend, after a clone, a pull or a branch switch. Checks what is built, stale and running, then runs only the Makefile targets needed (bootstrap only on a first run, rebuilds only the images whose dependencies changed), and copes with a frontend served by a local `yarn dev`. +--- + +# Start the Docs stack + +The goal is a running stack for the smallest cost: some contributors have a +slow connection or little memory. `make bootstrap` and every `make build-*` +are expensive, so run them only when the check below says so. `make run` is +usually all that is needed. + +## 1. Check first, change nothing + +```bash +bash .claude/skills/docs-start/check-stack.sh +``` + +Run it with the Bash tool (Git Bash on Windows). It is read-only and takes +around 20 seconds. It reports: + +- the Docker daemon, the `lasuite-network` network, the `env.d/development/*.local` files and the JWT keys, +- **how the frontend is served**: the `frontend-development` container, a host + process on :3000 (a local `yarn dev`: `mode: local`), or nothing yet, +- each dev image: missing, stale or up to date, +- containers whose **dependency volumes** are older than their image, +- the services' state, the two databases and the endpoints, +- a **suggested plan**, plus notes. + +Read the plan before running it. It is a suggestion; the rules below explain +it and take precedence. + +## 2. Rules behind the plan + +**First run.** When no dev image and no `postgresql` container exist (a fresh +clone), run `make bootstrap`. It creates the network, the `.local` files and +the keys, builds every image, runs the migrations, builds the mails, starts +everything and seeds the demo data. Never run it on a stack that is already +set up. It rebuilds everything, flushes the database (`make demo` runs +`resetdb`), and `generate-secret-keys` appends one more +`OIDC_STORE_REFRESH_TOKEN_KEY` to `common.local` on every run. + +**An image is stale only when its dependencies changed.** Every dev container +bind-mounts its sources, so code changes never need a rebuild. Only these do: + +| image | make target | stale when these change | services | +|---|---|---|---| +| `impress:backend-development` | `make build-backend` | `src/backend/uv.lock`, `pyproject.toml`, root `Dockerfile` | `app-dev`, `celery-dev` | +| `impress:yhub-development` | `make build-yhub` | `src/yhub-server/yarn.lock`, `package.json`, its `Dockerfile` | `yhub` | +| `impress:y-provider-development` | `make build-yjs-provider` | `src/frontend/yarn.lock`, the root, y-provider and eslint-plugin `package.json`, its `Dockerfile` | `y-provider-development-converter` | +| `impress:frontend-development` | `make build-frontend` | `src/frontend/yarn.lock`, the root, impress and eslint-plugin `package.json`, `src/frontend/Dockerfile` | `frontend-development` | + +Build only the stale images, one target each. Never use `make build`: it +rebuilds all four. When the frontend runs locally, never build the frontend +image. + +**Some containers need `--renew-anon-volumes`.** `/app/.venv` and the +`node_modules` directories are anonymous volumes. `docker compose up +--force-recreate`, which is what `make run` does, carries them over to the new +container. After a rebuild, the container can therefore keep running the *old* +dependencies. The check reports it ("STALE dependency volume(s)"), and the +plan renews them for those services only: + +```bash +DOCKER_USER= docker compose up -d --force-recreate --renew-anon-volumes --no-deps +``` + +- Keep `--no-deps`. Without it, a dependency recreated in the same `up` gets + its volumes renewed too, and `postgresql` and `kc_postgresql` keep their + data in anonymous volumes. +- Each renewal leaves the old volume dangling. `docker volume ls -f + dangling=true` lists them. Suggest a cleanup to the user; never prune + yourself. +- Always pass `DOCKER_USER` as the plan prints it: `0:0` on Windows, `uid:gid` + elsewhere. Without it, compose runs the containers as `1000` and the + bind-mounted files become unwritable. + +**The frontend.** + +- `mode: local`: something already answers on :3000 from the host, usually + the user's own `yarn dev`. This is expected. Run `make run-backend`, not + `make run`: `make run` would try to start the `frontend-development` + container and fail with "port 3000 is already allocated". Tell the user the + frontend on :3000 is theirs and was left alone. +- `mode: undecided`: nothing on :3000, but `src/frontend/node_modules` exists + locally. Ask the user whether to run the frontend locally or in Docker. + - Locally: `make run-backend`, then `make run-frontend-development`. That + target blocks, so start it with `run_in_background`. It stops the + container and runs `yarn dev`. + - In Docker: build the frontend image if it is missing or stale, then run + `make run`. +- `mode: container`: `make run`. +- When the check says the local `node_modules` are out of sync, tell the user + and offer `cd src/frontend && yarn install`. It mostly hits the yarn cache. + +**Already up.** When every backend service is running and answering, the plan +skips `make run`/`make run-backend` and only recreates what changed. Those +targets force-recreate every container and would drop the open collaboration +websockets for nothing. With nothing to change, the plan says so. Report that +and stop. + +**Databases.** + +- `make migrate` when migrations are pending. +- On an empty backend database (containers removed with `make down`: the + `postgresql` data lives in an anonymous volume), run `make migrate` then + `make demo`. +- `make migrate-yhub` after rebuilding yhub, or when its database is empty. + It is idempotent: a `BUSYGROUP` error in its output is harmless. + +## 3. Run the plan + +- Use the Bash tool from the repository root and the Makefile targets as + written. +- Builds and `make bootstrap` can exceed the 2-minute default. Pass + `timeout: 600000`, or use `run_in_background` for `make bootstrap`. +- Before `make bootstrap`, a rebuild, or anything else long, tell the user in + one line what you are about to run and why ("backend image stale: + `uv.lock` changed"). +- Never run `make down`, `make clean`, `docker compose down`, `docker system + prune` or `--no-cache` builds to "start clean". They lose data or the build + cache, and a slow connection pays for it. + +## 4. Verify, then report + +Run `check-stack.sh` again. The endpoints must answer `200` and the plan must +say "Nothing to do". Then give the user a short summary: + +- what was run, and why, +- the URLs: frontend http://localhost:3000, API/admin http://localhost:8071, + Keycloak http://localhost:8083, yhub http://localhost:3002, mails + http://localhost:1081, MinIO http://localhost:9001, +- the logins: `impress` / `impress` for the app (Keycloak); `admin@example.com` + / `admin` for the Django admin once `make demo` or `make superuser` has run, +- the notes from the check (local frontend, duplicated keys, drifted + `node_modules`). + +## Troubleshooting + +- **A service keeps restarting**: `docker compose logs --tail 80 `. + If the traceback shows a missing module or a missing export after a pull, + the dependencies are stale. The check tells whether the image or only the + volumes are behind. A container out of sync with its volumes is often fixed + by the `--renew-anon-volumes --no-deps` command above, even when the check + sees nothing. +- **`env file ... .local not found`**: run `make create-env-local-files`. A + new `.local` file was added to `compose.yml` since the last bootstrap. +- **yhub `/collaboration/ready/v1` answers `503`**: its postgres or valkey is + down (`docker compose ps yhub-postgres yhub-valkey`). If the log names a + missing relation, run `make migrate-yhub`. +- **Edits to `docker/auth/realm.json` have no effect** (`invalid_scope`, + `client_not_found`): Keycloak imports the realm only once, and its + `kc_postgresql` keeps it in an anonymous volume. Run `docker compose up -d + --force-recreate --renew-anon-volumes kc_postgresql keycloak` (with + `DOCKER_USER`). Everyone will have to log in again. +- **The Docker daemon is not reachable**: ask the user to start Docker + Desktop. Do not try to start it yourself. + +## Improvements + +Whenever you notice something that would make starting the stack cheaper or +more reliable, tell the user and **ask before proposing a change**. This +covers: + +- the Makefile: a target that does too much, a missing guard, a non-idempotent + step, +- `compose.yml` or the Dockerfiles, +- this skill or `check-stack.sh`: a false positive or negative in the + staleness check, a new service, a new image, a new `.local` file. + +Keep the image table above and the `IMAGES` list in `check-stack.sh` in step +with `compose.yml` and the Dockerfiles. diff --git a/.claude/skills/docs-start/check-stack.sh b/.claude/skills/docs-start/check-stack.sh new file mode 100644 index 000000000..964902dd9 --- /dev/null +++ b/.claude/skills/docs-start/check-stack.sh @@ -0,0 +1,348 @@ +#!/usr/bin/env bash +# Read-only report on the Docs development stack, for the docs-start skill. +# +# It changes nothing: no build, no pull, no container started or stopped. +# It prints what is missing, stale or running, then a suggested plan made of +# Makefile targets where one exists. Run it from anywhere in the repository: +# +# bash .claude/skills/docs-start/check-stack.sh +# +# Works in Git Bash (Windows), Linux and macOS. + +# no `set -u`: empty arrays are "unbound" under bash 3.2 (macOS) +export MSYS_NO_PATHCONV=1 # keep Git Bash from rewriting /app/... paths + +ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { + echo "Not inside the Docs repository" + exit 2 +} +cd "$ROOT" || exit 2 + +# Same user the Makefile runs the containers with: plain `docker compose` +# commands have to pass it too, or the bind-mounted files end up unwritable. +if [ "${OS:-}" = "Windows_NT" ]; then + DOCKER_USER="0:0" +else + DOCKER_USER="$(id -u):$(id -g)" +fi +COMPOSE="DOCKER_USER=$DOCKER_USER docker compose" + +PLAN=() +NOTES=() +plan() { PLAN+=("$1"); } +note() { NOTES+=("$1"); } + +# -- portable helpers --------------------------------------------------------- + +to_epoch() { # docker ISO 8601 date, "...Z" or "...+02:00" -> seconds + date -d "$1" +%s 2>/dev/null && return + # BSD date (macOS): no fractional seconds, and the offset spelled +hhmm + local tz + tz=$(echo "${1:19}" | sed 's/^\.[0-9]*//; s/^Z$/+00:00/; s/://') + date -j -f '%Y-%m-%dT%H:%M:%S%z' "${1:0:19}${tz}" +%s 2>/dev/null || echo 0 +} +mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; } +fhash() { tr -d '\r' | git hash-object --stdin; } +img_cat() { docker run --rm --entrypoint cat "$1" "$2" 2>/dev/null; } +head_at() { # commit HEAD pointed to at a given epoch, from the reflog + git log -g --date=unix --format='%gd %H' HEAD 2>/dev/null | + sed -n 's/^HEAD@{\([0-9]*\)} \(.*\)/\1 \2/p' | + awk -v t="$1" '$1 <= t { print $2; exit }' +} + +# -- docker ------------------------------------------------------------------- + +echo "== Docker" +if ! docker info >/dev/null 2>&1; then + echo "daemon: NOT REACHABLE" + echo + echo "== Suggested plan" + echo "1. Start Docker (Docker Desktop on Windows/macOS), then run this check again" + exit 1 +fi +echo "daemon: ok" +if docker network inspect lasuite-network >/dev/null 2>&1; then + echo "network lasuite-network: ok" +else + echo "network lasuite-network: MISSING" +fi + +# -- local setup files -------------------------------------------------------- + +echo +echo "== Local setup files" +missing_env=() +for f in $(sed -n 's#^[[:space:]]*@touch \(env\.d/development/[^[:space:]]*\.local\).*#\1#p' Makefile); do + [ -f "$f" ] || missing_env+=("$f") +done +if [ ${#missing_env[@]} -eq 0 ]; then + echo "env.d/development/*.local: ok" +else + echo "env.d/development/*.local MISSING: ${missing_env[*]}" +fi +for k in data/jwt/private.pem data/jwt/yhub-private.pem; do + if [ -f "$k" ]; then echo "$k: ok"; else echo "$k: MISSING (make run-backend creates it)"; fi +done +oidc_keys=0 +[ -f env.d/development/common.local ] && + oidc_keys=$(grep -c '^OIDC_STORE_REFRESH_TOKEN_KEY=' env.d/development/common.local) +echo "OIDC_STORE_REFRESH_TOKEN_KEY in common.local: $oidc_keys" +[ "$oidc_keys" -gt 1 ] && + note "common.local holds $oidc_keys OIDC_STORE_REFRESH_TOKEN_KEY lines (one per bootstrap, the last one wins): harmless but worth cleaning" + +# -- containers --------------------------------------------------------------- + +PS=$(docker compose ps -a --format '{{.Service}} {{.State}}' 2>/dev/null) +state() { # no associative array: macOS still ships bash 3.2 + local s + s=$(echo "$PS" | awk -v svc="$1" '$1 == svc { print $2; exit }') + echo "${s:-absent}" +} + +# -- frontend mode ------------------------------------------------------------ + +echo +echo "== Frontend (:3000)" +code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 http://localhost:3000/ 2>/dev/null) +fe_state=$(state frontend-development) +if [ "$fe_state" = "running" ]; then + FRONTEND_MODE=container + echo "served by the frontend-development container (http $code)" +elif [ "$code" != "000" ] && [ -n "$code" ]; then + FRONTEND_MODE=local + echo "served by a process on the host, not by docker (http $code): local \`yarn dev\`" +elif [ -d src/frontend/node_modules ]; then + FRONTEND_MODE=undecided + echo "nothing on :3000; the frontend is installed locally (src/frontend/node_modules)" +else + FRONTEND_MODE=container + echo "nothing on :3000; no local install, the container will serve it" +fi +echo "mode: $FRONTEND_MODE" +if [ -d src/frontend/node_modules ] && command -v yarn >/dev/null 2>&1; then + if (cd src/frontend && yarn check --integrity >/dev/null 2>&1); then + echo "local node_modules: in sync with yarn.lock" + else + echo "local node_modules: OUT OF SYNC with package.json/yarn.lock" + [ "$FRONTEND_MODE" != "container" ] && + note "local frontend deps drifted: \`cd src/frontend && yarn install\` before \`yarn dev\` (uses the yarn cache, light on the network)" + fi +fi + +# -- images ------------------------------------------------------------------- +# A dev image only goes stale when its dependency manifests or its Dockerfile +# change: the sources are bind-mounted. A file on disk newer than the image is +# a candidate; it is confirmed by comparing it with the copy baked in the image. +# +# spec: name|image|make target|services|image:local pairs|subset lockfile|dockerfile +IMAGES=( + "backend|impress:backend-development|build-backend|app-dev celery-dev|/app/uv.lock:src/backend/uv.lock /app/pyproject.toml:src/backend/pyproject.toml||Dockerfile" + "yhub|impress:yhub-development|build-yhub|yhub|/app/yarn.lock:src/yhub-server/yarn.lock /app/package.json:src/yhub-server/package.json||src/yhub-server/Dockerfile" + # the y-provider Dockerfile runs `yarn install` without --frozen-lockfile, + # which prunes yarn.lock inside the image: check it is a subset of ours + "y-provider|impress:y-provider-development|build-yjs-provider|y-provider-development-converter|/home/frontend/package.json:src/frontend/package.json /home/frontend/servers/y-provider/package.json:src/frontend/servers/y-provider/package.json /home/frontend/packages/eslint-plugin-docs/package.json:src/frontend/packages/eslint-plugin-docs/package.json|/home/frontend/yarn.lock:src/frontend/yarn.lock|src/frontend/servers/y-provider/Dockerfile" + "frontend|impress:frontend-development|build-frontend|frontend-development|/home/frontend/yarn.lock:src/frontend/yarn.lock /home/frontend/package.json:src/frontend/package.json /home/frontend/apps/impress/package.json:src/frontend/apps/impress/package.json /home/frontend/packages/eslint-plugin-docs/package.json:src/frontend/packages/eslint-plugin-docs/package.json||src/frontend/Dockerfile" +) + +echo +echo "== Images" +images_missing=0 +BUILD=() +RENEW=() +for spec in "${IMAGES[@]}"; do + IFS='|' read -r name image target services pairs subset dockerfile <<<"$spec" + if [ "$name" = "frontend" ] && [ "$FRONTEND_MODE" = "local" ]; then + echo "$name: skipped (frontend runs locally)" + continue + fi + created=$(docker image inspect -f '{{.Created}}' "$image" 2>/dev/null) + if [ -z "$created" ]; then + echo "$name ($image): MISSING -> make $target" + images_missing=$((images_missing + 1)) + BUILD+=("$name:$target") + continue + fi + ctime=$(to_epoch "$created") + reasons=() + for pair in $pairs; do + in=${pair%%:*} + loc=${pair#*:} + [ "$(mtime "$loc")" -gt "$ctime" ] || continue + [ "$(img_cat "$image" "$in" | fhash)" = "$(fhash <"$loc")" ] || reasons+=("$loc changed") + done + if [ -n "$subset" ]; then + in=${subset%%:*} + loc=${subset#*:} + if [ "$(mtime "$loc")" -gt "$ctime" ]; then + extra=$(comm -23 \ + <(img_cat "$image" "$in" | tr -d '\r' | grep '^ resolved ' | sort -u) \ + <(tr -d '\r' <"$loc" | grep '^ resolved ' | sort -u) | wc -l) + [ "$extra" -eq 0 ] || reasons+=("$loc changed ($extra entries)") + fi + fi + # the Dockerfile is not in the image: diff it against the commit checked + # out when the image was built (a branch switch touches files it does not + # change), and trust the mtime alone only when the reflog does not go back + if [ "$(mtime "$dockerfile")" -gt "$ctime" ]; then + built_from=$(head_at "$ctime") + if [ -z "$built_from" ]; then + reasons+=("$dockerfile touched since the build (no reflog to confirm)") + elif ! git diff --quiet "$built_from" -- "$dockerfile" 2>/dev/null; then + reasons+=("$dockerfile changed since the build") + fi + fi + if [ ${#reasons[@]} -gt 0 ]; then + echo "$name ($image, built ${created%%.*}): STALE -> make $target" + printf ' - %s\n' "${reasons[@]}" + BUILD+=("$name:$target") + else + echo "$name ($image, built ${created%%.*}): up to date" + fi +done + +# -- containers vs images ----------------------------------------------------- +# `docker compose up --force-recreate` (what `make run` does) keeps each +# container's anonymous volumes. The dependencies installed in the image live +# in them (/app/.venv, node_modules): a volume older than the image hides what +# the image installed, so the container keeps running the old dependencies. + +echo +echo "== Dependency volumes of the running containers" +for spec in "${IMAGES[@]}"; do + IFS='|' read -r name image target services _ <<<"$spec" + [ "$name" = "frontend" ] && [ "$FRONTEND_MODE" != "container" ] && continue + created=$(docker image inspect -f '{{.Created}}' "$image" 2>/dev/null) || continue + ctime=$(to_epoch "$created") + for svc in $services; do + cid=$(docker compose ps -a -q "$svc" 2>/dev/null | head -1) + [ -z "$cid" ] && { echo "$svc: no container yet"; continue; } + stale="" + for vol in $(docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}={{.Destination}} {{end}}{{end}}' "$cid"); do + vcreated=$(docker volume inspect -f '{{.CreatedAt}}' "${vol%%=*}" 2>/dev/null) + [ -n "$vcreated" ] && [ "$(to_epoch "$vcreated")" -lt "$ctime" ] && stale+=" ${vol#*=}" + done + if [ -n "$stale" ]; then + echo "$svc: STALE dependency volume(s):$stale (older than $image)" + RENEW+=("$svc") + else + echo "$svc: ok" + fi + done +done + +# -- services and databases --------------------------------------------------- + +echo +echo "== Services" +for svc in postgresql redis minio keycloak kc_postgresql nginx app-dev celery-dev yhub yhub-postgres yhub-valkey y-provider-development-converter docspec mailcatcher; do + printf '%-34s %s\n' "$svc" "$(state "$svc")" +done + +echo +echo "== Databases" +app_db=unknown +if [ "$(state postgresql)" = "running" ]; then + n=$(echo "select count(*) from information_schema.tables where table_name = 'django_migrations';" | + docker compose exec -T postgresql sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tA' 2>/dev/null | tr -d '\r[:space:]') + if [ "$n" = "1" ]; then app_db=migrated; elif [ "$n" = "0" ]; then app_db=empty; fi +fi +echo "backend database: $app_db" +pending_migrations=0 +if [ "$app_db" = "migrated" ] && [ "$(state app-dev)" = "running" ]; then + if docker compose exec -T app-dev python manage.py migrate --check >/dev/null 2>&1; then + echo "backend migrations: all applied" + else + echo "backend migrations: PENDING (or the check failed) -> make migrate" + pending_migrations=1 + fi +fi +yhub_db=unknown +if [ "$(state yhub-postgres)" = "running" ]; then + n=$(echo "select count(*) from pg_tables where schemaname not in ('pg_catalog', 'information_schema');" | + docker compose exec -T yhub-postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tA' 2>/dev/null | tr -d '\r[:space:]') + if [ -n "$n" ] && [ "$n" -gt 0 ] 2>/dev/null; then yhub_db=migrated; elif [ "$n" = "0" ]; then yhub_db=empty; fi +fi +echo "yhub database: $yhub_db" + +echo +echo "== Endpoints" +endpoints_ok=1 +probe() { + local c + c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$2" 2>/dev/null) + printf '%-40s %s\n' "$1" "$c" + [ "$c" = "200" ] || [ "$3" = "optional" ] || endpoints_ok=0 +} +probe "backend :8071/api/v1.0/config/" http://localhost:8071/api/v1.0/config/ +probe "yhub :3002/collaboration/ready/v1" http://localhost:3002/collaboration/ready/v1 +probe "keycloak :8083 (via nginx)" http://localhost:8083/realms/impress/.well-known/openid-configuration +probe "frontend :3000" http://localhost:3000/ optional + +# up = everything `make run-backend` starts is running and answering +stack_up=$endpoints_ok +for svc in app-dev celery-dev yhub y-provider-development-converter nginx docspec; do + [ "$(state "$svc")" = "running" ] || stack_up=0 +done + +# -- plan --------------------------------------------------------------------- + +any_image=0 +for spec in "${IMAGES[@]}"; do + IFS='|' read -r _ image _ <<<"$spec" + docker image inspect "$image" >/dev/null 2>&1 && any_image=1 +done + +echo +echo "== Suggested plan" +if [ "$any_image" -eq 0 ] && [ "$(state postgresql)" = "absent" ]; then + echo "1. make bootstrap # first run: nothing is built yet" + [ "$FRONTEND_MODE" = "local" ] && + echo " (the frontend already runs on the host: its container will be built but will not start, :3000 is taken)" +else + docker network inspect lasuite-network >/dev/null 2>&1 || plan "make create-docker-network" + [ ${#missing_env[@]} -gt 0 ] && plan "make create-env-local-files" + if [ "$FRONTEND_MODE" = "local" ] || [ "$FRONTEND_MODE" = "undecided" ]; then + for b in "${BUILD[@]}"; do [ "${b%%:*}" = "frontend" ] || plan "make ${b#*:}"; done + # already up: only what changed is recreated below, `make run-backend` + # would restart every container and drop the open websockets for nothing + [ "$stack_up" -eq 1 ] || plan "make run-backend" + else + for b in "${BUILD[@]}"; do plan "make ${b#*:}"; done + [ "$stack_up" -eq 1 ] && [ "$(state frontend-development)" = "running" ] || plan "make run" + fi + # services whose image gets rebuilt, and those already on stale volumes, + # must be recreated with fresh anonymous volumes: `make run` keeps them. + # --no-deps: --renew-anon-volumes would otherwise also renew the volumes of + # a dependency recreated in the same run, postgresql's data included + for b in "${BUILD[@]}"; do + for spec in "${IMAGES[@]}"; do + IFS='|' read -r name _ _ services _ <<<"$spec" + [ "$name" = "${b%%:*}" ] || continue + [ "$name" = "frontend" ] && [ "$FRONTEND_MODE" != "container" ] && continue + for s in $services; do RENEW+=("$s"); done + done + done + if [ ${#RENEW[@]} -gt 0 ]; then + uniq_renew=$(printf '%s\n' "${RENEW[@]}" | sort -u | tr '\n' ' ') + plan "$COMPOSE up -d --force-recreate --renew-anon-volumes --no-deps ${uniq_renew% }" + fi + for b in "${BUILD[@]}"; do [ "${b%%:*}" = "yhub" ] && plan "make migrate-yhub"; done + [ "$pending_migrations" -eq 1 ] && plan "make migrate" + [ "$app_db" = "empty" ] && plan "make migrate" && plan "make demo # empty database: seeds the demo data, flushes the DB first" + [ "$yhub_db" = "empty" ] && plan "make migrate-yhub" + [ "$FRONTEND_MODE" = "undecided" ] && + plan "ASK the user: frontend locally (make run-frontend-development, in the background) or in docker (make build-frontend if needed, then make run)" + [ ${#PLAN[@]} -eq 0 ] && echo "Nothing to do: the stack is up and current." + # a `make migrate` asked for twice (pending + empty) is run once + i=1 + printf '%s\n' "${PLAN[@]}" | awk '!seen[$0]++' | while read -r step; do + echo "$i. $step" + i=$((i + 1)) + done +fi + +if [ ${#NOTES[@]} -gt 0 ]; then + echo + echo "== Notes" + printf -- '- %s\n' "${NOTES[@]}" +fi diff --git a/README.md b/README.md index 790bad8d2..5b8f04b6c 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,13 @@ docker compose version ### Bootstrap the project -The easiest way to start is using GNU Make: +The easiest way to start is using the Claude `/docs-start` skill. + +It checks what is already built, stale or running, then runs only the Make +targets needed: `make bootstrap` on a first run, the rebuild of the images +whose dependencies changed after a pull, or just `make run`. + +Otherwise, bootstrap everything with GNU Make: ```bash make bootstrap FLUSH_ARGS='--no-input'