Make Setup Command Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a root make setup bootstrap flow for new git worktrees that installs dependencies, prepares local env files, starts infra, waits for PostgreSQL, runs backend migrations, and seeds demo data only when the shared local database has no organizations yet.
Architecture: Keep the root Makefile as a thin command surface and move all orchestration into one Bash entrypoint at scripts/setup.sh. The script should use Docker health checks plus a PostgreSQL sentinel query against organizations so rerunning setup in another worktree remains fail-fast and idempotent.
Tech Stack: GNU Make, Bash, Bun workspaces, Docker Compose v2, PostgreSQL/TimescaleDB, Biome, Markdown docs.
File Structure (planned changes)
Create
scripts/setup.sh- single owner for worktree bootstrap orchestration and operator messaging.
Modify
Makefile- exposesetupas a root command and delegate toscripts/setup.sh.README.md- makemake setupthe primary onboarding path and document the manual fallback.
Validate / reference while implementing
docker-compose.infra.yml- source of truth for service names, container names, and health checks.package.json- rootmigrateandseed:demoscripts thatscripts/setup.shshould call.backend/.env.examplefrontend/.env.examplebackend/src/database/migrations.ts- confirms the backend readsDATABASE_*env vars used by local setup.backend/src/database/seeds/run-seed.tsbackend/src/database/seeds/demo-data.seed.ts- confirms the demo seed createsorganizations, makingSELECT COUNT(*) FROM organizationsthe correct empty-database sentinel.
Task 1: Add the root setup command and first-pass bootstrap script
Files:
Create:
scripts/setup.shModify:
MakefileTest: command-level verification with
make setupStep 1: Confirm the root command does not exist yet
Run: make setup
Expected: FAIL with make: *** No rule to make target 'setup'. Stop.
- Step 2: Add
setupto the rootMakefileand keep it thin
.PHONY: setup lint build build-containers check release dev dev\:infra dev\:stop test backend\:test frontend\:test
.ONESHELL:
setup:
bash ./scripts/setup.sh
lint:
bun run backend:lint
bun run frontend:lint
build:
bun run backend:build
bun run frontend:build
build-containers:
bash ./build.sh
check: lint build
bun run check
release: check
bash ./release.sh
dev\:infra:
docker compose -f docker-compose.infra.yml up -d
dev\:stop:
docker compose -f docker-compose.infra.yml down
dev: dev\:infra
bun run backend:start:dev & BE_PID=$$!; \
bun run frontend:start:dev & FE_PID=$$!; \
trap "kill $$BE_PID $$FE_PID 2>/dev/null" INT TERM EXIT; \
wait
backend\:test:
cd backend && if [ -n "$(FILE)" ]; then npx jest "$(FILE)"; else bun run test; fi
frontend\:test:
cd frontend && if [ -n "$(FILE)" ]; then bun run test -- "$(FILE)"; else bun run test; fi
test:
$(MAKE) backend:test
$(MAKE) frontend:test
- Step 3: Create the first-pass
scripts/setup.shwith Docker checks, env bootstrap, infra readiness, migrations, and unconditional seed
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
COMPOSE_FILE="docker-compose.infra.yml"
POSTGRES_CONTAINER="hmpiot_postgres_dev"
REDIS_CONTAINER="hmpiot_redis_dev"
EMQX_CONTAINER="hmpiot_emqx_dev"
MINIO_CONTAINER="hmpiot_minio_dev"
WAIT_TIMEOUT_SECONDS=90
POLL_INTERVAL_SECONDS=3
BACKEND_ENV_STATUS="preserved"
FRONTEND_ENV_STATUS="preserved"
MIGRATION_STATUS="not-run"
SEED_STATUS="not-run"
log_stage() {
printf '\n==> %s\n' "$1"
}
log_info() {
printf ' %s\n' "$1"
}
fail() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
check_docker_cli() {
log_stage "Checking Docker CLI"
command -v docker >/dev/null 2>&1 || fail "Docker CLI not found. Install Docker before running make setup."
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is not available from the docker CLI."
log_info "Docker CLI is available."
}
check_docker_daemon() {
log_stage "Checking Docker daemon"
docker info >/dev/null 2>&1 || fail "Docker daemon is not available. Start Docker and rerun make setup."
log_info "Docker daemon is available."
}
install_dependencies() {
log_stage "Installing workspace dependencies"
bun install
log_info "Workspace dependencies installed."
}
ensure_env_files() {
log_stage "Ensuring local environment files"
if [ -f "backend/.env" ]; then
BACKEND_ENV_STATUS="preserved"
log_info "Preserved existing backend/.env"
else
cp "backend/.env.example" "backend/.env"
BACKEND_ENV_STATUS="created"
log_info "Created backend/.env from backend/.env.example"
fi
if [ -f "frontend/.env" ]; then
FRONTEND_ENV_STATUS="preserved"
log_info "Preserved existing frontend/.env"
else
cp "frontend/.env.example" "frontend/.env"
FRONTEND_ENV_STATUS="created"
log_info "Created frontend/.env from frontend/.env.example"
fi
}
start_infra() {
log_stage "Starting infrastructure"
docker compose -f "$COMPOSE_FILE" up -d
log_info "Infrastructure start command completed."
}
container_health() {
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$1" 2>/dev/null || true
}
wait_for_infra() {
log_stage "Waiting for PostgreSQL readiness"
local elapsed=0
while [ "$elapsed" -lt "$WAIT_TIMEOUT_SECONDS" ]; do
local postgres_status
postgres_status="$(container_health "$POSTGRES_CONTAINER")"
if [ "$postgres_status" = "healthy" ]; then
log_info "PostgreSQL is healthy."
return 0
fi
log_info "postgres status: ${postgres_status:-starting} (${elapsed}s/${WAIT_TIMEOUT_SECONDS}s)"
sleep "$POLL_INTERVAL_SECONDS"
elapsed=$((elapsed + POLL_INTERVAL_SECONDS))
done
fail "PostgreSQL did not become healthy within ${WAIT_TIMEOUT_SECONDS}s. Inspect with: docker compose -f ${COMPOSE_FILE} ps"
}
run_migrations() {
log_stage "Running backend migrations"
bun run migrate
MIGRATION_STATUS="applied"
log_info "Backend migrations applied."
}
seed_demo_data() {
log_stage "Seeding demo data"
bun run seed:demo
SEED_STATUS="seeded"
log_info "Demo seed completed."
}
print_success_message() {
printf '\nSetup complete.\n'
printf 'backend env: %s\n' "$BACKEND_ENV_STATUS"
printf 'frontend env: %s\n' "$FRONTEND_ENV_STATUS"
printf 'postgres: %s\n' "$(container_health "$POSTGRES_CONTAINER")"
printf 'redis: %s\n' "$(container_health "$REDIS_CONTAINER")"
printf 'emqx: %s\n' "$(container_health "$EMQX_CONTAINER")"
printf 'minio: %s\n' "$(container_health "$MINIO_CONTAINER")"
printf 'migrations: %s\n' "$MIGRATION_STATUS"
printf 'demo data: %s\n' "$SEED_STATUS"
printf 'next: make dev\n'
}
main() {
check_docker_cli
check_docker_daemon
install_dependencies
ensure_env_files
start_infra
wait_for_infra
run_migrations
seed_demo_data
print_success_message
}
main "$@"
- Step 4: Run the new command and verify the first-pass bootstrap succeeds
Run: make setup
Expected: PASS with stage output for Docker checks, bun install, env-file creation or preservation, docker compose -f docker-compose.infra.yml up -d, PostgreSQL readiness, bun run migrate, bun run seed:demo, and a final summary ending with next: make dev.
- Step 5: Commit
git add Makefile scripts/setup.sh
git commit -m "feat: add make setup bootstrap command"
Task 2: Make demo seeding conditional on an empty local database
Files:
Modify:
scripts/setup.shTest: command-level verification with
make setupplus a direct PostgreSQL sentinel queryStep 1: Prove the first-pass script violates the shared-database contract
Run: docker compose -f docker-compose.infra.yml exec -T postgres psql -U hmpiot -d hmpiot -tAc "SELECT COUNT(*) FROM organizations;"
Expected: PASS with a non-zero count such as 1.
Run: make setup
Expected: PASS, but contract is still wrong because the script runs the Seeding demo data stage again instead of skipping when organizations already has rows.
- Step 2: Replace unconditional seed logic with an
organizationssentinel check
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
COMPOSE_FILE="docker-compose.infra.yml"
POSTGRES_CONTAINER="hmpiot_postgres_dev"
REDIS_CONTAINER="hmpiot_redis_dev"
EMQX_CONTAINER="hmpiot_emqx_dev"
MINIO_CONTAINER="hmpiot_minio_dev"
POSTGRES_USER="hmpiot"
POSTGRES_DB="hmpiot"
WAIT_TIMEOUT_SECONDS=90
POLL_INTERVAL_SECONDS=3
BACKEND_ENV_STATUS="preserved"
FRONTEND_ENV_STATUS="preserved"
MIGRATION_STATUS="not-run"
SEED_STATUS="not-run"
log_stage() {
printf '\n==> %s\n' "$1"
}
log_info() {
printf ' %s\n' "$1"
}
fail() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
check_docker_cli() {
log_stage "Checking Docker CLI"
command -v docker >/dev/null 2>&1 || fail "Docker CLI not found. Install Docker before running make setup."
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is not available from the docker CLI."
log_info "Docker CLI is available."
}
check_docker_daemon() {
log_stage "Checking Docker daemon"
docker info >/dev/null 2>&1 || fail "Docker daemon is not available. Start Docker and rerun make setup."
log_info "Docker daemon is available."
}
install_dependencies() {
log_stage "Installing workspace dependencies"
bun install
log_info "Workspace dependencies installed."
}
ensure_env_files() {
log_stage "Ensuring local environment files"
if [ -f "backend/.env" ]; then
BACKEND_ENV_STATUS="preserved"
log_info "Preserved existing backend/.env"
else
cp "backend/.env.example" "backend/.env"
BACKEND_ENV_STATUS="created"
log_info "Created backend/.env from backend/.env.example"
fi
if [ -f "frontend/.env" ]; then
FRONTEND_ENV_STATUS="preserved"
log_info "Preserved existing frontend/.env"
else
cp "frontend/.env.example" "frontend/.env"
FRONTEND_ENV_STATUS="created"
log_info "Created frontend/.env from frontend/.env.example"
fi
}
start_infra() {
log_stage "Starting infrastructure"
docker compose -f "$COMPOSE_FILE" up -d
log_info "Infrastructure start command completed."
}
container_health() {
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$1" 2>/dev/null || true
}
wait_for_infra() {
log_stage "Waiting for PostgreSQL readiness"
local elapsed=0
while [ "$elapsed" -lt "$WAIT_TIMEOUT_SECONDS" ]; do
local postgres_status
postgres_status="$(container_health "$POSTGRES_CONTAINER")"
if [ "$postgres_status" = "healthy" ]; then
log_info "PostgreSQL is healthy."
return 0
fi
log_info "postgres status: ${postgres_status:-starting} (${elapsed}s/${WAIT_TIMEOUT_SECONDS}s)"
sleep "$POLL_INTERVAL_SECONDS"
elapsed=$((elapsed + POLL_INTERVAL_SECONDS))
done
fail "PostgreSQL did not become healthy within ${WAIT_TIMEOUT_SECONDS}s. Inspect with: docker compose -f ${COMPOSE_FILE} ps"
}
run_migrations() {
log_stage "Running backend migrations"
bun run migrate
MIGRATION_STATUS="applied"
log_info "Backend migrations applied."
}
query_postgres_scalar() {
local sql="$1"
docker compose -f "$COMPOSE_FILE" exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "$sql"
}
get_organization_count() {
local count
count="$(query_postgres_scalar 'SELECT COUNT(*) FROM organizations;' | tr -d '[:space:]')" || fail "Unable to query organizations count from PostgreSQL."
case "$count" in
''|*[!0-9]*)
fail "Unexpected organizations count returned from PostgreSQL: ${count:-<empty>}"
;;
esac
printf '%s' "$count"
}
seed_demo_data_if_database_empty() {
log_stage "Checking whether demo seed should run"
local organization_count
organization_count="$(get_organization_count)"
if [ "$organization_count" = "0" ]; then
SEED_STATUS="seeded"
log_info "organizations table is empty; running demo seed."
bun run seed:demo
log_info "Demo seed completed."
return 0
fi
SEED_STATUS="skipped"
log_info "Detected existing organizations (${organization_count}); skipping demo seed."
}
print_success_message() {
printf '\nSetup complete.\n'
printf 'backend env: %s\n' "$BACKEND_ENV_STATUS"
printf 'frontend env: %s\n' "$FRONTEND_ENV_STATUS"
printf 'postgres: %s\n' "$(container_health "$POSTGRES_CONTAINER")"
printf 'redis: %s\n' "$(container_health "$REDIS_CONTAINER")"
printf 'emqx: %s\n' "$(container_health "$EMQX_CONTAINER")"
printf 'minio: %s\n' "$(container_health "$MINIO_CONTAINER")"
printf 'migrations: %s\n' "$MIGRATION_STATUS"
printf 'demo data: %s\n' "$SEED_STATUS"
printf 'next: make dev\n'
}
main() {
check_docker_cli
check_docker_daemon
install_dependencies
ensure_env_files
start_infra
wait_for_infra
run_migrations
seed_demo_data_if_database_empty
print_success_message
}
main "$@"
- Step 3: Re-run setup and verify the seed is skipped when the database is already initialized
Run: make setup
Expected: PASS. The output should include Checking whether demo seed should run, then Detected existing organizations (1); skipping demo seed. and the final summary should include demo data: skipped.
- Step 4: Verify the sentinel query still reports the existing initialized database
Run: docker compose -f docker-compose.infra.yml exec -T postgres psql -U hmpiot -d hmpiot -tAc "SELECT COUNT(*) FROM organizations;"
Expected: PASS with the same non-zero organization count as before, confirming setup skipped seeding rather than resetting data.
- Step 5: Commit
git add scripts/setup.sh
git commit -m "fix: skip demo seed when local database is initialized"
Task 3: Update onboarding docs and run repo verification
Files:
Modify:
README.mdTest: README discoverability checks plus repo-level verification
Step 1: Confirm the current README still documents the old manual onboarding flow
Run: rg -n "make setup|bun install|cp backend/.env.example|bun run migrate" README.md
Expected: PASS with matches for bun install, env-file copy, and bun run migrate, but no match for make setup in the primary onboarding steps.
- Step 2: Rewrite the getting-started section so
make setupis the primary worktree bootstrap path
# HMP IoT Platform
Multi-tenant aquaculture (fish/shrimp farming) monitoring and management system for the Vietnamese market. Integrates IoT devices with business operations to track water quality, growth data, and manage farming cycles.
**Monorepo:** Bun workspaces | **Backend:** NestJS + PostgreSQL + Redis + MQTT | **Frontend:** React 19 + Vite + Ant Design
## Getting Started
### Bootstrap a New Worktree
```bash
make setup
make dev
```
`make setup` installs workspace dependencies, creates missing local env files from tracked examples, starts the local Docker infra stack, waits for PostgreSQL to become healthy, runs backend migrations, and seeds demo data only when the local database has no organizations yet.
If you rerun `make setup` from another worktree that points at the same local PostgreSQL instance, the command preserves existing env files and skips demo seeding once the shared database has already been initialized.
### Manual Fallback
```bash
bun install
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
docker compose -f docker-compose.infra.yml up -d
bun run migrate
bun run seed:demo
make dev
```
### Access
| Service | URL |
|---------|-----|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:3000/api/v1 |
| Swagger Docs | http://localhost:3000/api |
| EMQX Dashboard | http://localhost:18083 (admin/public) |
| MinIO Console | http://localhost:9001 (minioadmin/minioadmin) |
| Role | Email | Password |
|------|-------|----------|
| Super Admin | superadmin@hmpiot.com | Demo@123456 |
| Admin | admin@bentre-shrimp.vn | Demo@123456 |
| Owner | owner1@bentre-shrimp.vn | Demo@123456 |
### Useful Commands
```bash
docker compose -f docker-compose.infra.yml down # Stop infrastructure
docker compose -f docker-compose.infra.yml logs -f # View logs
```
---
## Make Commands
| Command | Description |
|---------|-------------|
| `make setup` | Install workspace dependencies, create missing local env files, start Docker infra, wait for PostgreSQL, run migrations, and seed demo data only when the local database has no organizations yet. |
| `make dev` | Start infra in Docker (detached) + run backend and frontend natively with `bun`. Ctrl+C stops the app processes. |
| `make dev:infra` | Start only the infrastructure containers (postgres, redis, emqx, minio) in detached mode. |
| `make dev:stop` | Stop and remove the infrastructure containers. |
| `make test` | Run backend and frontend test suites. |
| `make backend:test` | Run backend unit tests (`bun test`). |
| `make frontend:test` | Run frontend tests (`bun test`). |
| `make lint` | Run Biome lint check on backend and frontend. |
| `make build` | Build backend and frontend for production. |
| `make build-containers` | Build Docker images via `build.sh`. |
| `make check` | Run lint, build, and root Biome checks. |
| `make release` | Run full check then deploy via `release.sh`. |
- Step 3: Verify the command is now discoverable in both docs and the command surface
Run: rg -n "make setup|setup:" README.md Makefile
Expected: PASS with hits in README.md and the root Makefile.
- Step 4: Run repo-level verification required by this repo
Run: bun run check
Expected: PASS.
- Step 5: Commit
git add README.md
git commit -m "docs: document make setup onboarding flow"