diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2d9c3c1b..aa4e2346 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -74,7 +74,22 @@ "Bash(npx playwright test:*)", "Bash(sort:*)", "Bash(npx playwright install:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(grep -r '\"\"clean\"\"' --include=\"package.json\" .)", + "Bash(grep -r '\"\"clean\"\"' --include=\"package.json\" . --exclude-dir=node_modules)", + "Bash(find /c/Users/User/Documents/Projects/nimble/apps/portal/app /c/Users/User/Documents/Projects/nimble/apps/portal/lib /c/Users/User/Documents/Projects/nimble/apps/portal/components -type f \\\\\\(-name *.ts -o -name *.tsx \\\\\\))", + "Bash(git:*)", + "Bash(echo \"EXIT:$?\")", + "Bash(docker compose:*)", + "Bash(bash -n /c/Users/User/Documents/Projects/nimble/.devcontainer/setup.sh)", + "Bash(bash -n /c/Users/User/Documents/Projects/nimble/apps/sidekick-api/scripts/start-local-db.sh)", + "Bash(DEVCONTAINER=true bash /c/Users/User/Documents/Projects/nimble/apps/sidekick-api/scripts/start-local-db.sh)", + "Bash(echo \"Exit code: $?\")", + "Bash(docker build:*)", + "Bash(docker rmi:*)", + "Bash(wsl --list --verbose)", + "Bash(wsl:*)", + "Bash(docker ps:*)" ], "deny": [], "ask": [], diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d0fedbd4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +out +dist +.turbo +.git +.claude +.env +.env.* +!.env.example +test-results +playwright-report diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..025b86ff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..82a9d3b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +# Enforce LF line endings for all text files +* text=auto eol=lf + +# Ensure shell scripts always use LF (critical for bash execution) +*.sh text eol=lf +.husky/* text eol=lf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.webp binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 1231f19a..80593d33 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -2,6 +2,93 @@ This guide will help you set up and run the Sidekick monorepo locally. +There are two ways to set up your development environment: + +- **[Native Setup](#prerequisites)** (macOS/Linux) — Install Node.js, Docker, and dependencies directly on your machine +- **[Docker Setup](#docker-development-setup)** (Windows / Cross-platform) — Run everything inside Docker containers with a single command + +--- + +## Docker Development Setup + +Use this setup if you're on **Windows** or prefer a containerized environment. All you need is [Docker Desktop](https://www.docker.com/products/docker-desktop/) — no Node.js, PostgreSQL, or other dependencies required on your machine. + +### Prerequisites + +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running + +### Quick Start + +```bash +# Clone the repository +git clone +cd nimble + +# Start the development environment (first run takes a few minutes) +npm run docker:up + +# Start the web app +npm run docker:dev:sidekick +``` + +Open http://localhost:3000 in your browser. + +The first `docker:up` automatically handles npm install, shared package builds, database creation, and Prisma migrations. Subsequent starts reuse cached dependencies and are much faster. + +### Available Docker Commands + +| Command | Description | +|---------|-------------| +| `npm run docker:up` | Start containers (runs setup on first boot) | +| `npm run docker:down` | Stop containers | +| `npm run docker:shell` | Open a bash shell inside the container | +| `npm run docker:dev` | Start all apps | +| `npm run docker:dev:sidekick` | Start the web app only (port 3000) | +| `npm run docker:dev:api` | Start the API server only (port 3001) | +| `npm run docker:test` | Run unit tests | +| `npm run docker:lint` | Run ESLint | +| `npm run docker:typecheck` | Run TypeScript type checking | + +For any command not listed above, use the shell: + +```bash +npm run docker:shell +# Then run any command inside the container, e.g.: +npm run dev:vault +npx turbo run test:e2e --filter=@nimble/sidekick +``` + +### IDE Support (TypeScript Intellisense) + +The Docker setup uses a separate `node_modules` volume inside the container, so your host machine won't have `node_modules` by default. To get TypeScript autocomplete and go-to-definition working in your editor, run `npm install` locally as well: + +```bash +npm install +``` + +This gives your editor access to type definitions. The container and host `node_modules` are independent and don't interfere with each other. + +### Resetting the Environment + +```bash +# Stop containers and remove all data (node_modules, database) +docker compose down -v + +# Start fresh +npm run docker:up +``` + +### How It Works + +- **`docker-compose.yml`** defines two services: a Node.js 20 app container and a PostgreSQL 15 database +- **`docker/entrypoint.sh`** runs automatically on first start: installs dependencies, builds shared packages, copies `.env.example` files, and runs database migrations +- Source code is bind-mounted from your host, so edits are reflected immediately +- The `DEVCONTAINER=true` environment variable tells the existing `start-local-db.sh` script to skip its Docker logic (the database is already provided by Docker Compose) + +--- + +## Native Setup (macOS/Linux) + ## Prerequisites Before you begin, ensure you have the following installed on your system: diff --git a/apps/sidekick-api/scripts/start-local-db.sh b/apps/sidekick-api/scripts/start-local-db.sh index 7fc15f42..eac71c45 100755 --- a/apps/sidekick-api/scripts/start-local-db.sh +++ b/apps/sidekick-api/scripts/start-local-db.sh @@ -9,6 +9,12 @@ if [ "$NODE_ENV" = "production" ] || [ "$CI" = "true" ]; then exit 0 fi +# In a dev container, PostgreSQL is provided by Docker Compose +if [ "$DEVCONTAINER" = "true" ]; then + echo "✅ Dev container detected — PostgreSQL is managed by Docker Compose" + exit 0 +fi + # Check if Docker is running if ! docker info > /dev/null 2>&1; then echo "Docker is not running. Please start Docker Desktop." diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b73c1720 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + app: + build: + context: . + dockerfile: docker/Dockerfile + volumes: + - .:/workspace:cached + - node_modules:/workspace/node_modules + entrypoint: ["/workspace/docker/entrypoint.sh"] + command: ["sleep", "infinity"] + depends_on: + db: + condition: service_healthy + environment: + DEVCONTAINER: "true" + DATABASE_URL: "postgresql://postgres:nimblelocal123@db:5432/nimbledb?schema=public" + SESSION_SECRET: "dev-session-secret-not-for-production-1234567890" + ports: + - "3000:3000" + - "3001:3001" + - "3002:3002" + - "4000:4000" + - "4321:4321" + + db: + image: postgres:15-alpine + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: nimblelocal123 + POSTGRES_DB: nimbledb + volumes: + - postgres-data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + node_modules: + postgres-data: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..3a82cb78 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20-bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..9075213e --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +echo "=== Nimble Dev Environment Setup ===" + +# Fix execute bits (Windows checkout strips them) +find /workspace -name "*.sh" -not -path "*/node_modules/*" -exec chmod +x {} \; +chmod +x /workspace/.husky/pre-commit /workspace/.husky/pre-push 2>/dev/null || true + +# Copy .env.example files if .env doesn't exist +copy_env() { + local src="$1" dest="$2" + if [ ! -f "$dest" ]; then + echo " Creating $dest from example..." + cp "$src" "$dest" + fi +} + +copy_env apps/sidekick-api/.env.example apps/sidekick-api/.env +copy_env apps/sidekick/.env.example apps/sidekick/.env.local +copy_env apps/portal/.env.example apps/portal/.env.local +copy_env apps/vault/.env.example apps/vault/.env +copy_env apps/discord/.env.example apps/discord/.env + +# Install dependencies +echo "Installing npm dependencies..." +npm install + +# Wait for database to be ready +echo "Waiting for database..." +for i in $(seq 1 30); do + if node -e " + const net = require('net'); + const s = net.createConnection({host:'db',port:5432}); + s.on('connect',()=>{s.end();process.exit(0)}); + s.on('error',()=>process.exit(1)); + " 2>/dev/null; then + echo " Database is ready" + break + fi + if [ "$i" -eq 30 ]; then + echo " ERROR: Database not reachable after 30s" + exit 1 + fi + sleep 1 +done + +# Build all shared packages so apps can import them +echo "Building shared packages..." +npx turbo build --filter='./packages/*' + +# Generate Prisma client and run migrations +echo "Setting up database..." +cd apps/sidekick-api +npx prisma generate +npx prisma migrate deploy +cd /workspace + +echo "" +echo "=== Setup complete! ===" +echo "" + +exec "$@" diff --git a/package-lock.json b/package-lock.json index 7c9d5e40..8be7c1ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1847,6 +1847,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -1864,6 +1865,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1881,6 +1883,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1898,6 +1901,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -1915,6 +1919,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1932,6 +1937,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -1949,6 +1955,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1966,6 +1973,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -1983,6 +1991,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2000,6 +2009,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2017,6 +2027,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2034,6 +2045,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2051,6 +2063,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2068,6 +2081,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2085,6 +2099,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2102,6 +2117,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2119,6 +2135,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -2136,6 +2153,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2153,6 +2171,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2170,6 +2189,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2187,6 +2207,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -2204,6 +2225,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -2221,6 +2243,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -2238,6 +2261,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2255,6 +2279,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2272,6 +2297,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -7789,9 +7815,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7806,9 +7829,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7823,9 +7843,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7840,9 +7857,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7857,9 +7871,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7874,9 +7885,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7891,9 +7899,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7908,9 +7913,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7925,9 +7927,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7942,9 +7941,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7959,9 +7955,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7976,9 +7969,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7993,9 +7983,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8349,9 +8336,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8368,9 +8352,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8387,9 +8368,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8406,9 +8384,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -15350,9 +15325,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -15373,9 +15345,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -15396,9 +15365,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -15419,9 +15385,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -20906,6 +20869,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/package.json b/package.json index 80ef37b8..e4eb853b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,16 @@ "test": "turbo test", "test:watch": "turbo test:watch", "test:ui": "turbo test:ui", - "prepare": "npx husky || true" + "prepare": "npx husky || true", + "docker:up": "docker compose up -d", + "docker:down": "docker compose down", + "docker:shell": "docker compose exec app bash", + "docker:dev": "docker compose exec app npm run dev", + "docker:dev:sidekick": "docker compose exec app npm run dev:sidekick", + "docker:dev:api": "docker compose exec app npm run dev:api", + "docker:test": "docker compose exec app npm run test", + "docker:lint": "docker compose exec app npm run lint", + "docker:typecheck": "docker compose exec app npm run typecheck" }, "devDependencies": { "husky": "^9.1.7",