Dev infra: fresh-clone bootstrap for the fully-local dev stack (backend + webapp)

Adds script/bootstrap/ — idempotent Ubuntu 22.04+/WSL2/CI automation that takes
a fresh clone to a running local stack with zero production traffic:

- bootstrap_dev_stack.sh: apt packages, MySQL 8 + Redis up, storyteller DB/user,
  281 diesel migrations, idempotent role/badge seeds, generated dummy
  development-secrets.env (19 required-at-boot vars no checked-in config
  supplies), SQLX_OFFLINE backend build, frontend npm install
- run_backend_dev.sh / seed_demo_user.sh: run the API from the repo root and
  create+verify a demo login through the real POST /v1/create_account path
- dev_stack_doctor.sh: PASS/WARN/FAIL diagnosis of every prerequisite

_docs/dev_setup_local_stack.md records the requirements the scripts encode and
corrects stale claims (server reads MYSQL_URL, not DATABASE_URL; repo-root .env
is not loaded by the server; config precedence is first-value-wins;
SQLX_OFFLINE is compile-time only).

.gitattributes now forces LF for *.sh/*.env so Windows clones run under WSL2.
This commit is contained in:
Tanish Manku
2026-07-28 20:51:19 -07:00
parent 2d7bf65a23
commit 537409eb3e
7 changed files with 912 additions and 0 deletions
+5
View File
@@ -1,3 +1,8 @@
# Shell scripts and dotenv config files are executed/parsed on Linux (WSL2, CI)
# even when the repo is cloned on Windows — they must stay LF in the worktree.
*.sh text eol=lf
*.env text eol=lf
# Ignore certain code for Github language statistics
# https://github.com/github/linguist/blob/master/docs/overrides.md
old_code/trumpet-trumped.com/dictionary/cmudict-0.7b linguist-vendored
+213
View File
@@ -0,0 +1,213 @@
# Local Dev Stack — Requirements & Bootstrap
How to run the full ArtCraft dev stack **entirely locally** — the
`storyteller-web` backend plus the `artcraft-webapp` frontend — with no traffic
to production servers. Scope: webapp-first; the Tauri desktop app is a later
phase.
The automated path is:
```bash
./script/bootstrap/bootstrap_dev_stack.sh # one-time setup, idempotent, re-run anytime
./script/bootstrap/run_backend_dev.sh # terminal 1: API on http://localhost:12345
./script/bootstrap/seed_demo_user.sh # terminal 2: create demo login (localdev1 / localdev1pass)
cd frontend && npx nx dev artcraft-webapp # terminal 2: webapp on http://localhost:4201
./script/bootstrap/dev_stack_doctor.sh # health check, any time
```
This document records the underlying requirements so the scripts aren't a
black box, and corrects some stale claims in older docs.
## Supported platforms
| Platform | Status |
|---------------------|---------------------------------------------------------------------------|
| Ubuntu 22.04+ | Supported by the bootstrap scripts (native, WSL2, or CI runner/container) |
| Windows | Use WSL2: `wsl --install -d Ubuntu-22.04`, then run the scripts inside it. Prefer cloning inside the WSL filesystem (`~/...`), not `/mnt/c` — Rust/npm builds through `/mnt` are much slower |
| macOS | Not covered by the scripts; follow `_docs/dev_setup_server.md` manually |
## Requirements matrix
| Requirement | Version / detail | Needed for |
|-------------------|--------------------------------------------------|---------------------------------------------------|
| MySQL server | 8.x series (9.x unsupported by tooling) | Backend boot (eager connect) + migrations |
| Redis server | any recent | Backend boot (r2d2 pool connects eagerly) |
| Rust (stable) | no pinned MSRV; Docker builds use 1.93.0 | Building `storyteller-web`, diesel_cli |
| diesel_cli | `cargo install diesel_cli --no-default-features --features mysql,sqlite` | Running the 281 migrations |
| Node.js | 20+ (enforced by `script/common/frontend_preflight.sh`) | Frontend (Nx 21 / Vite 6) |
| npm | ships with Node; **pnpm must not be used** | Frontend install (`frontend/package-lock.json`) |
| System libs (apt) | build-essential, cmake, libssl-dev, libclang-dev, libfontconfig1-dev, fontconfig, perl, pkg-config, ffmpeg, libmysqlclient-dev, libsqlite3-dev, curl, jq | Compiling the workspace + diesel_cli; seeding scripts |
| Elasticsearch | optional (7.x) | Only 4 search endpoints; server boots without it |
| Docker | **not required** — no docker-compose exists | — |
Database identity (used consistently by docs, `.env`, and defaults):
database `storyteller`, user `storyteller`, password `password`, host
`localhost`.
## Backend: what `storyteller-web` needs to boot
Binary: `storyteller-web` in `crates/service/web/storyteller_web`
(binaries: `storyteller-web`, `docs` — Swagger UI on :8989, `docs-cli` — emits
`api.json`). Binds `0.0.0.0:12345` (`BIND_ADDRESS`), plain HTTP.
**Run it from the repo root** — the config search path and `includes/` paths
are cwd-relative.
### Config loading (corrected facts)
- Mode is selected by the `SERVER_ENVIRONMENT` **process env var only**
(default: Development). Setting it inside an env file does nothing — it is
read before any file loads.
- In Development mode, files load from
`crates/service/web/storyteller_web/config/` in this order:
`storyteller-web.common.env` → `storyteller-web.development.env` →
`storyteller-web.development-secrets.env` (gitignored; the bootstrap
generates it).
- **Precedence is first-value-wins** (dotenv never overrides an existing key):
real process env > common.env > development.env > development-secrets.env.
The secrets file therefore cannot override anything already set in
`development.env`.
- The repo-root `.env` is **NOT loaded by the server** (it passes
`ignore_legacy_dot_env_file: true`). Root `.env`'s `DATABASE_URL` feeds
diesel-cli/sqlx-cli only; the server reads **`MYSQL_URL`** (default
`mysql://storyteller:password@localhost/storyteller`).
### Hard requirements at boot
| Requirement | Behavior if missing |
|----------------------------------|--------------------------------------------------------|
| MySQL reachable via `MYSQL_URL` | Boot fails (sqlx pool connects eagerly; the DB must exist and be migrated — health-check and ban-poller threads query immediately) |
| Redis reachable via `REDIS_0_URL` (default `redis://localhost/0`) | Boot fails (r2d2 pool connects eagerly) |
| ~30 `get_env_string_required` vars present | Boot fails on the first absent var |
Of the required-present vars, 19 are supplied by **no** checked-in file and
live in the generated secrets file: `ACCESS_KEY`, `SECRET_KEY`, `REGION_NAME`,
`W2L_PRIVATE_DOWNLOAD_BUCKET_NAME`, `W2L_PUBLIC_DOWNLOAD_BUCKET_NAME`,
`RESEND_API_KEY`, `FAL_API_KEY`, `GMICLOUD_API_KEY`, `GROK_API_KEY`,
`BEEBLE_API_KEY`, `OPENAI_API_KEY`, `WORLDLABS_API_KEY`,
`SEEDANCE2PRO_COOKIES`, `SEEDANCE2PRO_WHITELIST_COOKIES`,
`SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES`, `STRIPE_FAKEYOU_ACCOUNT_ID`,
`STRIPE_ARTCRAFT_ACCOUNT_ID`, `STRIPE_ARTCRAFT_SECRET_KEY`,
`STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY`.
**Placeholder values are safe**: none of these services is contacted at boot
(clients are constructed offline; Elasticsearch, S3/R2, Stripe, Resend, and
Google-cert fetches are all lazy). Generation stays safe by construction —
handlers only validate, bill the wallet, and insert job rows; providers are
contacted exclusively by separate worker binaries that local dev never runs,
so enqueued jobs just stay pending at zero cost.
### Auth on localhost
- `POST /v1/login` takes `{"username_or_email": "...", "password": "..."}`;
success returns `{"success": true, "signed_session": "<jwt>"}` **and** a
`session` cookie.
- With the checked-in dev config (`COOKIE_DOMAIN=.jungle.horse`), the session
cookie is issued non-Secure and host-only (the Domain attribute is never
set), so plain-HTTP localhost login works as-is. The frontend additionally
stores `signed_session` in localStorage and sends it as a `session` header.
- Dev-mode CORS allows any `localhost`/`127.0.0.1` origin on any port, with
credentials.
- Health endpoint for scripts/CI: `GET /_status`.
### Build
- Compile with `SQLX_OFFLINE=true` (uses the checked-in `.sqlx/` cache; 483
queries). Without it, the sqlx macros open a **live DB connection at compile
time** using `DATABASE_URL` from the root `.env`. `SQLX_OFFLINE` has no
runtime effect — ignore older notes saying not to set it when running.
- First build takes several minutes; the crate has ~370 pre-existing warnings.
## Database: migrations and seed data
- 281 Diesel migrations in `_database/sql/migrations/` (`diesel.toml` at repo
root points there). Run from the repo root: `diesel migration run`.
`"Encountered unknown type for Mysql: enum"` warnings are harmless.
`migrations_squashed/` is documentation only — not executable.
- Seed data: `_database/sql/seed/sql/system_roles.sql` (roles `user`, `mod`,
`admin`) and `user_badges.sql`. **The `user` role is mandatory** — account
creation hardcodes `user_role_slug='user'`. The legacy wrapper
`bootstrap_inserts_roles_etc.sh` must run with cwd `_database/sql/` and is
not idempotent; the bootstrap script applies the same SQL files with
idempotency guards instead.
- Demo user: created through **`POST /v1/create_account`** (no captcha, no
invite code, no email-verification gate; password just needs 6+ chars). This
exercises the real signup path instead of hand-rolled SQL. Gotcha: usernames
like `demo`, `admin`, `test`, `dev`, `artcraft` are reserved
(`includes/binary_includes/usernames/reserved_usernames.txt`) — the default
demo user is `localdev1`. The Rust seeder
(`cargo run --bin dev-database-seed`) exists but hard-requires a
`.env-secrets` file and also seeds model weights, so the HTTP path is
preferred.
## Frontend: `artcraft-webapp`
- Nx workspace at repo-root `frontend/` (npm workspaces; `npm install` from
`frontend/`). The FontAwesome Pro registry token is committed in
`frontend/.npmrc` — no manual auth step.
- Serve: `npx nx dev artcraft-webapp` → `http://localhost:4201`. No lib
pre-build is needed in dev (libs resolve to TS source via the
`"development"` export condition); production `nx build` does need lib
builds.
- **In dev the webapp already targets the local backend**: `src/main.tsx`
calls `StorytellerApiHostStore.setDevelopment()` under
`import.meta.env.DEV`, pointing all API traffic at
`http://localhost:12345`. (The adjacent comment claiming this is disabled is
stale — the call is live, and it bypasses the Vite `/v1` →
`api.storyteller.ai` proxy.) So: local backend running → fully local stack;
local backend **not** running → dev webapp is broken, not silently on prod.
- Optional: `VITE_GOOGLE_CLIENT_ID` in `frontend/apps/artcraft-webapp/.env`
enables the Google SSO button; email/password login needs nothing.
- Other dev ports if you run more apps: website 4200, dashboard 4202 (separate
pnpm app, excluded from Nx), video-info-website 4250, genhub 4300, Tauri
frontend 5173.
## What still touches production (known, deliberate seams)
Cleaning these up is the next phase of the infra work; the bootstrap does not
change runtime behavior:
| Seam | Today |
|-----------------------------------------------|--------------------------------------------------------------|
| Media/CDN URLs in API responses | Compiled-in constants (`cdn_link.rs`) point dev builds at a public `pub-….r2.dev` bucket — media loads remotely even with a local backend |
| Vite `/v1` proxy in webapp/website | Hardcoded `https://api.storyteller.ai` (bypassed in dev by `setDevelopment()`, but still the fallback path) |
| `SegmentationApi`, `GetCdnOrigin`, `API_TARGETS` | Hardcoded off-store production hosts |
| Elasticsearch-backed search | `search_featured` / `search_session` media files, `weights/search`, legacy `tts/search` fail without a local ES + `elasticsearch-cli` reindex (optional) |
## CI usage
The same bootstrap works on a fresh Ubuntu runner/container:
```bash
export MYSQL_ROOT_PASSWORD=root # GitHub Actions' preinstalled MySQL uses password auth
./script/bootstrap/bootstrap_dev_stack.sh --yes
./script/bootstrap/run_backend_dev.sh &
./script/bootstrap/seed_demo_user.sh # waits for /_status
./script/bootstrap/dev_stack_doctor.sh
```
`--yes` answers all prompts; `--skip-frontend` / `--skip-rust-build` /
`--skip-packages` slice the work for focused jobs.
## Verification checklist
- `./script/bootstrap/dev_stack_doctor.sh` exits 0 with no FAILs.
- `POST /v1/login` with the demo user returns `success: true` plus a
`signed_session` (done automatically by `seed_demo_user.sh`).
- Webapp at `http://localhost:4201` can log in as `localdev1`.
- A generation enqueue returns `{"success": true, "inference_job_token": …}`
and no external provider is contacted (no workers running).
- Nothing hits `api.storyteller.ai` during the above (verify in the browser
network tab; media/CDN requests are the known exception — see seams table).
## Corrections to older docs
- `crates/service/web/storyteller_web/CLAUDE.md`: the repo-root `.env` is
*not* auto-loaded by the server, and `SQLX_OFFLINE=true` is fine (and
recommended) when running — it only affects compile-time query checking.
- `_docs/dev_setup_server.md`: `./script/dev_mysql_connect.sh` doesn't exist —
it's `./script/dev_database_connect.sh`.
- `diesel.toml`'s comment about needing an unreleased diesel CLI is stale —
any current `diesel_cli` works.
- `TESTING_INFRA_HANDOFF.md` (untracked): the server env var is `MYSQL_URL`,
not `DATABASE_URL`; migration count is 281; crate path is
`crates/service/web/storyteller_web`.
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env bash
# Bootstrap the full local dev stack (backend + frontend) on a fresh clone.
#
# Target platform: Ubuntu 22.04+ — native, WSL2, or a CI runner/container.
# (macOS users: follow _docs/dev_setup_server.md manually; Windows users: run
# this inside WSL2. See _docs/dev_setup_local_stack.md.)
#
# Idempotent: safe to re-run at any time; each step detects work already done.
#
# Usage:
# ./script/bootstrap/bootstrap_dev_stack.sh [options]
#
# Options:
# --yes, -y Non-interactive: assume "yes" for prompts (CI mode)
# --skip-packages Don't apt-install system packages
# --skip-rust-build Don't build the storyteller-web binary
# --skip-frontend Don't run the frontend npm install
#
# After it succeeds:
# ./script/bootstrap/run_backend_dev.sh # start the API on :12345
# ./script/bootstrap/seed_demo_user.sh # create the demo login
# cd frontend && npx nx dev artcraft-webapp # webapp on :4201
set -euo pipefail
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${root_dir}/script/bootstrap/common.sh"
SECRETS_ENV_FILE="${root_dir}/crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env"
# System packages: MySQL + Redis servers, the native libs needed to compile the
# Rust workspace (per build/service_cpu.Dockerfile) and diesel_cli
# (libmysqlclient-dev, libsqlite3-dev), plus curl/jq for the seeding scripts.
SYSTEM_PACKAGES=(
build-essential
ca-certificates
cmake
curl
ffmpeg
fontconfig
git
jq
libclang-dev
libfontconfig1-dev
libmysqlclient-dev
libsqlite3-dev
libssl-dev
mysql-server
perl
pkg-config
redis-server
)
ASSUME_YES=false
SKIP_PACKAGES=false
SKIP_RUST_BUILD=false
SKIP_FRONTEND=false
main() {
parse_args "$@"
ensure_supported_platform
install_system_packages
ensure_mysql_running
provision_mysql_database
ensure_redis_running
ensure_rust_toolchain
ensure_diesel_cli
run_migrations
seed_roles_and_badges
write_secrets_env_if_missing
build_backend
setup_frontend
print_next_steps
}
parse_args() {
for arg in "$@"; do
case "${arg}" in
--yes|-y) ASSUME_YES=true ;;
--skip-packages) SKIP_PACKAGES=true ;;
--skip-rust-build) SKIP_RUST_BUILD=true ;;
--skip-frontend) SKIP_FRONTEND=true ;;
--help|-h) sed -n '2,22p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "Unknown option: ${arg} (try --help)" ;;
esac
done
}
ensure_supported_platform() {
case "$(uname -s)" in
Linux) ;;
Darwin)
die "This script targets Ubuntu/WSL2. On macOS, follow _docs/dev_setup_server.md."
;;
*)
die "Unsupported platform '$(uname -s)'. Run inside WSL2 Ubuntu (wsl --install -d Ubuntu-22.04)."
;;
esac
if grep -qi microsoft /proc/version 2>/dev/null && [[ "${root_dir}" == /mnt/* ]]; then
warn "This checkout lives on the Windows filesystem (${root_dir})."
warn "Rust + npm builds are dramatically slower through /mnt. Consider cloning"
warn "into the WSL filesystem (e.g. ~/storyteller-rust) instead."
fi
}
install_system_packages() {
step "System packages"
if [ "${SKIP_PACKAGES}" = "true" ]; then
log "Skipped (--skip-packages)."
return
fi
local missing=()
for pkg in "${SYSTEM_PACKAGES[@]}"; do
dpkg -s "${pkg}" >/dev/null 2>&1 || missing+=("${pkg}")
done
if [ "${#missing[@]}" -eq 0 ]; then
log "All required apt packages are already installed."
return
fi
log "Missing packages: ${missing[*]}"
confirm "Install them with apt-get?" || die "Cannot continue without system packages (or pass --skip-packages if you manage them yourself)."
maybe_sudo env DEBIAN_FRONTEND=noninteractive apt-get update
maybe_sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}"
}
ensure_mysql_running() {
step "MySQL server"
if mysqladmin ping --silent 2>/dev/null; then
log "MySQL is running."
else
log "Starting MySQL..."
maybe_sudo service mysql start 2>/dev/null \
|| maybe_sudo systemctl start mysql 2>/dev/null \
|| die "Could not start MySQL (tried 'service mysql start' and 'systemctl start mysql')."
mysqladmin ping --silent 2>/dev/null || sleep 3
mysqladmin ping --silent 2>/dev/null || die "MySQL did not come up."
fi
# The tooling supports the MySQL 8.x series; 9.x is known-unsupported
# (_docs/dev_setup_server.md).
local version
version="$(mysqld --version 2>/dev/null || true)"
case "${version}" in
*" 8."*) ;;
"") warn "Could not determine MySQL server version." ;;
*) warn "MySQL 8.x is expected; found: ${version}" ;;
esac
}
provision_mysql_database() {
step "MySQL database + user (${DEV_MYSQL_DB} / ${DEV_MYSQL_USER}@localhost)"
if mysql_app_reachable; then
log "Database and app user already provisioned."
return
fi
# Same DDL as _docs/dev_setup_server.md, made idempotent.
mysql_admin <<SQL
CREATE DATABASE IF NOT EXISTS ${DEV_MYSQL_DB};
CREATE USER IF NOT EXISTS '${DEV_MYSQL_USER}'@'localhost' IDENTIFIED BY '${DEV_MYSQL_PASSWORD}';
GRANT ALL PRIVILEGES ON ${DEV_MYSQL_DB}.* TO '${DEV_MYSQL_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
mysql_app_reachable || die "Provisioning ran but '${DEV_MYSQL_USER}' still cannot reach '${DEV_MYSQL_DB}'."
log "Provisioned."
}
ensure_redis_running() {
step "Redis server"
if redis_reachable; then
log "Redis is running."
return
fi
log "Starting Redis..."
maybe_sudo service redis-server start 2>/dev/null \
|| maybe_sudo systemctl start redis-server 2>/dev/null \
|| die "Could not start Redis."
redis_reachable || die "Redis did not come up."
}
ensure_rust_toolchain() {
step "Rust toolchain"
ensure_cargo_on_path
if command -v cargo >/dev/null 2>&1; then
log "Found $(rustc --version 2>/dev/null || echo 'cargo (rustc missing?)')."
return
fi
confirm "Rust is not installed. Install stable via rustup?" || die "Rust is required to build the backend."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
ensure_cargo_on_path
command -v cargo >/dev/null 2>&1 || die "rustup install finished but cargo is not on PATH."
}
ensure_diesel_cli() {
step "diesel_cli (migration runner)"
if command -v diesel >/dev/null 2>&1; then
log "Found $(diesel --version)."
return
fi
# Same install command as _docs/dev_setup_server.md. Needs libmysqlclient-dev
# and libsqlite3-dev (installed above).
log "Installing diesel_cli (this compiles from source; a few minutes)..."
cargo install diesel_cli --no-default-features --features mysql,sqlite
}
run_migrations() {
step "Database migrations (_database/sql/migrations)"
# diesel-cli reads DATABASE_URL; the repo-root .env provides it, but export a
# default so this also works if that file is ever absent (e.g. sparse CI).
export DATABASE_URL="${DATABASE_URL:-mysql://${DEV_MYSQL_USER}:${DEV_MYSQL_PASSWORD}@${DEV_MYSQL_HOST}/${DEV_MYSQL_DB}}"
cd "${root_dir}"
# NB: "Encountered unknown type for Mysql: enum" warnings are harmless
# (_docs/dev_setup_server.md).
diesel migration run
log "Migrations up to date."
}
seed_roles_and_badges() {
step "Seed data (system roles + badges)"
# Same data as _database/sql/seed/bootstrap_inserts_roles_etc.sh, but guarded
# so re-runs don't hit duplicate-key errors. The 'user' role is mandatory:
# account creation hardcodes user_role_slug='user'.
local role_count badge_count
role_count="$(mysql_app -N -e "SELECT COUNT(*) FROM user_roles" 2>/dev/null || echo 0)"
if [ "${role_count}" -gt 0 ]; then
log "user_roles already seeded (${role_count} rows)."
else
mysql_app < "${root_dir}/_database/sql/seed/sql/system_roles.sql"
log "Inserted system roles (user, mod, admin)."
fi
badge_count="$(mysql_app -N -e "SELECT COUNT(*) FROM badges" 2>/dev/null || echo 0)"
if [ "${badge_count}" -gt 0 ]; then
log "badges already seeded (${badge_count} rows)."
else
mysql_app < "${root_dir}/_database/sql/seed/sql/user_badges.sql"
log "Inserted badges."
fi
}
write_secrets_env_if_missing() {
step "Development secrets file"
if [ -f "${SECRETS_ENV_FILE}" ]; then
log "Already exists: ${SECRETS_ENV_FILE} (leaving it untouched)."
return
fi
# Every env var below is read with get_env_string_required at server startup
# (build_dependencies.rs / setup_inference_providers.rs / setup_stripe_*.rs)
# but is NOT supplied by the checked-in config files. None of them is
# contacted at boot — placeholder values are enough to run the server; the
# endpoints that would use them fail lazily if exercised. Generation is still
# safe: providers are only contacted by the separate job worker binaries,
# which local dev does not run.
#
# NOTE: config loading is FIRST-VALUE-WINS (dotenv never overrides an
# already-set key), so values here cannot override anything already set in
# storyteller-web.development.env — only real process env vars can.
cat > "${SECRETS_ENV_FILE}" <<'EOF'
# Local-development secrets for storyteller-web. Generated by
# script/bootstrap/bootstrap_dev_stack.sh — gitignored; edit freely.
#
# Placeholders below satisfy required-at-boot checks without enabling any
# external integration. Replace individual values with real dev credentials
# only if you need that specific integration locally.
# R2/S3 object storage (clients are constructed offline)
ACCESS_KEY=dummy-local-dev
SECRET_KEY=dummy-local-dev
REGION_NAME=us-east-1
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME=dummy-local-dev-private
W2L_PUBLIC_DOWNLOAD_BUCKET_NAME=dummy-local-dev-public
# Email (Resend)
RESEND_API_KEY=dummy-local-dev
# Generation providers (only job workers ever contact these; workers are not
# run in local dev, so enqueued jobs simply stay pending)
FAL_API_KEY=dummy-local-dev
GMICLOUD_API_KEY=dummy-local-dev
GROK_API_KEY=dummy-local-dev
BEEBLE_API_KEY=dummy-local-dev
OPENAI_API_KEY=dummy-local-dev
WORLDLABS_API_KEY=dummy-local-dev
SEEDANCE2PRO_COOKIES=dummy-local-dev
SEEDANCE2PRO_WHITELIST_COOKIES=dummy-local-dev
SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES=dummy-local-dev
# Stripe account ids (test-mode secret keys are already checked into
# storyteller-web.development.env; only the account ids are missing there)
STRIPE_FAKEYOU_ACCOUNT_ID=acct_dummylocaldev
STRIPE_ARTCRAFT_ACCOUNT_ID=acct_dummylocaldev
STRIPE_ARTCRAFT_SECRET_KEY=sk_test_dummylocaldev
STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY=whsec_dummylocaldev
EOF
log "Wrote ${SECRETS_ENV_FILE}"
}
build_backend() {
step "Backend build (storyteller-web)"
if [ "${SKIP_RUST_BUILD}" = "true" ]; then
log "Skipped (--skip-rust-build)."
return
fi
cd "${root_dir}"
# SQLX_OFFLINE only affects compile-time query verification (uses the
# checked-in .sqlx/ cache instead of a live DB); it has no runtime effect.
log "Building (first build takes several minutes)..."
SQLX_OFFLINE=true cargo build -p storyteller-web --bin storyteller-web
log "Built target/debug/storyteller-web."
}
setup_frontend() {
step "Frontend install (frontend/)"
if [ "${SKIP_FRONTEND}" = "true" ]; then
log "Skipped (--skip-frontend)."
return
fi
source "${root_dir}/script/common/frontend_preflight.sh"
frontend_preflight "${root_dir}/frontend"
pushd "${root_dir}/frontend" >/dev/null
frontend_npm_install
popd >/dev/null
log "Frontend dependencies installed."
}
print_next_steps() {
step "Done"
cat <<EOF
The dev stack is bootstrapped. Next steps:
1. Start the backend (binds ${DEV_BACKEND_URL}):
./script/bootstrap/run_backend_dev.sh
2. In another terminal, create the demo login (idempotent):
./script/bootstrap/seed_demo_user.sh
Credentials: ${DEMO_USERNAME} / ${DEMO_PASSWORD}
3. Start the webapp (binds http://localhost:4201; in dev it talks to
${DEV_BACKEND_URL} automatically):
cd frontend && npx nx dev artcraft-webapp
Health checks at any time:
./script/bootstrap/dev_stack_doctor.sh
EOF
}
main "$@"
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Shared helpers for the dev-stack bootstrap scripts.
#
# Usage (from a script that has set ${root_dir}):
#
# source "${root_dir}/script/bootstrap/common.sh"
#
# See _docs/dev_setup_local_stack.md for the full picture.
# Canonical local-dev database identity. These match _docs/dev_setup_server.md,
# the diesel DATABASE_URL in the repo-root .env, and the server's MYSQL_URL
# default (crates/schema/config/shared_env_var_config/src/mysql.rs). Override
# via environment only if your local MySQL differs — most tooling assumes them.
DEV_MYSQL_DB="${DEV_MYSQL_DB:-storyteller}"
DEV_MYSQL_USER="${DEV_MYSQL_USER:-storyteller}"
DEV_MYSQL_PASSWORD="${DEV_MYSQL_PASSWORD:-password}"
DEV_MYSQL_HOST="${DEV_MYSQL_HOST:-localhost}"
# Where the backend listens (BIND_ADDRESS default in
# storyteller_web/src/startup/build_dependencies.rs).
DEV_BACKEND_URL="${DEV_BACKEND_URL:-http://localhost:12345}"
# Default demo account. The username must NOT be on the reserved-usernames list
# (includes/binary_includes/usernames/reserved_usernames.txt — "demo", "admin",
# "test", and "dev" are all reserved) and must match ^[A-Za-z0-9_\-]{3,16}$.
DEMO_USERNAME="${DEMO_USERNAME:-localdev1}"
DEMO_PASSWORD="${DEMO_PASSWORD:-localdev1pass}"
DEMO_EMAIL="${DEMO_EMAIL:-localdev1@example.com}"
log() { echo "[bootstrap] $*"; }
warn() { echo "[bootstrap] WARNING: $*" >&2; }
die() { echo "[bootstrap] ERROR: $*" >&2; exit 1; }
step() {
echo ""
echo "=== $* ==="
}
# sudo unless we already are root (CI containers often run as root without sudo).
maybe_sudo() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
sudo "$@"
fi
}
confirm() {
local prompt="$1"
if [ "${ASSUME_YES:-false}" = "true" ]; then
return 0
fi
local reply
read -r -p "${prompt} [y/N] " reply
[[ "${reply}" =~ ^[Yy] ]]
}
# Run a statement (or stdin) as the storyteller app user against the dev DB.
# MYSQL_PWD keeps the password off the process list.
mysql_app() {
MYSQL_PWD="${DEV_MYSQL_PASSWORD}" mysql \
-u "${DEV_MYSQL_USER}" -h "${DEV_MYSQL_HOST}" -D "${DEV_MYSQL_DB}" "$@"
}
# Run a statement as an administrator, for provisioning the DB and user.
# On a fresh Ubuntu/WSL install, root uses auth_socket, so `sudo mysql` works.
# On GitHub Actions runners the preinstalled MySQL root password is "root" —
# export MYSQL_ROOT_PASSWORD to use password auth instead.
mysql_admin() {
if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then
MYSQL_PWD="${MYSQL_ROOT_PASSWORD}" mysql -u root -h "${DEV_MYSQL_HOST}" "$@"
else
maybe_sudo mysql "$@"
fi
}
mysql_app_reachable() {
mysql_app -e "SELECT 1" >/dev/null 2>&1
}
redis_reachable() {
redis-cli ping 2>/dev/null | grep -q PONG
}
backend_reachable() {
curl -sf -o /dev/null --max-time 3 "${DEV_BACKEND_URL}/_status"
}
# Make ~/.cargo/bin visible when rustup was installed but the shell profile
# hasn't been re-sourced (fresh installs, CI).
ensure_cargo_on_path() {
if ! command -v cargo >/dev/null 2>&1 && [ -x "${HOME}/.cargo/bin/cargo" ]; then
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
}
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Diagnose the local dev stack: checks every prerequisite the bootstrap sets up
# and reports PASS / WARN / FAIL per item.
#
# FAIL = the stack cannot work until fixed (exit code 1).
# WARN = optional or run-time-only (e.g. backend not currently running).
set -uo pipefail
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${root_dir}/script/bootstrap/common.sh"
SECRETS_ENV_FILE="${root_dir}/crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env"
failures=0
warnings=0
pass() { printf ' PASS %s\n' "$1"; }
fail() { printf ' FAIL %s\n' "$1"; failures=$((failures + 1)); }
warn_check() { printf ' WARN %s\n' "$1"; warnings=$((warnings + 1)); }
echo "Dev stack doctor (${root_dir})"
echo ""
echo "--- Toolchains ---"
ensure_cargo_on_path
if command -v cargo >/dev/null 2>&1; then
pass "rust: $(rustc --version 2>/dev/null | head -1)"
else
fail "rust: cargo not on PATH (bootstrap installs via rustup)"
fi
if command -v diesel >/dev/null 2>&1; then
pass "diesel_cli: $(diesel --version 2>/dev/null | head -1)"
else
fail "diesel_cli: not installed (cargo install diesel_cli --no-default-features --features mysql,sqlite)"
fi
if command -v node >/dev/null 2>&1; then
node_major="$(node --version | sed -E 's/^v([0-9]+).*/\1/')"
if [ "${node_major}" -ge 20 ]; then
pass "node: $(node --version) (>= 20 required by Nx 21 / Vite 6)"
else
fail "node: $(node --version) is too old — Node 20+ required"
fi
else
fail "node: not installed (Node 20+ required for the frontend)"
fi
echo ""
echo "--- Services ---"
if mysql_app_reachable; then
pass "mysql: reachable as '${DEV_MYSQL_USER}' on database '${DEV_MYSQL_DB}'"
migration_dirs="$(find "${root_dir}/_database/sql/migrations" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
applied="$(mysql_app -N -e "SELECT COUNT(*) FROM __diesel_schema_migrations" 2>/dev/null || echo 0)"
if [ "${applied}" -ge "${migration_dirs}" ] && [ "${applied}" -gt 0 ]; then
pass "migrations: ${applied} applied (${migration_dirs} in _database/sql/migrations)"
elif [ "${applied}" -gt 0 ]; then
fail "migrations: only ${applied}/${migration_dirs} applied — run 'diesel migration run'"
else
fail "migrations: none applied — run 'diesel migration run'"
fi
role_count="$(mysql_app -N -e "SELECT COUNT(*) FROM user_roles" 2>/dev/null || echo 0)"
if [ "${role_count}" -ge 3 ]; then
pass "seed: user_roles has ${role_count} rows (user/mod/admin present)"
else
fail "seed: user_roles has ${role_count} rows — account creation needs the 'user' role (re-run bootstrap)"
fi
badge_count="$(mysql_app -N -e "SELECT COUNT(*) FROM badges" 2>/dev/null || echo 0)"
if [ "${badge_count}" -gt 0 ]; then
pass "seed: badges has ${badge_count} rows"
else
warn_check "seed: badges table is empty (cosmetic; re-run bootstrap to fill)"
fi
demo_count="$(mysql_app -N -e "SELECT COUNT(*) FROM users WHERE username='${DEMO_USERNAME}'" 2>/dev/null || echo 0)"
if [ "${demo_count}" -gt 0 ]; then
pass "demo user: '${DEMO_USERNAME}' exists"
else
warn_check "demo user: '${DEMO_USERNAME}' not created yet (./script/bootstrap/seed_demo_user.sh, backend must be running)"
fi
else
fail "mysql: cannot connect as '${DEV_MYSQL_USER}'@'${DEV_MYSQL_HOST}' to '${DEV_MYSQL_DB}' (is MySQL running? run bootstrap)"
fi
if redis_reachable; then
pass "redis: PONG"
else
fail "redis: not reachable on localhost (the backend's r2d2 pool connects eagerly at boot)"
fi
echo ""
echo "--- Backend ---"
if [ -f "${SECRETS_ENV_FILE}" ]; then
pass "secrets env: ${SECRETS_ENV_FILE#"${root_dir}"/} exists"
else
fail "secrets env: missing — the server aborts at boot on ~19 required vars (re-run bootstrap)"
fi
if [ -x "${root_dir}/target/debug/storyteller-web" ] || [ -x "${root_dir}/target/release/storyteller-web" ]; then
pass "binary: storyteller-web is built"
else
warn_check "binary: storyteller-web not built yet (bootstrap builds it; cargo run will build on demand)"
fi
if backend_reachable; then
pass "server: ${DEV_BACKEND_URL}/_status responds"
else
warn_check "server: not running (./script/bootstrap/run_backend_dev.sh)"
fi
echo ""
echo "--- Frontend ---"
if [ -d "${root_dir}/frontend/node_modules" ]; then
pass "frontend: node_modules present"
else
warn_check "frontend: node_modules missing (cd frontend && npm install)"
fi
if [ -e "${root_dir}/frontend/pnpm-lock.yaml" ] || [ -d "${root_dir}/frontend/node_modules/.pnpm" ]; then
fail "frontend: stale pnpm artifacts detected (see frontend/README.md — the workspace uses npm)"
else
pass "frontend: no stale pnpm artifacts"
fi
echo ""
echo "Summary: ${failures} failure(s), ${warnings} warning(s)."
if [ "${failures}" -gt 0 ]; then
echo "Fix failures with: ./script/bootstrap/bootstrap_dev_stack.sh (safe to re-run)"
exit 1
fi
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Run the storyteller-web backend for local development (binds 0.0.0.0:12345).
#
# Must run from a bootstrapped environment (./script/bootstrap/bootstrap_dev_stack.sh).
# Always runs from the repo root: the server's config search path
# (crates/service/web/storyteller_web/config/*.env) and its includes/ defaults
# are cwd-relative.
set -euo pipefail
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${root_dir}/script/bootstrap/common.sh"
SECRETS_ENV_FILE="${root_dir}/crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env"
mysql_app_reachable || die "MySQL is not reachable as '${DEV_MYSQL_USER}'. Run ./script/bootstrap/bootstrap_dev_stack.sh first."
redis_reachable || die "Redis is not reachable. Run ./script/bootstrap/bootstrap_dev_stack.sh first."
[ -f "${SECRETS_ENV_FILE}" ] || die "Missing ${SECRETS_ENV_FILE}. Run ./script/bootstrap/bootstrap_dev_stack.sh first."
ensure_cargo_on_path
cd "${root_dir}"
# SERVER_ENVIRONMENT defaults to Development when unset (bootstrap.rs), and the
# dev config files are picked up from the search path automatically.
# SQLX_OFFLINE only affects compile-time query checking, never runtime.
log "Starting storyteller-web on ${DEV_BACKEND_URL} (Ctrl-C to stop)..."
exec env SQLX_OFFLINE=true cargo run -p storyteller-web --bin storyteller-web
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Create (or verify) the local demo user via the running backend, then verify
# that login works. Idempotent: an already-existing user is a success.
#
# Requires the backend to be up (./script/bootstrap/run_backend_dev.sh) — this
# script waits up to --wait-seconds (default 60) for it.
#
# The account is created through POST /v1/create_account rather than raw SQL so
# it exercises the real signup path (bcrypt hash, session, firehose row) and
# stays correct if the users schema evolves.
#
# Override the credentials via DEMO_USERNAME / DEMO_PASSWORD / DEMO_EMAIL.
# Username rules: 3-16 chars of [A-Za-z0-9_-], not on the reserved list
# (includes/binary_includes/usernames/reserved_usernames.txt); password >= 6 chars.
set -euo pipefail
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${root_dir}/script/bootstrap/common.sh"
WAIT_SECONDS=60
for arg in "$@"; do
case "${arg}" in
--wait-seconds=*) WAIT_SECONDS="${arg#*=}" ;;
--help|-h) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "Unknown option: ${arg} (try --help)" ;;
esac
done
command -v jq >/dev/null 2>&1 || die "jq is required (apt-get install jq)."
log "Waiting for backend at ${DEV_BACKEND_URL} (up to ${WAIT_SECONDS}s)..."
deadline=$((SECONDS + WAIT_SECONDS))
until backend_reachable; do
if [ "${SECONDS}" -ge "${deadline}" ]; then
die "Backend not reachable at ${DEV_BACKEND_URL}/_status. Start it with ./script/bootstrap/run_backend_dev.sh"
fi
sleep 2
done
log "Backend is up."
log "Creating account '${DEMO_USERNAME}'..."
create_response="$(curl -sS -X POST "${DEV_BACKEND_URL}/v1/create_account" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg u "${DEMO_USERNAME}" \
--arg p "${DEMO_PASSWORD}" \
--arg e "${DEMO_EMAIL}" \
'{username: $u, password: $p, password_confirmation: $p, email_address: $e}')")"
if [ "$(echo "${create_response}" | jq -r '.success // false')" = "true" ]; then
log "Account created."
else
error_code="$(echo "${create_response}" | jq -r '.error_code // .error_code_str // empty')"
case "${error_code}" in
*[Uu]sername[Tt]aken*|*[Ee]mail[Tt]aken*)
log "Account already exists (${error_code}) — continuing."
;;
*)
die "Account creation failed: ${create_response}"
;;
esac
fi
log "Verifying login..."
login_response="$(curl -sS -X POST "${DEV_BACKEND_URL}/v1/login" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg u "${DEMO_USERNAME}" \
--arg p "${DEMO_PASSWORD}" \
'{username_or_email: $u, password: $p}')")"
if [ "$(echo "${login_response}" | jq -r '.success // false')" != "true" ]; then
die "Login verification failed: ${login_response}"
fi
if [ -z "$(echo "${login_response}" | jq -r '.signed_session // empty')" ]; then
die "Login succeeded but returned no signed_session: ${login_response}"
fi
log "Demo user is ready."
log " Username: ${DEMO_USERNAME}"
log " Password: ${DEMO_PASSWORD}"
log "Log in at the webapp /login page, or via curl:"
log " curl -c /tmp/cookies.txt -X POST ${DEV_BACKEND_URL}/v1/login -H 'Content-Type: application/json' -d '{\"username_or_email\":\"${DEMO_USERNAME}\",\"password\":\"${DEMO_PASSWORD}\"}'"