Skip to content

Commit 410ff27

Browse files
Programcodeclaude
andcommitted
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163LcrphWJjYjTXM33mQuH6
1 parent 89529bf commit 410ff27

9 files changed

Lines changed: 184 additions & 1 deletion

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ supabase/.env.*
5050
# Local lock generated by Deno checks
5151
deno.lock
5252

53+
# Personal Superset workspace overrides
54+
.superset/config.local.json
55+
5356
# Local agent/task files
5457
.claude/
5558
.codex/

‎.superset/config.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"setup": ["npm ci"],
3+
"run": ["./.superset/dev.sh"],
4+
"teardown": ["./.superset/teardown.sh"]
5+
}

‎.superset/db-gate.sh‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
# Runs the Docker-backed Supabase migration/pgTAP gate
3+
# (`supabase db start` + `npm run test:db`) with exclusive access across
4+
# Superset workspaces of this repo. `supabase/config.toml` sets no
5+
# project_id or custom port block, so `supabase db start` always binds the
6+
# CLI's default ports and container names - two workspaces running it at
7+
# once collide in Docker, not just on a socket. Use this instead of calling
8+
# `supabase db start` directly whenever more than one workspace might be
9+
# active.
10+
set -euo pipefail
11+
12+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13+
source "$DIR/lib/lock.sh"
14+
15+
DB_LOCK="$HOME/.superset/debatelab-db.lock"
16+
17+
echo "Waiting for exclusive access to the local Supabase stack..."
18+
lock_acquire "$DB_LOCK" 600
19+
trap 'lock_release "$DB_LOCK"' EXIT
20+
21+
supabase db start
22+
npm run test:db
23+
supabase stop --no-backup

‎.superset/dev.sh‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# Superset "run" script: starts the Vite dev server on a port unique to this
3+
# workspace so several DebateLab workspaces can run `npm run dev` at once
4+
# without fighting over :8080 (vite.config.ts hardcodes that port otherwise).
5+
set -euo pipefail
6+
7+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
source "$DIR/lib/lock.sh"
9+
10+
WORKSPACE_KEY="$(pwd)"
11+
PORTS_LOCK="$HOME/.superset/debatelab-ports/lock"
12+
13+
lock_acquire "$PORTS_LOCK" 30
14+
PORT="$(node "$DIR/lib/port-alloc.cjs" allocate "$WORKSPACE_KEY" 8080 8179)"
15+
lock_release "$PORTS_LOCK"
16+
17+
echo "Superset workspace dev server: http://127.0.0.1:${PORT}"
18+
export PORT
19+
exec npm run dev -- --host 127.0.0.1 --port "$PORT"

‎.superset/lib/lock.sh‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
# Portable (macOS + Linux) advisory locking via atomic mkdir. Superset does
3+
# not serialize setup/run/teardown across workspaces of the same repo, so
4+
# scripts that touch shared state (a port registry, the local Supabase
5+
# stack) take one of these locks first.
6+
#
7+
# Usage:
8+
# source lib/lock.sh
9+
# lock_acquire "$HOME/.superset/some.lock" 30
10+
# trap 'lock_release "$HOME/.superset/some.lock"' EXIT
11+
12+
lock_acquire() {
13+
local lock_dir="$1"
14+
local timeout="${2:-30}"
15+
local waited=0
16+
17+
mkdir -p "$(dirname "$lock_dir")"
18+
19+
while ! mkdir "$lock_dir" 2>/dev/null; do
20+
# Reclaim a lock left behind by a process that no longer exists.
21+
if [ -f "$lock_dir/pid" ]; then
22+
local owner_pid
23+
owner_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
24+
if [ -n "$owner_pid" ] && ! kill -0 "$owner_pid" 2>/dev/null; then
25+
rm -rf "$lock_dir"
26+
continue
27+
fi
28+
fi
29+
30+
if [ "$waited" -ge "$timeout" ]; then
31+
echo "lock: timed out after ${timeout}s waiting for $lock_dir" >&2
32+
return 1
33+
fi
34+
sleep 1
35+
waited=$((waited + 1))
36+
done
37+
38+
echo $$ > "$lock_dir/pid"
39+
}
40+
41+
lock_release() {
42+
local lock_dir="$1"
43+
rm -rf "$lock_dir"
44+
}

