Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
5 changes: 5 additions & 0 deletions .superset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"setup": ["npm ci"],
"run": ["./.superset/dev.sh"],
"teardown": ["./.superset/teardown.sh"]
}
23 changes: 23 additions & 0 deletions .superset/db-gate.sh
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions .superset/dev.sh
Original file line number Diff line number Diff line change
@@ -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"
44 changes: 44 additions & 0 deletions .superset/lib/lock.sh
Original file line number Diff line number Diff line change
@@ -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"
}
64 changes: 64 additions & 0 deletions .superset/lib/port-alloc.cjs
Original file line number Diff line number Diff line change
@@ -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 <workspaceKey> <rangeStart> <rangeEnd>
// port-alloc.cjs release <workspaceKey>
'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 <workspaceKey> [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);
}
19 changes: 19 additions & 0 deletions .superset/teardown.sh
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default defineConfig(({ mode }) => ({
base: mode === 'capacitor' ? './' : '/',
server: {
host: "::",
port: 8080,
port: Number(process.env.PORT) || 8080,
hmr: {
overlay: false,
},
Expand Down
Loading