From 69eb70c62ae63566cceb65025196e2f917929837 Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Mon, 22 Jun 2026 16:32:36 +0200 Subject: [PATCH 1/5] chore: add Makefile for one-command dev setup Wrap the existing npm scripts and scripts/db-init.sh behind make targets so a fresh clone comes up with 'make setup' (install, generate .env.local secrets, start DB, migrate, seed). 'make check' mirrors the CI gates. --- Makefile | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2cf728e --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +# DataShield developer tasks. Thin wrapper over the npm scripts and +# scripts/db-init.sh so a fresh clone can be brought up with a single command. +# Requires Node 22 and (for the local database) Docker. + +.DEFAULT_GOAL := help +SHELL := /bin/sh + +.PHONY: help setup install env db-up db-down db-init migrate seed seed-dev \ + dev build lint lint-fix test check clean + +help: ## List available targets + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | sort \ + | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' + +setup: install env db-init ## One-shot: install deps, create .env.local, start DB, migrate, seed + @echo "Setup complete. Start the app with 'make dev'." + +install: ## Install dependencies (also runs prisma generate) + npm install + +env: ## Create .env.local from .env.example with generated secrets (no-op if it exists) + @if [ -f .env.local ]; then \ + echo ".env.local already exists, leaving it untouched."; \ + else \ + cp .env.example .env.local; \ + auth=$$(openssl rand -base64 32); \ + enc=$$(openssl rand -base64 32); \ + sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$$auth|" .env.local; \ + sed -i "s|^DIRECTORY_ENCRYPTION_KEY=.*|DIRECTORY_ENCRYPTION_KEY=$$enc|" .env.local; \ + echo ".env.local created with generated AUTH_SECRET and DIRECTORY_ENCRYPTION_KEY."; \ + fi + +db-up: ## Start the local PostgreSQL container + npm run db:up + +db-down: ## Stop the local database container + npm run db:down + +db-init: ## Start DB, apply migrations, seed demo data (Docker required) + npm run db:init + +migrate: ## Apply pending Prisma migrations + npm run db:migrate + +seed: ## Seed the base admin account + npm run seed + +seed-dev: ## Seed demo data for development + npm run seed:dev + +dev: ## Run the dev server + npm run dev + +build: ## Production build + npm run build + +lint: ## Lint + npm run lint + +lint-fix: ## Lint and auto-fix + npm run lint:fix + +test: ## Run the test suite + npm test + +check: ## Run the same gates CI enforces (lint, types, schema, build) + npm run lint -- --max-warnings 0 + npx tsc --noEmit + npx prisma validate + npm run build + +clean: ## Stop the DB and remove node_modules and the Next.js build cache + npm run db:down + rm -rf node_modules .next From d406e1f43a002bf05100c97cf868bd40e589348f Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Mon, 22 Jun 2026 16:35:41 +0200 Subject: [PATCH 2/5] chore: rename dev target to run, add doctor diagnostics 'make run' replaces 'make dev'. 'make doctor' reports node, .env.local, Docker daemon, the db container health, prisma client, and live migration status to troubleshoot setup issues. --- Makefile | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2cf728e..176ec76 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ SHELL := /bin/sh .PHONY: help setup install env db-up db-down db-init migrate seed seed-dev \ - dev build lint lint-fix test check clean + run build lint lint-fix test check doctor clean help: ## List available targets @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ @@ -14,7 +14,7 @@ help: ## List available targets | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' setup: install env db-init ## One-shot: install deps, create .env.local, start DB, migrate, seed - @echo "Setup complete. Start the app with 'make dev'." + @echo "Setup complete. Start the app with 'make run'." install: ## Install dependencies (also runs prisma generate) npm install @@ -49,7 +49,7 @@ seed: ## Seed the base admin account seed-dev: ## Seed demo data for development npm run seed:dev -dev: ## Run the dev server +run: ## Run the dev server npm run dev build: ## Production build @@ -70,6 +70,23 @@ check: ## Run the same gates CI enforces (lint, types, schema, build) npx prisma validate npm run build +doctor: ## Diagnose Docker, database, Prisma, and env for troubleshooting + @echo "DataShield doctor" + @echo "-----------------" + @printf "node: "; node -v 2>/dev/null || echo "MISSING (need Node 22)" + @printf ".env.local: "; if [ -f .env.local ]; then echo "present"; else echo "MISSING (run 'make env')"; fi + @if [ -f .env.local ]; then \ + grep -q '^AUTH_SECRET=change-me' .env.local && echo " WARN: AUTH_SECRET is still the placeholder"; \ + grep -q '^DIRECTORY_ENCRYPTION_KEY=change-me' .env.local && echo " WARN: DIRECTORY_ENCRYPTION_KEY is still the placeholder (run 'make env' on a fresh file)"; \ + true; \ + fi + @printf "docker: "; if command -v docker >/dev/null 2>&1; then docker --version; else echo "MISSING (install Docker)"; fi + @printf "docker daemon: "; if docker info >/dev/null 2>&1; then echo "running"; else echo "NOT running (start Docker Desktop / enable WSL integration)"; fi + @printf "db container: "; status=$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null); if [ -n "$$status" ]; then echo "$$status"; else echo "not found (run 'make db-up')"; fi + @printf "prisma client: "; if [ -d node_modules/.prisma/client ]; then echo "generated"; else echo "MISSING (run 'make install' or 'npx prisma generate')"; fi + @echo "migrations (live DB check):" + @npx prisma migrate status 2>&1 | sed 's/^/ /' || true + clean: ## Stop the DB and remove node_modules and the Next.js build cache npm run db:down rm -rf node_modules .next From 5124706429ed8ab737034e82f6324d8c22a8cc61 Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Mon, 22 Jun 2026 16:39:24 +0200 Subject: [PATCH 3/5] chore: make doctor a full setup diagnosis Check toolchain (node/npm/openssl), every .env.local var (presence, placeholders, key length), Docker (binary/compose/daemon/db container), database TCP reachability, and Prisma (client, schema validity, migration status). Print an OK/WARN/FAIL summary and exit non-zero on any failure. --- Makefile | 85 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 176ec76..388ebad 100644 --- a/Makefile +++ b/Makefile @@ -70,22 +70,75 @@ check: ## Run the same gates CI enforces (lint, types, schema, build) npx prisma validate npm run build -doctor: ## Diagnose Docker, database, Prisma, and env for troubleshooting - @echo "DataShield doctor" - @echo "-----------------" - @printf "node: "; node -v 2>/dev/null || echo "MISSING (need Node 22)" - @printf ".env.local: "; if [ -f .env.local ]; then echo "present"; else echo "MISSING (run 'make env')"; fi - @if [ -f .env.local ]; then \ - grep -q '^AUTH_SECRET=change-me' .env.local && echo " WARN: AUTH_SECRET is still the placeholder"; \ - grep -q '^DIRECTORY_ENCRYPTION_KEY=change-me' .env.local && echo " WARN: DIRECTORY_ENCRYPTION_KEY is still the placeholder (run 'make env' on a fresh file)"; \ - true; \ - fi - @printf "docker: "; if command -v docker >/dev/null 2>&1; then docker --version; else echo "MISSING (install Docker)"; fi - @printf "docker daemon: "; if docker info >/dev/null 2>&1; then echo "running"; else echo "NOT running (start Docker Desktop / enable WSL integration)"; fi - @printf "db container: "; status=$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null); if [ -n "$$status" ]; then echo "$$status"; else echo "not found (run 'make db-up')"; fi - @printf "prisma client: "; if [ -d node_modules/.prisma/client ]; then echo "generated"; else echo "MISSING (run 'make install' or 'npx prisma generate')"; fi - @echo "migrations (live DB check):" - @npx prisma migrate status 2>&1 | sed 's/^/ /' || true +doctor: ## Full setup diagnosis: toolchain, env, Docker, database, Prisma + @ok=0; warn=0; bad=0; \ + pass() { echo " [OK] $$1"; ok=$$((ok+1)); }; \ + wrn() { echo " [WARN] $$1"; warn=$$((warn+1)); }; \ + err() { echo " [FAIL] $$1"; bad=$$((bad+1)); }; \ + getv() { grep -E "^$$1=" .env.local 2>/dev/null | head -1 | cut -d= -f2-; }; \ + echo "DataShield doctor"; \ + echo "================="; \ + echo "Toolchain:"; \ + nv=$$(node -v 2>/dev/null); \ + if [ -z "$$nv" ]; then err "node: not found (need Node 22)"; \ + elif [ "$${nv#v22.}" != "$$nv" ]; then pass "node $$nv"; \ + else wrn "node $$nv (project targets Node 22)"; fi; \ + if command -v npm >/dev/null 2>&1; then pass "npm $$(npm -v)"; else err "npm: not found"; fi; \ + if command -v openssl >/dev/null 2>&1; then pass "openssl present (used by 'make env')"; else wrn "openssl: not found ('make env' cannot generate secrets)"; fi; \ + echo "Environment (.env.local):"; \ + if [ ! -f .env.local ]; then err ".env.local missing (run 'make env')"; \ + else \ + pass ".env.local present"; \ + [ -n "$$(getv DATABASE_URL)" ] && pass "DATABASE_URL set" || err "DATABASE_URL empty"; \ + as=$$(getv AUTH_SECRET); \ + if [ -z "$$as" ]; then err "AUTH_SECRET empty"; \ + elif [ "$${as#change-me}" != "$$as" ]; then err "AUTH_SECRET still the placeholder"; \ + else pass "AUTH_SECRET set"; fi; \ + ek=$$(getv DIRECTORY_ENCRYPTION_KEY); ekl=$$(printf %s "$$ek" | wc -c | tr -d ' '); \ + if [ -z "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY empty (app refuses directory configs)"; \ + elif [ "$${ek#change-me}" != "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY still the placeholder"; \ + elif [ "$$ekl" -lt 32 ]; then err "DIRECTORY_ENCRYPTION_KEY too short ($$ekl chars, need >= 32)"; \ + else pass "DIRECTORY_ENCRYPTION_KEY set ($$ekl chars)"; fi; \ + [ -n "$$(getv AUTH_URL)" ] && pass "AUTH_URL set" || wrn "AUTH_URL empty (defaults to http://localhost:3000)"; \ + [ -n "$$(getv CRON_SECRET)" ] && pass "CRON_SECRET set" || wrn "CRON_SECRET empty (scheduler endpoint /api/cron returns 503)"; \ + [ -n "$$(getv HIBP_API_KEY)" ] && pass "HIBP_API_KEY set" || wrn "HIBP_API_KEY empty (no breach lookups unless a per-company key is stored)"; \ + rk=$$(getv RESEND_API_KEY); ef=$$(getv EMAIL_FROM); \ + if [ -n "$$rk" ] && [ -n "$$ef" ]; then pass "email configured (RESEND_API_KEY + EMAIL_FROM)"; \ + elif [ -z "$$rk" ] && [ -z "$$ef" ]; then wrn "email disabled (RESEND_API_KEY + EMAIL_FROM unset)"; \ + else wrn "email half-configured: set both RESEND_API_KEY and EMAIL_FROM or neither"; fi; \ + fi; \ + echo "Docker:"; \ + if command -v docker >/dev/null 2>&1; then pass "docker $$(docker --version | awk '{print $$3}' | tr -d ',')"; \ + else err "docker not found (needed for the local database)"; fi; \ + if docker compose version >/dev/null 2>&1; then pass "docker compose available"; else wrn "docker compose not available"; fi; \ + if docker info >/dev/null 2>&1; then pass "docker daemon running"; \ + else err "docker daemon not running (start Docker Desktop / enable WSL integration)"; fi; \ + st=$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null); \ + if [ "$$st" = "healthy" ]; then pass "db container healthy"; \ + elif [ -n "$$st" ]; then wrn "db container status: $$st"; \ + else wrn "db container not found (run 'make db-up')"; fi; \ + echo "Database connectivity:"; \ + du=$$(getv DATABASE_URL); \ + hp=$$(printf %s "$$du" | sed -E 's#^[a-z]+://([^@]*@)?([^/?]+).*#\2#'); \ + host=$${hp%%:*}; port=$${hp##*:}; [ "$$port" = "$$host" ] && port=5432; \ + if [ -z "$$host" ]; then wrn "could not parse host from DATABASE_URL"; \ + elif command -v nc >/dev/null 2>&1; then \ + if nc -z -w 2 "$$host" "$$port" >/dev/null 2>&1; then pass "TCP $$host:$$port reachable"; \ + else err "TCP $$host:$$port not reachable (is the DB up?)"; fi; \ + else wrn "nc not installed, skipping TCP probe ($$host:$$port)"; fi; \ + echo "Prisma:"; \ + if [ -d node_modules ]; then pass "node_modules installed"; else err "node_modules missing (run 'make install')"; fi; \ + if [ -d node_modules/.prisma/client ]; then pass "prisma client generated"; else err "prisma client missing (run 'npx prisma generate')"; fi; \ + if npx prisma validate >/dev/null 2>&1; then pass "schema valid (prisma validate)"; else err "schema invalid (run 'npx prisma validate' for details)"; fi; \ + ms=$$(npx prisma migrate status 2>&1); \ + if printf %s "$$ms" | grep -q "up to date"; then pass "migrations up to date"; \ + elif printf %s "$$ms" | grep -q "have not yet been applied"; then wrn "pending migrations (run 'make migrate')"; \ + elif printf %s "$$ms" | grep -qE "P1001|reach|unreachable"; then err "database unreachable for migrate status"; \ + else wrn "migrate status inconclusive (run 'make migrate' / 'npx prisma migrate status')"; fi; \ + echo "================="; \ + echo "Summary: $$ok OK, $$warn warning(s), $$bad failure(s)"; \ + if [ "$$bad" -gt 0 ]; then echo "Result: NOT ready, fix the failures above."; else echo "Result: ready."; fi; \ + [ "$$bad" -eq 0 ] clean: ## Stop the DB and remove node_modules and the Next.js build cache npm run db:down From ed4ed0758ee0bb45e57d4dfd7d2eb7b41a1bc194 Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Mon, 22 Jun 2026 16:46:42 +0200 Subject: [PATCH 4/5] feat: make doctor offer to fix detected issues After the diagnosis, prompt to auto-fix (interactive tty only), separating failures from warnings: [e]rrors only, [a]ll, or [n]o. Fixes cover .env.local creation and secret generation, AUTH_URL/CRON_SECRET, npm install, prisma generate, db-up, and migrate deploy. --- Makefile | 59 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 388ebad..f4ba4dd 100644 --- a/Makefile +++ b/Makefile @@ -71,11 +71,13 @@ check: ## Run the same gates CI enforces (lint, types, schema, build) npm run build doctor: ## Full setup diagnosis: toolchain, env, Docker, database, Prisma - @ok=0; warn=0; bad=0; \ + @ok=0; warn=0; bad=0; dkr=0; \ + fx_env=0; fx_auth=0; fx_enc=0; fx_authurl=0; fx_cron=0; fx_install=0; fx_generate=0; fx_dbup=0; fx_migrate=0; \ pass() { echo " [OK] $$1"; ok=$$((ok+1)); }; \ wrn() { echo " [WARN] $$1"; warn=$$((warn+1)); }; \ err() { echo " [FAIL] $$1"; bad=$$((bad+1)); }; \ getv() { grep -E "^$$1=" .env.local 2>/dev/null | head -1 | cut -d= -f2-; }; \ + setval() { if grep -qE "^$$1=" .env.local 2>/dev/null; then sed -i "s|^$$1=.*|$$1=$$2|" .env.local; else printf '%s=%s\n' "$$1" "$$2" >> .env.local; fi; }; \ echo "DataShield doctor"; \ echo "================="; \ echo "Toolchain:"; \ @@ -86,21 +88,21 @@ doctor: ## Full setup diagnosis: toolchain, env, Docker, database, Prisma if command -v npm >/dev/null 2>&1; then pass "npm $$(npm -v)"; else err "npm: not found"; fi; \ if command -v openssl >/dev/null 2>&1; then pass "openssl present (used by 'make env')"; else wrn "openssl: not found ('make env' cannot generate secrets)"; fi; \ echo "Environment (.env.local):"; \ - if [ ! -f .env.local ]; then err ".env.local missing (run 'make env')"; \ + if [ ! -f .env.local ]; then err ".env.local missing (run 'make env')"; fx_env=1; \ else \ pass ".env.local present"; \ [ -n "$$(getv DATABASE_URL)" ] && pass "DATABASE_URL set" || err "DATABASE_URL empty"; \ as=$$(getv AUTH_SECRET); \ - if [ -z "$$as" ]; then err "AUTH_SECRET empty"; \ - elif [ "$${as#change-me}" != "$$as" ]; then err "AUTH_SECRET still the placeholder"; \ + if [ -z "$$as" ]; then err "AUTH_SECRET empty"; fx_auth=1; \ + elif [ "$${as#change-me}" != "$$as" ]; then err "AUTH_SECRET still the placeholder"; fx_auth=1; \ else pass "AUTH_SECRET set"; fi; \ ek=$$(getv DIRECTORY_ENCRYPTION_KEY); ekl=$$(printf %s "$$ek" | wc -c | tr -d ' '); \ - if [ -z "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY empty (app refuses directory configs)"; \ - elif [ "$${ek#change-me}" != "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY still the placeholder"; \ - elif [ "$$ekl" -lt 32 ]; then err "DIRECTORY_ENCRYPTION_KEY too short ($$ekl chars, need >= 32)"; \ + if [ -z "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY empty (app refuses directory configs)"; fx_enc=1; \ + elif [ "$${ek#change-me}" != "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY still the placeholder"; fx_enc=1; \ + elif [ "$$ekl" -lt 32 ]; then err "DIRECTORY_ENCRYPTION_KEY too short ($$ekl chars, need >= 32)"; fx_enc=1; \ else pass "DIRECTORY_ENCRYPTION_KEY set ($$ekl chars)"; fi; \ - [ -n "$$(getv AUTH_URL)" ] && pass "AUTH_URL set" || wrn "AUTH_URL empty (defaults to http://localhost:3000)"; \ - [ -n "$$(getv CRON_SECRET)" ] && pass "CRON_SECRET set" || wrn "CRON_SECRET empty (scheduler endpoint /api/cron returns 503)"; \ + if [ -n "$$(getv AUTH_URL)" ]; then pass "AUTH_URL set"; else wrn "AUTH_URL empty (defaults to http://localhost:3000)"; fx_authurl=1; fi; \ + if [ -n "$$(getv CRON_SECRET)" ]; then pass "CRON_SECRET set"; else wrn "CRON_SECRET empty (scheduler endpoint /api/cron returns 503)"; fx_cron=1; fi; \ [ -n "$$(getv HIBP_API_KEY)" ] && pass "HIBP_API_KEY set" || wrn "HIBP_API_KEY empty (no breach lookups unless a per-company key is stored)"; \ rk=$$(getv RESEND_API_KEY); ef=$$(getv EMAIL_FROM); \ if [ -n "$$rk" ] && [ -n "$$ef" ]; then pass "email configured (RESEND_API_KEY + EMAIL_FROM)"; \ @@ -111,12 +113,12 @@ doctor: ## Full setup diagnosis: toolchain, env, Docker, database, Prisma if command -v docker >/dev/null 2>&1; then pass "docker $$(docker --version | awk '{print $$3}' | tr -d ',')"; \ else err "docker not found (needed for the local database)"; fi; \ if docker compose version >/dev/null 2>&1; then pass "docker compose available"; else wrn "docker compose not available"; fi; \ - if docker info >/dev/null 2>&1; then pass "docker daemon running"; \ + if docker info >/dev/null 2>&1; then pass "docker daemon running"; dkr=1; \ else err "docker daemon not running (start Docker Desktop / enable WSL integration)"; fi; \ st=$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null); \ if [ "$$st" = "healthy" ]; then pass "db container healthy"; \ elif [ -n "$$st" ]; then wrn "db container status: $$st"; \ - else wrn "db container not found (run 'make db-up')"; fi; \ + else wrn "db container not found (run 'make db-up')"; [ "$$dkr" = "1" ] && fx_dbup=1; fi; \ echo "Database connectivity:"; \ du=$$(getv DATABASE_URL); \ hp=$$(printf %s "$$du" | sed -E 's#^[a-z]+://([^@]*@)?([^/?]+).*#\2#'); \ @@ -127,17 +129,44 @@ doctor: ## Full setup diagnosis: toolchain, env, Docker, database, Prisma else err "TCP $$host:$$port not reachable (is the DB up?)"; fi; \ else wrn "nc not installed, skipping TCP probe ($$host:$$port)"; fi; \ echo "Prisma:"; \ - if [ -d node_modules ]; then pass "node_modules installed"; else err "node_modules missing (run 'make install')"; fi; \ - if [ -d node_modules/.prisma/client ]; then pass "prisma client generated"; else err "prisma client missing (run 'npx prisma generate')"; fi; \ + if [ -d node_modules ]; then pass "node_modules installed"; else err "node_modules missing (run 'make install')"; fx_install=1; fi; \ + if [ -d node_modules/.prisma/client ]; then pass "prisma client generated"; else err "prisma client missing (run 'npx prisma generate')"; fx_generate=1; fi; \ if npx prisma validate >/dev/null 2>&1; then pass "schema valid (prisma validate)"; else err "schema invalid (run 'npx prisma validate' for details)"; fi; \ ms=$$(npx prisma migrate status 2>&1); \ if printf %s "$$ms" | grep -q "up to date"; then pass "migrations up to date"; \ - elif printf %s "$$ms" | grep -q "have not yet been applied"; then wrn "pending migrations (run 'make migrate')"; \ + elif printf %s "$$ms" | grep -q "have not yet been applied"; then wrn "pending migrations (run 'make migrate')"; fx_migrate=1; \ elif printf %s "$$ms" | grep -qE "P1001|reach|unreachable"; then err "database unreachable for migrate status"; \ else wrn "migrate status inconclusive (run 'make migrate' / 'npx prisma migrate status')"; fi; \ echo "================="; \ echo "Summary: $$ok OK, $$warn warning(s), $$bad failure(s)"; \ - if [ "$$bad" -gt 0 ]; then echo "Result: NOT ready, fix the failures above."; else echo "Result: ready."; fi; \ + if [ "$$bad" -gt 0 ]; then echo "Result: NOT ready."; else echo "Result: ready."; fi; \ + errfix=$$((fx_env+fx_auth+fx_enc+fx_install+fx_generate)); \ + warnfix=$$((fx_authurl+fx_cron+fx_dbup+fx_migrate)); \ + ans=n; \ + if [ $$((errfix+warnfix)) -gt 0 ] && [ -t 0 ]; then \ + echo ""; \ + echo "Auto-fixable: $$errfix from failures, $$warnfix from warnings."; \ + printf "Fix now? [e]rrors only / [a]ll / [n]o: "; read ans; \ + fi; \ + do_err=0; do_warn=0; \ + case "$$ans" in e|E) do_err=1;; a|A) do_err=1; do_warn=1;; esac; \ + applied=0; \ + if [ "$$do_err" = "1" ]; then \ + if [ "$$fx_env" = "1" ]; then cp .env.example .env.local; setval AUTH_SECRET "$$(openssl rand -base64 32)"; setval DIRECTORY_ENCRYPTION_KEY "$$(openssl rand -base64 32)"; echo "fixed: created .env.local with generated secrets"; applied=1; \ + else \ + if [ "$$fx_auth" = "1" ]; then setval AUTH_SECRET "$$(openssl rand -base64 32)"; echo "fixed: AUTH_SECRET"; applied=1; fi; \ + if [ "$$fx_enc" = "1" ]; then setval DIRECTORY_ENCRYPTION_KEY "$$(openssl rand -base64 32)"; echo "fixed: DIRECTORY_ENCRYPTION_KEY"; applied=1; fi; \ + fi; \ + if [ "$$fx_install" = "1" ]; then npm install; applied=1; fi; \ + if [ "$$fx_generate" = "1" ]; then npx prisma generate; applied=1; fi; \ + fi; \ + if [ "$$do_warn" = "1" ]; then \ + if [ "$$fx_authurl" = "1" ]; then setval AUTH_URL "http://localhost:3000"; echo "fixed: AUTH_URL"; applied=1; fi; \ + if [ "$$fx_cron" = "1" ]; then setval CRON_SECRET "$$(openssl rand -base64 32)"; echo "fixed: CRON_SECRET"; applied=1; fi; \ + if [ "$$fx_dbup" = "1" ]; then npm run db:up; echo "waiting for db to become healthy"; i=0; while [ "$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null)" != "healthy" ] && [ $$i -lt 60 ]; do i=$$((i+1)); sleep 1; done; applied=1; fi; \ + if [ "$$fx_migrate" = "1" ]; then npx prisma migrate deploy; applied=1; fi; \ + fi; \ + if [ "$$applied" = "1" ]; then echo ""; echo "Fixes applied. Re-run 'make doctor' to verify."; exit 0; fi; \ [ "$$bad" -eq 0 ] clean: ## Stop the DB and remove node_modules and the Next.js build cache From 88929fe28d479a066bf94481d0ee7f121534a64b Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Mon, 22 Jun 2026 16:50:28 +0200 Subject: [PATCH 5/5] refactor: move env and doctor logic into scripts/ Extract the env-init and doctor shell logic out of the Makefile into scripts/env-init.sh and scripts/doctor.sh, matching scripts/db-init.sh. The env and doctor targets are now thin callers. --- Makefile | 111 +----------------------------- scripts/doctor.sh | 161 ++++++++++++++++++++++++++++++++++++++++++++ scripts/env-init.sh | 19 ++++++ 3 files changed, 183 insertions(+), 108 deletions(-) create mode 100644 scripts/doctor.sh create mode 100644 scripts/env-init.sh diff --git a/Makefile b/Makefile index f4ba4dd..e2ac390 100644 --- a/Makefile +++ b/Makefile @@ -20,16 +20,7 @@ install: ## Install dependencies (also runs prisma generate) npm install env: ## Create .env.local from .env.example with generated secrets (no-op if it exists) - @if [ -f .env.local ]; then \ - echo ".env.local already exists, leaving it untouched."; \ - else \ - cp .env.example .env.local; \ - auth=$$(openssl rand -base64 32); \ - enc=$$(openssl rand -base64 32); \ - sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$$auth|" .env.local; \ - sed -i "s|^DIRECTORY_ENCRYPTION_KEY=.*|DIRECTORY_ENCRYPTION_KEY=$$enc|" .env.local; \ - echo ".env.local created with generated AUTH_SECRET and DIRECTORY_ENCRYPTION_KEY."; \ - fi + @sh scripts/env-init.sh db-up: ## Start the local PostgreSQL container npm run db:up @@ -70,104 +61,8 @@ check: ## Run the same gates CI enforces (lint, types, schema, build) npx prisma validate npm run build -doctor: ## Full setup diagnosis: toolchain, env, Docker, database, Prisma - @ok=0; warn=0; bad=0; dkr=0; \ - fx_env=0; fx_auth=0; fx_enc=0; fx_authurl=0; fx_cron=0; fx_install=0; fx_generate=0; fx_dbup=0; fx_migrate=0; \ - pass() { echo " [OK] $$1"; ok=$$((ok+1)); }; \ - wrn() { echo " [WARN] $$1"; warn=$$((warn+1)); }; \ - err() { echo " [FAIL] $$1"; bad=$$((bad+1)); }; \ - getv() { grep -E "^$$1=" .env.local 2>/dev/null | head -1 | cut -d= -f2-; }; \ - setval() { if grep -qE "^$$1=" .env.local 2>/dev/null; then sed -i "s|^$$1=.*|$$1=$$2|" .env.local; else printf '%s=%s\n' "$$1" "$$2" >> .env.local; fi; }; \ - echo "DataShield doctor"; \ - echo "================="; \ - echo "Toolchain:"; \ - nv=$$(node -v 2>/dev/null); \ - if [ -z "$$nv" ]; then err "node: not found (need Node 22)"; \ - elif [ "$${nv#v22.}" != "$$nv" ]; then pass "node $$nv"; \ - else wrn "node $$nv (project targets Node 22)"; fi; \ - if command -v npm >/dev/null 2>&1; then pass "npm $$(npm -v)"; else err "npm: not found"; fi; \ - if command -v openssl >/dev/null 2>&1; then pass "openssl present (used by 'make env')"; else wrn "openssl: not found ('make env' cannot generate secrets)"; fi; \ - echo "Environment (.env.local):"; \ - if [ ! -f .env.local ]; then err ".env.local missing (run 'make env')"; fx_env=1; \ - else \ - pass ".env.local present"; \ - [ -n "$$(getv DATABASE_URL)" ] && pass "DATABASE_URL set" || err "DATABASE_URL empty"; \ - as=$$(getv AUTH_SECRET); \ - if [ -z "$$as" ]; then err "AUTH_SECRET empty"; fx_auth=1; \ - elif [ "$${as#change-me}" != "$$as" ]; then err "AUTH_SECRET still the placeholder"; fx_auth=1; \ - else pass "AUTH_SECRET set"; fi; \ - ek=$$(getv DIRECTORY_ENCRYPTION_KEY); ekl=$$(printf %s "$$ek" | wc -c | tr -d ' '); \ - if [ -z "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY empty (app refuses directory configs)"; fx_enc=1; \ - elif [ "$${ek#change-me}" != "$$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY still the placeholder"; fx_enc=1; \ - elif [ "$$ekl" -lt 32 ]; then err "DIRECTORY_ENCRYPTION_KEY too short ($$ekl chars, need >= 32)"; fx_enc=1; \ - else pass "DIRECTORY_ENCRYPTION_KEY set ($$ekl chars)"; fi; \ - if [ -n "$$(getv AUTH_URL)" ]; then pass "AUTH_URL set"; else wrn "AUTH_URL empty (defaults to http://localhost:3000)"; fx_authurl=1; fi; \ - if [ -n "$$(getv CRON_SECRET)" ]; then pass "CRON_SECRET set"; else wrn "CRON_SECRET empty (scheduler endpoint /api/cron returns 503)"; fx_cron=1; fi; \ - [ -n "$$(getv HIBP_API_KEY)" ] && pass "HIBP_API_KEY set" || wrn "HIBP_API_KEY empty (no breach lookups unless a per-company key is stored)"; \ - rk=$$(getv RESEND_API_KEY); ef=$$(getv EMAIL_FROM); \ - if [ -n "$$rk" ] && [ -n "$$ef" ]; then pass "email configured (RESEND_API_KEY + EMAIL_FROM)"; \ - elif [ -z "$$rk" ] && [ -z "$$ef" ]; then wrn "email disabled (RESEND_API_KEY + EMAIL_FROM unset)"; \ - else wrn "email half-configured: set both RESEND_API_KEY and EMAIL_FROM or neither"; fi; \ - fi; \ - echo "Docker:"; \ - if command -v docker >/dev/null 2>&1; then pass "docker $$(docker --version | awk '{print $$3}' | tr -d ',')"; \ - else err "docker not found (needed for the local database)"; fi; \ - if docker compose version >/dev/null 2>&1; then pass "docker compose available"; else wrn "docker compose not available"; fi; \ - if docker info >/dev/null 2>&1; then pass "docker daemon running"; dkr=1; \ - else err "docker daemon not running (start Docker Desktop / enable WSL integration)"; fi; \ - st=$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null); \ - if [ "$$st" = "healthy" ]; then pass "db container healthy"; \ - elif [ -n "$$st" ]; then wrn "db container status: $$st"; \ - else wrn "db container not found (run 'make db-up')"; [ "$$dkr" = "1" ] && fx_dbup=1; fi; \ - echo "Database connectivity:"; \ - du=$$(getv DATABASE_URL); \ - hp=$$(printf %s "$$du" | sed -E 's#^[a-z]+://([^@]*@)?([^/?]+).*#\2#'); \ - host=$${hp%%:*}; port=$${hp##*:}; [ "$$port" = "$$host" ] && port=5432; \ - if [ -z "$$host" ]; then wrn "could not parse host from DATABASE_URL"; \ - elif command -v nc >/dev/null 2>&1; then \ - if nc -z -w 2 "$$host" "$$port" >/dev/null 2>&1; then pass "TCP $$host:$$port reachable"; \ - else err "TCP $$host:$$port not reachable (is the DB up?)"; fi; \ - else wrn "nc not installed, skipping TCP probe ($$host:$$port)"; fi; \ - echo "Prisma:"; \ - if [ -d node_modules ]; then pass "node_modules installed"; else err "node_modules missing (run 'make install')"; fx_install=1; fi; \ - if [ -d node_modules/.prisma/client ]; then pass "prisma client generated"; else err "prisma client missing (run 'npx prisma generate')"; fx_generate=1; fi; \ - if npx prisma validate >/dev/null 2>&1; then pass "schema valid (prisma validate)"; else err "schema invalid (run 'npx prisma validate' for details)"; fi; \ - ms=$$(npx prisma migrate status 2>&1); \ - if printf %s "$$ms" | grep -q "up to date"; then pass "migrations up to date"; \ - elif printf %s "$$ms" | grep -q "have not yet been applied"; then wrn "pending migrations (run 'make migrate')"; fx_migrate=1; \ - elif printf %s "$$ms" | grep -qE "P1001|reach|unreachable"; then err "database unreachable for migrate status"; \ - else wrn "migrate status inconclusive (run 'make migrate' / 'npx prisma migrate status')"; fi; \ - echo "================="; \ - echo "Summary: $$ok OK, $$warn warning(s), $$bad failure(s)"; \ - if [ "$$bad" -gt 0 ]; then echo "Result: NOT ready."; else echo "Result: ready."; fi; \ - errfix=$$((fx_env+fx_auth+fx_enc+fx_install+fx_generate)); \ - warnfix=$$((fx_authurl+fx_cron+fx_dbup+fx_migrate)); \ - ans=n; \ - if [ $$((errfix+warnfix)) -gt 0 ] && [ -t 0 ]; then \ - echo ""; \ - echo "Auto-fixable: $$errfix from failures, $$warnfix from warnings."; \ - printf "Fix now? [e]rrors only / [a]ll / [n]o: "; read ans; \ - fi; \ - do_err=0; do_warn=0; \ - case "$$ans" in e|E) do_err=1;; a|A) do_err=1; do_warn=1;; esac; \ - applied=0; \ - if [ "$$do_err" = "1" ]; then \ - if [ "$$fx_env" = "1" ]; then cp .env.example .env.local; setval AUTH_SECRET "$$(openssl rand -base64 32)"; setval DIRECTORY_ENCRYPTION_KEY "$$(openssl rand -base64 32)"; echo "fixed: created .env.local with generated secrets"; applied=1; \ - else \ - if [ "$$fx_auth" = "1" ]; then setval AUTH_SECRET "$$(openssl rand -base64 32)"; echo "fixed: AUTH_SECRET"; applied=1; fi; \ - if [ "$$fx_enc" = "1" ]; then setval DIRECTORY_ENCRYPTION_KEY "$$(openssl rand -base64 32)"; echo "fixed: DIRECTORY_ENCRYPTION_KEY"; applied=1; fi; \ - fi; \ - if [ "$$fx_install" = "1" ]; then npm install; applied=1; fi; \ - if [ "$$fx_generate" = "1" ]; then npx prisma generate; applied=1; fi; \ - fi; \ - if [ "$$do_warn" = "1" ]; then \ - if [ "$$fx_authurl" = "1" ]; then setval AUTH_URL "http://localhost:3000"; echo "fixed: AUTH_URL"; applied=1; fi; \ - if [ "$$fx_cron" = "1" ]; then setval CRON_SECRET "$$(openssl rand -base64 32)"; echo "fixed: CRON_SECRET"; applied=1; fi; \ - if [ "$$fx_dbup" = "1" ]; then npm run db:up; echo "waiting for db to become healthy"; i=0; while [ "$$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null)" != "healthy" ] && [ $$i -lt 60 ]; do i=$$((i+1)); sleep 1; done; applied=1; fi; \ - if [ "$$fx_migrate" = "1" ]; then npx prisma migrate deploy; applied=1; fi; \ - fi; \ - if [ "$$applied" = "1" ]; then echo ""; echo "Fixes applied. Re-run 'make doctor' to verify."; exit 0; fi; \ - [ "$$bad" -eq 0 ] +doctor: ## Full setup diagnosis with optional auto-fix (toolchain, env, Docker, DB, Prisma) + @sh scripts/doctor.sh clean: ## Stop the DB and remove node_modules and the Next.js build cache npm run db:down diff --git a/scripts/doctor.sh b/scripts/doctor.sh new file mode 100644 index 0000000..2096a5a --- /dev/null +++ b/scripts/doctor.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env sh +# Full setup diagnosis for DataShield: toolchain, env, Docker, database, Prisma. +# After the report, offers to auto-fix detected issues (interactive only), +# separating failures from warnings. Exits non-zero when failures remain. +set -u + +root="$(CDPATH= cd "$(dirname "$0")/.." && pwd)" +cd "$root" + +ok=0 +warn=0 +bad=0 +dkr=0 +fx_env=0 +fx_auth=0 +fx_enc=0 +fx_authurl=0 +fx_cron=0 +fx_install=0 +fx_generate=0 +fx_dbup=0 +fx_migrate=0 + +pass() { echo " [OK] $1"; ok=$((ok + 1)); } +wrn() { echo " [WARN] $1"; warn=$((warn + 1)); } +err() { echo " [FAIL] $1"; bad=$((bad + 1)); } +getv() { grep -E "^$1=" .env.local 2>/dev/null | head -1 | cut -d= -f2-; } +setval() { + if grep -qE "^$1=" .env.local 2>/dev/null; then + sed -i "s|^$1=.*|$1=$2|" .env.local + else + printf '%s=%s\n' "$1" "$2" >>.env.local + fi +} + +echo "DataShield doctor" +echo "=================" + +echo "Toolchain:" +nv="$(node -v 2>/dev/null || true)" +if [ -z "$nv" ]; then err "node: not found (need Node 22)" +elif [ "${nv#v22.}" != "$nv" ]; then pass "node $nv" +else wrn "node $nv (project targets Node 22)"; fi +if command -v npm >/dev/null 2>&1; then pass "npm $(npm -v)"; else err "npm: not found"; fi +if command -v openssl >/dev/null 2>&1; then pass "openssl present (used by 'make env')" +else wrn "openssl: not found ('make env' cannot generate secrets)"; fi + +echo "Environment (.env.local):" +if [ ! -f .env.local ]; then + err ".env.local missing (run 'make env')" + fx_env=1 +else + pass ".env.local present" + [ -n "$(getv DATABASE_URL)" ] && pass "DATABASE_URL set" || err "DATABASE_URL empty" + as="$(getv AUTH_SECRET)" + if [ -z "$as" ]; then err "AUTH_SECRET empty"; fx_auth=1 + elif [ "${as#change-me}" != "$as" ]; then err "AUTH_SECRET still the placeholder"; fx_auth=1 + else pass "AUTH_SECRET set"; fi + ek="$(getv DIRECTORY_ENCRYPTION_KEY)" + ekl="$(printf %s "$ek" | wc -c | tr -d ' ')" + if [ -z "$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY empty (app refuses directory configs)"; fx_enc=1 + elif [ "${ek#change-me}" != "$ek" ]; then err "DIRECTORY_ENCRYPTION_KEY still the placeholder"; fx_enc=1 + elif [ "$ekl" -lt 32 ]; then err "DIRECTORY_ENCRYPTION_KEY too short ($ekl chars, need >= 32)"; fx_enc=1 + else pass "DIRECTORY_ENCRYPTION_KEY set ($ekl chars)"; fi + if [ -n "$(getv AUTH_URL)" ]; then pass "AUTH_URL set" + else wrn "AUTH_URL empty (defaults to http://localhost:3000)"; fx_authurl=1; fi + if [ -n "$(getv CRON_SECRET)" ]; then pass "CRON_SECRET set" + else wrn "CRON_SECRET empty (scheduler endpoint /api/cron returns 503)"; fx_cron=1; fi + [ -n "$(getv HIBP_API_KEY)" ] && pass "HIBP_API_KEY set" || wrn "HIBP_API_KEY empty (no breach lookups unless a per-company key is stored)" + rk="$(getv RESEND_API_KEY)"; ef="$(getv EMAIL_FROM)" + if [ -n "$rk" ] && [ -n "$ef" ]; then pass "email configured (RESEND_API_KEY + EMAIL_FROM)" + elif [ -z "$rk" ] && [ -z "$ef" ]; then wrn "email disabled (RESEND_API_KEY + EMAIL_FROM unset)" + else wrn "email half-configured: set both RESEND_API_KEY and EMAIL_FROM or neither"; fi +fi + +echo "Docker:" +if command -v docker >/dev/null 2>&1; then pass "docker $(docker --version | awk '{print $3}' | tr -d ',')" +else err "docker not found (needed for the local database)"; fi +if docker compose version >/dev/null 2>&1; then pass "docker compose available"; else wrn "docker compose not available"; fi +if docker info >/dev/null 2>&1; then pass "docker daemon running"; dkr=1 +else err "docker daemon not running (start Docker Desktop / enable WSL integration)"; fi +st="$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null || true)" +if [ "$st" = "healthy" ]; then pass "db container healthy" +elif [ -n "$st" ]; then wrn "db container status: $st" +else wrn "db container not found (run 'make db-up')"; [ "$dkr" = "1" ] && fx_dbup=1; fi + +echo "Database connectivity:" +du="$(getv DATABASE_URL)" +hp="$(printf %s "$du" | sed -E 's#^[a-z]+://([^@]*@)?([^/?]+).*#\2#')" +host="${hp%%:*}"; port="${hp##*:}"; [ "$port" = "$host" ] && port=5432 +if [ -z "$host" ]; then wrn "could not parse host from DATABASE_URL" +elif command -v nc >/dev/null 2>&1; then + if nc -z -w 2 "$host" "$port" >/dev/null 2>&1; then pass "TCP $host:$port reachable" + else err "TCP $host:$port not reachable (is the DB up?)"; fi +else wrn "nc not installed, skipping TCP probe ($host:$port)"; fi + +echo "Prisma:" +if [ -d node_modules ]; then pass "node_modules installed"; else err "node_modules missing (run 'make install')"; fx_install=1; fi +if [ -d node_modules/.prisma/client ]; then pass "prisma client generated"; else err "prisma client missing (run 'npx prisma generate')"; fx_generate=1; fi +if npx prisma validate >/dev/null 2>&1; then pass "schema valid (prisma validate)"; else err "schema invalid (run 'npx prisma validate' for details)"; fi +ms="$(npx prisma migrate status 2>&1 || true)" +if printf %s "$ms" | grep -q "up to date"; then pass "migrations up to date" +elif printf %s "$ms" | grep -q "have not yet been applied"; then wrn "pending migrations (run 'make migrate')"; fx_migrate=1 +elif printf %s "$ms" | grep -qE "P1001|reach|unreachable"; then err "database unreachable for migrate status" +else wrn "migrate status inconclusive (run 'make migrate' / 'npx prisma migrate status')"; fi + +echo "=================" +echo "Summary: $ok OK, $warn warning(s), $bad failure(s)" +if [ "$bad" -gt 0 ]; then echo "Result: NOT ready."; else echo "Result: ready."; fi + +errfix=$((fx_env + fx_auth + fx_enc + fx_install + fx_generate)) +warnfix=$((fx_authurl + fx_cron + fx_dbup + fx_migrate)) +ans=n +if [ $((errfix + warnfix)) -gt 0 ] && [ -t 0 ]; then + echo "" + echo "Auto-fixable: $errfix from failures, $warnfix from warnings." + printf "Fix now? [e]rrors only / [a]ll / [n]o: " + read ans +fi + +do_err=0; do_warn=0 +case "$ans" in + e | E) do_err=1 ;; + a | A) do_err=1; do_warn=1 ;; +esac + +applied=0 +if [ "$do_err" = "1" ]; then + if [ "$fx_env" = "1" ]; then + cp .env.example .env.local + setval AUTH_SECRET "$(openssl rand -base64 32)" + setval DIRECTORY_ENCRYPTION_KEY "$(openssl rand -base64 32)" + echo "fixed: created .env.local with generated secrets"; applied=1 + else + if [ "$fx_auth" = "1" ]; then setval AUTH_SECRET "$(openssl rand -base64 32)"; echo "fixed: AUTH_SECRET"; applied=1; fi + if [ "$fx_enc" = "1" ]; then setval DIRECTORY_ENCRYPTION_KEY "$(openssl rand -base64 32)"; echo "fixed: DIRECTORY_ENCRYPTION_KEY"; applied=1; fi + fi + if [ "$fx_install" = "1" ]; then npm install; applied=1; fi + if [ "$fx_generate" = "1" ]; then npx prisma generate; applied=1; fi +fi +if [ "$do_warn" = "1" ]; then + if [ "$fx_authurl" = "1" ]; then setval AUTH_URL "http://localhost:3000"; echo "fixed: AUTH_URL"; applied=1; fi + if [ "$fx_cron" = "1" ]; then setval CRON_SECRET "$(openssl rand -base64 32)"; echo "fixed: CRON_SECRET"; applied=1; fi + if [ "$fx_dbup" = "1" ]; then + npm run db:up + echo "waiting for db to become healthy" + i=0 + while [ "$(docker inspect -f '{{.State.Health.Status}}' datashield-db 2>/dev/null || true)" != "healthy" ] && [ "$i" -lt 60 ]; do + i=$((i + 1)); sleep 1 + done + applied=1 + fi + if [ "$fx_migrate" = "1" ]; then npx prisma migrate deploy; applied=1; fi +fi + +if [ "$applied" = "1" ]; then + echo "" + echo "Fixes applied. Re-run 'make doctor' to verify." + exit 0 +fi +[ "$bad" -eq 0 ] diff --git a/scripts/env-init.sh b/scripts/env-init.sh new file mode 100644 index 0000000..476be08 --- /dev/null +++ b/scripts/env-init.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh +# Create .env.local from .env.example with generated secrets. +# No-op if .env.local already exists (never clobbers secrets). +set -eu + +root="$(CDPATH= cd "$(dirname "$0")/.." && pwd)" +cd "$root" + +if [ -f .env.local ]; then + echo ".env.local already exists, leaving it untouched." + exit 0 +fi + +cp .env.example .env.local +auth="$(openssl rand -base64 32)" +enc="$(openssl rand -base64 32)" +sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$auth|" .env.local +sed -i "s|^DIRECTORY_ENCRYPTION_KEY=.*|DIRECTORY_ENCRYPTION_KEY=$enc|" .env.local +echo ".env.local created with generated AUTH_SECRET and DIRECTORY_ENCRYPTION_KEY."