‎.superset/lib/port-alloc.cjs‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env node
2+
// Shared per-workspace port allocator for parallel Superset workspaces of
3+
// this repo. Superset itself only observes listening ports, it does not
4+
// assign them (docs.superset.sh/ports), so this implements the
5+
// shared-allocation-file pattern the docs point to. Callers are expected to
6+
// hold lib/lock.sh's lock around invocations to avoid a read/write race
7+
// between two workspaces allocating at once.
8+
//
9+
// Usage:
10+
// port-alloc.cjs allocate <workspaceKey> <rangeStart> <rangeEnd>
11+
// port-alloc.cjs release <workspaceKey>
12+
'use strict';
13+
14+
const fs = require('fs');
15+
const os = require('os');
16+
const path = require('path');
17+
18+
const file = path.join(os.homedir(), '.superset', 'debatelab-ports', 'allocations.json');
19+
20+
function readData() {
21+
try {
22+
return JSON.parse(fs.readFileSync(file, 'utf8'));
23+
} catch {
24+
return {};
25+
}
26+
}
27+
28+
function writeData(data) {
29+
fs.mkdirSync(path.dirname(file), { recursive: true });
30+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
31+
}
32+
33+
const [, , cmd, workspace, start, end] = process.argv;
34+
35+
if (!cmd || !workspace) {
36+
console.error('usage: port-alloc.cjs allocate|release <workspaceKey> [rangeStart] [rangeEnd]');
37+
process.exit(1);
38+
}
39+
40+
if (cmd === 'allocate') {
41+
const data = readData();
42+
if (data[workspace]) {
43+
console.log(data[workspace]);
44+
process.exit(0);
45+
}
46+
const used = new Set(Object.values(data));
47+
for (let p = Number(start); p <= Number(end); p++) {
48+
if (!used.has(p)) {
49+
data[workspace] = p;
50+
writeData(data);
51+
console.log(p);
52+
process.exit(0);
53+
}
54+
}
55+
console.error(`port-alloc: no free port in ${start}-${end}`);
56+
process.exit(1);
57+
} else if (cmd === 'release') {
58+
const data = readData();
59+
delete data[workspace];
60+
writeData(data);
61+
} else {
62+
console.error(`port-alloc: unknown command "${cmd}"`);
63+
process.exit(1);
64+
}

‎.superset/teardown.sh‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# Superset "teardown" script: releases this workspace's port allocation and,
3+
# if this workspace has a local Supabase stack running, stops it so it
4+
# doesn't hold ports/containers after the worktree is deleted.
5+
set -euo pipefail
6+
7+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
source "$DIR/lib/lock.sh"
9+
10+
WORKSPACE_KEY="$(pwd)"
11+
PORTS_LOCK="$HOME/.superset/debatelab-ports/lock"
12+
13+
lock_acquire "$PORTS_LOCK" 30
14+
node "$DIR/lib/port-alloc.cjs" release "$WORKSPACE_KEY" || true
15+
lock_release "$PORTS_LOCK"
16+
17+
if command -v supabase >/dev/null 2>&1 && supabase status >/dev/null 2>&1; then
18+
supabase stop --no-backup || true
19+
fi

‎AGENTS.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
- The development server normally uses port 8080. Automated browser verification owns `127.0.0.1:18080` and never reuses an existing server.
88
- Local Supabase development uses port 54321. Do not point staging checks at the production project.
99

10+
## Parallel Superset workspaces
11+
12+
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.
13+
14+
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.
15+
1016
## Verification ladder
1117

1218
Run the smallest relevant reproduction first, then the complete gate before a PR:

‎vite.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default defineConfig(({ mode }) => ({
77
base: mode === 'capacitor' ? './' : '/',
88
server: {
99
host: "::",
10-
port: 8080,
10+
port: Number(process.env.PORT) || 8080,
1111
hmr: {
1212
overlay: false,
1313
},

0 commit comments

Comments
 (0)