From 410ff27d2d19a6c4451768288e66b2c9d86e5312 Mon Sep 17 00:00:00 2001 From: Programcode <117768520+Programcode@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:29:24 +0200 Subject: [PATCH] feat: configure Superset for parallel workspaces Wire up .superset/config.json (setup/run/teardown) so a new workspace installs deps automatically, and add a shared port allocator so several worktrees can run `npm run dev` at once without colliding on the hardcoded :8080. Serialize the Docker-backed Supabase DB gate behind a lock instead, since it has no per-project port config. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0163LcrphWJjYjTXM33mQuH6 --- .gitignore | 3 ++ .superset/config.json | 5 +++ .superset/db-gate.sh | 23 +++++++++++++ .superset/dev.sh | 19 +++++++++++ .superset/lib/lock.sh | 44 +++++++++++++++++++++++++ .superset/lib/port-alloc.cjs | 64 ++++++++++++++++++++++++++++++++++++ .superset/teardown.sh | 19 +++++++++++ AGENTS.md | 6 ++++ vite.config.ts | 2 +- 9 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 .superset/config.json create mode 100755 .superset/db-gate.sh create mode 100755 .superset/dev.sh create mode 100755 .superset/lib/lock.sh create mode 100755 .superset/lib/port-alloc.cjs create mode 100755 .superset/teardown.sh diff --git a/.gitignore b/.gitignore index f3cf0c306..25dd91035 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,9 @@ supabase/.env.* # Local lock generated by Deno checks deno.lock +# Personal Superset workspace overrides +.superset/config.local.json + # Local agent/task files .claude/ .codex/ diff --git a/.superset/config.json b/.superset/config.json new file mode 100644 index 000000000..2f775dd66 --- /dev/null +++ b/.superset/config.json @@ -0,0 +1,5 @@ +{ + "setup": ["npm ci"], + "run": ["./.superset/dev.sh"], + "teardown": ["./.superset/teardown.sh"] +} diff --git a/.superset/db-gate.sh b/.superset/db-gate.sh new file mode 100755 index 000000000..a8fda9a46 --- /dev/null +++ b/.superset/db-gate.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Runs the Docker-backed Supabase migration/pgTAP gate +# (`supabase db start` + `npm run test:db`) with exclusive access across +# Superset workspaces of this repo. `supabase/config.toml` sets no +# project_id or custom port block, so `supabase db start` always binds the +# CLI's default ports and container names - two workspaces running it at +# once collide in Docker, not just on a socket. Use this instead of calling +# `supabase db start` directly whenever more than one workspace might be +# active. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DIR/lib/lock.sh" + +DB_LOCK="$HOME/.superset/debatelab-db.lock" + +echo "Waiting for exclusive access to the local Supabase stack..." +lock_acquire "$DB_LOCK" 600 +trap 'lock_release "$DB_LOCK"' EXIT + +supabase db start +npm run test:db +supabase stop --no-backup diff --git a/.superset/dev.sh b/.superset/dev.sh new file mode 100755 index 000000000..0bf881bd8 --- /dev/null +++ b/.superset/dev.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Superset "run" script: starts the Vite dev server on a port unique to this +# workspace so several DebateLab workspaces can run `npm run dev` at once +# without fighting over :8080 (vite.config.ts hardcodes that port otherwise). +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DIR/lib/lock.sh" + +WORKSPACE_KEY="$(pwd)" +PORTS_LOCK="$HOME/.superset/debatelab-ports/lock" + +lock_acquire "$PORTS_LOCK" 30 +PORT="$(node "$DIR/lib/port-alloc.cjs" allocate "$WORKSPACE_KEY" 8080 8179)" +lock_release "$PORTS_LOCK" + +echo "Superset workspace dev server: http://127.0.0.1:${PORT}" +export PORT +exec npm run dev -- --host 127.0.0.1 --port "$PORT" diff --git a/.superset/lib/lock.sh b/.superset/lib/lock.sh new file mode 100755 index 000000000..f639fa8ae --- /dev/null +++ b/.superset/lib/lock.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Portable (macOS + Linux) advisory locking via atomic mkdir. Superset does +# not serialize setup/run/teardown across workspaces of the same repo, so +# scripts that touch shared state (a port registry, the local Supabase +# stack) take one of these locks first. +# +# Usage: +# source lib/lock.sh +# lock_acquire "$HOME/.superset/some.lock" 30 +# trap 'lock_release "$HOME/.superset/some.lock"' EXIT + +lock_acquire() { + local lock_dir="$1" + local timeout="${2:-30}" + local waited=0 + + mkdir -p "$(dirname "$lock_dir")" + + while ! mkdir "$lock_dir" 2>/dev/null; do + # Reclaim a lock left behind by a process that no longer exists. + if [ -f "$lock_dir/pid" ]; then + local owner_pid + owner_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)" + if [ -n "$owner_pid" ] && ! kill -0 "$owner_pid" 2>/dev/null; then + rm -rf "$lock_dir" + continue + fi + fi + + if [ "$waited" -ge "$timeout" ]; then + echo "lock: timed out after ${timeout}s waiting for $lock_dir" >&2 + return 1 + fi + sleep 1 + waited=$((waited + 1)) + done + + echo $$ > "$lock_dir/pid" +} + +lock_release() { + local lock_dir="$1" + rm -rf "$lock_dir" +} diff --git a/.superset/lib/port-alloc.cjs b/.superset/lib/port-alloc.cjs new file mode 100755 index 000000000..412cd2e58 --- /dev/null +++ b/.superset/lib/port-alloc.cjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +// Shared per-workspace port allocator for parallel Superset workspaces of +// this repo. Superset itself only observes listening ports, it does not +// assign them (docs.superset.sh/ports), so this implements the +// shared-allocation-file pattern the docs point to. Callers are expected to +// hold lib/lock.sh's lock around invocations to avoid a read/write race +// between two workspaces allocating at once. +// +// Usage: +// port-alloc.cjs allocate +// port-alloc.cjs release +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const file = path.join(os.homedir(), '.superset', 'debatelab-ports', 'allocations.json'); + +function readData() { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')); + } catch { + return {}; + } +} + +function writeData(data) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, JSON.stringify(data, null, 2)); +} + +const [, , cmd, workspace, start, end] = process.argv; + +if (!cmd || !workspace) { + console.error('usage: port-alloc.cjs allocate|release [rangeStart] [rangeEnd]'); + process.exit(1); +} + +if (cmd === 'allocate') { + const data = readData(); + if (data[workspace]) { + console.log(data[workspace]); + process.exit(0); + } + const used = new Set(Object.values(data)); + for (let p = Number(start); p <= Number(end); p++) { + if (!used.has(p)) { + data[workspace] = p; + writeData(data); + console.log(p); + process.exit(0); + } + } + console.error(`port-alloc: no free port in ${start}-${end}`); + process.exit(1); +} else if (cmd === 'release') { + const data = readData(); + delete data[workspace]; + writeData(data); +} else { + console.error(`port-alloc: unknown command "${cmd}"`); + process.exit(1); +} diff --git a/.superset/teardown.sh b/.superset/teardown.sh new file mode 100755 index 000000000..306fa212c --- /dev/null +++ b/.superset/teardown.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Superset "teardown" script: releases this workspace's port allocation and, +# if this workspace has a local Supabase stack running, stops it so it +# doesn't hold ports/containers after the worktree is deleted. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DIR/lib/lock.sh" + +WORKSPACE_KEY="$(pwd)" +PORTS_LOCK="$HOME/.superset/debatelab-ports/lock" + +lock_acquire "$PORTS_LOCK" 30 +node "$DIR/lib/port-alloc.cjs" release "$WORKSPACE_KEY" || true +lock_release "$PORTS_LOCK" + +if command -v supabase >/dev/null 2>&1 && supabase status >/dev/null 2>&1; then + supabase stop --no-backup || true +fi diff --git a/AGENTS.md b/AGENTS.md index 296bd609a..cb7cabfb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,12 @@ - The development server normally uses port 8080. Automated browser verification owns `127.0.0.1:18080` and never reuses an existing server. - Local Supabase development uses port 54321. Do not point staging checks at the production project. +## Parallel Superset workspaces + +Each Superset workspace is a separate git worktree. `.superset/config.json` runs `npm ci` on create and `./.superset/dev.sh` for the Run button; `dev.sh` allocates a free port for this workspace from a shared `~/.superset/debatelab-ports/allocations.json` file (range 8080-8179) so several workspaces' dev servers never collide on the hardcoded `:8080` in `vite.config.ts`, and `./.superset/teardown.sh` releases that allocation on delete. + +The local Supabase stack is not parallel-safe: `supabase/config.toml` sets no `project_id` or custom port block, so `supabase db start` always binds the CLI's default ports and container names. Run the DB gate through `./.superset/db-gate.sh` rather than calling `supabase db start` / `npm run test:db` directly whenever more than one workspace might be active - it takes an exclusive lock (`~/.superset/debatelab-db.lock`, up to 10 minutes) before starting the stack and stops it afterward. + ## Verification ladder Run the smallest relevant reproduction first, then the complete gate before a PR: diff --git a/vite.config.ts b/vite.config.ts index 64bf37a9b..c4aa9d1a1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig(({ mode }) => ({ base: mode === 'capacitor' ? './' : '/', server: { host: "::", - port: 8080, + port: Number(process.env.PORT) || 8080, hmr: { overlay: false, },