Skip to content

refactor(desktop): externalize port allocation to setup/teardown scripts#1494

Merged
Kitenite merged 6 commits into
mainfrom
kitenite/allocate-port-base
Feb 14, 2026
Merged

refactor(desktop): externalize port allocation to setup/teardown scripts#1494
Kitenite merged 6 commits into
mainfrom
kitenite/allocate-port-base

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Feb 14, 2026

Summary

  • Move port base allocation out of the desktop app's local SQLite DB and into a shared file (~/.superset/port-allocations.json) managed by the setup and teardown scripts
  • This decouples port allocation from the desktop app, allowing it to work independently (e.g., CLI-only workflows)
  • The portBase column in the local DB schema is left in place but is no longer written to or read from

Changes

Setup script (.superset/setup.sh)

  • Add allocate_port_base() function that reads/writes ~/.superset/port-allocations.json, keyed by $PWD, reusing existing allocations or finding the first available slot (start=3000, range=20)
  • Call allocate_port_base at the beginning of step_write_env() so SUPERSET_PORT_BASE is always set
  • Remove if [ -n "${SUPERSET_PORT_BASE:-}" ] guards around port env vars and ports.json generation

Teardown script (.superset/teardown.sh)

  • Add step_deallocate_port() that removes the $PWD entry from the allocations file
  • Wire into main() as a new step after Neon branch deletion

Desktop app — remove all portBase plumbing

  • db-helpers.ts: Delete allocatePortBase(), START_PORT, PORT_RANGE constants, unused asc import
  • create.ts: Remove allocatePortBase import and portBase from 3 workspace insert calls
  • env.ts: Remove portBase param from buildTerminalEnv() and SUPERSET_PORT_BASE injection
  • types.ts: Remove portBase from CreateSessionParams
  • daemon-manager.ts: Remove portBase from destructuring and buildTerminalEnv() call
  • session.ts: Remove portBase from destructuring and buildTerminalEnv() call
  • terminal.ts: Remove portBase: workspace?.portBase from createOrAttach() call

Test Plan

  • bun run typecheck passes
  • bun run lint:fix passes
  • Run .superset/setup.sh in a worktree — should create ~/.superset/port-allocations.json with an entry
  • Run .superset/teardown.sh — should remove the entry
  • Verify desktop app terminal sessions still work without SUPERSET_PORT_BASE env var from desktop

Summary by CodeRabbit

  • New Features

    • Setup/teardown are modularized with clearer step workflows, safer port allocation locking, improved env/ports output, and run summaries.
  • Bug Fixes

    • Ports mapping and environment outputs are consistently generated for reliability.
  • Chores

    • Desktop no longer propagates or consumes per-workspace port base; workspace port handling centralized.
  • Documentation

    • Removed the Multi-Worktree Development Plan.

Move port base allocation from the desktop app's local SQLite DB to a
shared file (~/.superset/port-allocations.json) managed by setup and
teardown scripts, making port allocation work independently of the
desktop app.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Modularized the setup/teardown tooling into sourced libraries and step scripts under .superset/lib; moved workspace port allocation and ports.json/env emission into the shell-based setup steps; removed desktop-side port allocation/portBase plumbing and deleted the Multi-Worktree plan doc.

Changes

Cohort / File(s) Summary
Top-level bootstraps
.superset/setup.sh, .superset/teardown.sh
Replaced monolithic scripts with small bootstraps that source .superset/lib/* modules and call setup_main / teardown_main.
Shared shell library
.superset/lib/common.sh
New shared helpers: colorized messaging, step tracking, env escaping/writing, JSON validation, port-allocation lock helpers, and summary printing.
Setup flow (args, steps, main)
.superset/lib/setup/args.sh, .superset/lib/setup/steps.sh, .superset/lib/setup/main.sh
New setup argument parsing and a sequence of step functions (env load, deps check/install, Neon branch setup, allocate_port_base (file-backed + lock), start Electric, write workspace .env & ports.json, seed DB) orchestrated by setup_main.
Teardown flow (args, steps, main)
.superset/lib/teardown/args.sh, .superset/lib/teardown/steps.sh, .superset/lib/teardown/main.sh
New teardown argument parsing and step functions (env load, deps check, kill terminal daemons, stop Electric, delete Neon branch, deallocate port mapping, optional dev-data removal) orchestrated by teardown_main.
Desktop port allocation removal
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts, apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
Removed in-process port allocation: deleted allocatePortBase, START_PORT/PORT_RANGE, and removed portBase computation/insertion in workspace creation flows.
Desktop terminal/session plumbing
apps/desktop/src/main/lib/terminal/types.ts, apps/desktop/src/main/lib/terminal/env.ts, apps/desktop/src/main/lib/terminal/session.ts, apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts, apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Dropped portBase from CreateSessionParams and removed portBase from build/create/attach flows; buildTerminalEnv no longer sets SUPERSET_PORT_BASE.
Docs / misc
apps/desktop/docs/MULTI_WORKTREE_PLAN.md, .gitignore
Deleted the Multi-Worktree plan doc; simplified .gitignore to explicitly track .superset/ports.json and treat other .superset files as generated.

Sequence Diagram(s)

sequenceDiagram
  participant Setup as ".superset/lib/setup/main.sh"
  participant AllocFile as "ports.json / allocations file"
  participant Desktop as "Desktop App"
  participant Terminal as "Terminal Service"
  participant Teardown as ".superset/lib/teardown/main.sh"

  Setup->>AllocFile: acquire lock, compute/persist BASE mapping for cwd
  Setup->>Setup: export SUPERSET_* vars, write workspace .env and ports.json
  Desktop->>AllocFile: read ports.json / env at startup
  Desktop->>Terminal: request createOrAttach (no portBase arg)
  Terminal->>Desktop: session uses env-derived port mapping
  Teardown->>AllocFile: acquire lock, remove mapping for cwd
  Teardown->>Teardown: report deallocation outcome
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I tunneled locks and wrote a JSON bed,
Setup plants bases where the ports are led,
Desktop dropped its map, the env hums neat,
Teardown lifts the peg when devs retreat,
Rabbit hops—ports tidy, workflow sweet.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (22 files):

⚔️ .gitignore (content)
⚔️ .superset/setup.sh (content)
⚔️ .superset/teardown.sh (content)
⚔️ apps/desktop/package.json (content)
⚔️ apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts (content)
⚔️ apps/desktop/src/main/index.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/env.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/session.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/types.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceDiffStats.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/constants.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/hooks/index.ts (content)
⚔️ packages/local-db/src/schema/zod.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: externalizing port allocation from the desktop app to setup/teardown scripts.
Description check ✅ Passed The PR description comprehensively covers all required sections: summary of changes, detailed breakdown by file, and a test plan with clear verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kitenite/allocate-port-base
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch kitenite/allocate-port-base
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.superset/setup.sh (1)

500-510: ⚠️ Potential issue | 🟡 Minor

Fallback port values are inconsistent with the allocation scheme.

Lines 502-503 use ${API_PORT:-3041} and ${ELECTRIC_PORT:-3049} as fallbacks, but with BASE=3000 the offsets would yield 3001 and 3009. These fallbacks would only be reached if the local variables from the block above somehow aren't set (shouldn't happen in practice), but the magic numbers are misleading if they ever do apply.

🤖 Fix all issues with AI agents
In @.superset/setup.sh:
- Around line 395-396: The startup sequence currently calls step_start_electric
before step_write_env so allocate_port_base() runs too late causing
SUPERSET_PORT_BASE to be unset when Electric boots; fix by moving the
allocate_port_base() call (or the entire step_write_env execution) earlier in
main() so allocate_port_base() runs before step_start_electric (or reorder the
steps so step_write_env precedes step_start_electric), ensuring
SUPERSET_PORT_BASE is set before Electric starts and ELECTRIC_PORT written to
.env matches the actual container port.
- Around line 350-385: The allocate_port_base function is vulnerable to a race
on port-allocations.json; wrap the entire read-modify-write sequence in an
exclusive lock (e.g., use flock) so two concurrent invocations cannot pick the
same candidate; specifically, acquire a lock (on a dedicated lockfile) before
reading with jq, computing used/candidate, and writing the temp file+mv, then
release the lock; ensure you do not run the locked work in a subshell so
exported SUPERSET_PORT_BASE from allocate_port_base propagates (use a grouped
command { ... } with fd-based flock or perform the work inside the lock and
write the chosen port to a temp file/variable that you then export in the
parent).

In @.superset/teardown.sh:
- Around line 245-271: step_deallocate_port relies on jq but
step_check_dependencies doesn't verify jq; add jq to the dependency list checked
by step_check_dependencies OR add an explicit pre-check inside
step_deallocate_port (use command -v jq >/dev/null 2>&1) and if missing call
warn and step_skipped (or return non-zero) so the function fails loudly instead
of silently on the jq call; reference the existing helper names
step_deallocate_port, step_check_dependencies, warn, and step_skipped when
making the change.

Comment thread .superset/setup.sh Outdated
Comment thread .superset/teardown.sh Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 14, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Fly.io Streams (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.superset/setup.sh:
- Around line 345-415: The mkdir-based locking in allocate_port_base can leave a
stale lock dir if the process dies; update allocate_port_base (and
step_deallocate_port) to detect and recover stale locks by writing the owner PID
into the lock dir when created, and before waiting check if the lock dir exists
and contains a PID whose process no longer exists (kill -0 PID) and if so remove
the stale lock; additionally set a trap in allocate_port_base to remove the lock
dir on EXIT/ERR/INT/TERM only if this process created it (use a local flag like
created_lock=1) so normal cleanup happens even on termination; alternatively you
may implement flock fallback by attempting flock on a temp lockfile before using
the mkdir+PID approach.
- Line 469: The script crashes under set -u because line using bare
$SUPERSET_PORT_BASE assumes allocate_port_base exported it; change that usage to
a safe expansion or fail early: replace the bare $SUPERSET_PORT_BASE reference
with ${SUPERSET_PORT_BASE:-} (matching step_start_electric) or, alternatively,
check the result of allocate_port_base (and exit from main with an error) so
that SUPERSET_PORT_BASE is guaranteed before continuing; reference
allocate_port_base, SUPERSET_PORT_BASE, step_start_electric and the set -u
option when making the change.
🧹 Nitpick comments (2)
.superset/setup.sh (2)

481-481: Local ELECTRIC_PORT shadows the exported variable from step_start_electric.

Line 481 declares local ELECTRIC_PORT=$((BASE + 9)), which shadows the ELECTRIC_PORT exported on line 339. While the computed values match today, this coupling is implicit and fragile — if either offset or the export changes independently, the .env and the running container will silently disagree.

Consider reusing the already-exported ELECTRIC_PORT instead of recomputing it, or at minimum adding a comment noting the intentional duplication.


529-536: Fallback defaults on lines 529–530 may diverge from the computed ports.

api_port falls back to 3041 and electric_port to 3049 (hardcoded), but the local declarations on lines 473 and 481 derive these from BASE. If step_write_env somehow reaches this point with empty locals (e.g., after the set -u fix adds an early guard), the .dev.vars file would use stale defaults inconsistent with the allocated base.

These fallbacks should ideally also be derived from SUPERSET_PORT_BASE, or be removed if the early-exit guard makes them unreachable.

Comment thread .superset/setup.sh Outdated
Comment on lines +345 to +415
allocate_port_base() {
local alloc_file="$HOME/.superset/port-allocations.json"
local lock_dir="$HOME/.superset/port-allocations.lock"
local start=3000
local range=20

# Ensure directory and file exist
mkdir -p "$HOME/.superset"
if [ ! -f "$alloc_file" ]; then
echo '{}' > "$alloc_file"
fi

# Acquire lock (mkdir is atomic across processes)
local waited=0
local timeout=30
while ! mkdir "$lock_dir" 2>/dev/null; do
if [ "$waited" -ge "$timeout" ]; then
error "Timed out waiting for port allocation lock: $lock_dir"
return 1
fi
sleep 1
waited=$((waited + 1))
done

local key="$PWD"
local existing
if ! existing=$(jq -r --arg k "$key" '.[$k] // empty' "$alloc_file" 2>/dev/null); then
error "Failed to read port allocations: $alloc_file"
rmdir "$lock_dir" 2>/dev/null || true
return 1
fi

if [ -n "$existing" ]; then
export SUPERSET_PORT_BASE="$existing"
rmdir "$lock_dir" 2>/dev/null || true
return 0
fi

# Collect used port bases
local used
if ! used=$(jq -r '[.[]] | sort | .[]' "$alloc_file" 2>/dev/null); then
error "Failed to parse used port allocations: $alloc_file"
rmdir "$lock_dir" 2>/dev/null || true
return 1
fi

# Find first available slot
local candidate=$start
while echo "$used" | grep -qx "$candidate" 2>/dev/null; do
candidate=$((candidate + range))
done

# Write allocation
local tmp_file="${alloc_file}.tmp.$$"
if ! jq --arg k "$key" --argjson v "$candidate" '. + {($k): $v}' "$alloc_file" > "$tmp_file"; then
error "Failed to write updated port allocations"
rm -f "$tmp_file"
rmdir "$lock_dir" 2>/dev/null || true
return 1
fi
if ! mv "$tmp_file" "$alloc_file"; then
error "Failed to persist port allocations"
rm -f "$tmp_file"
rmdir "$lock_dir" 2>/dev/null || true
return 1
fi

export SUPERSET_PORT_BASE="$candidate"
rmdir "$lock_dir" 2>/dev/null || true
return 0
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stale lock directory can permanently block port allocation.

The mkdir-based lock has no stale-lock detection. If setup.sh is killed (SIGKILL, OOM, power loss, etc.) between acquiring the lock (line 360) and releasing it (line 413), the lock directory persists indefinitely. All subsequent allocate_port_base and step_deallocate_port calls will time out after 30 seconds and fail.

Consider one of:

  1. Store the owning PID inside the lock dir and treat it as stale if that PID no longer exists.
  2. Add a trap to clean up the lock on ERR/EXIT/INT/TERM (with a guard to only remove if this process created it).
  3. Fall back to flock if available, with mkdir as a secondary.
Option 1 — PID-based stale detection
   # Acquire lock (mkdir is atomic across processes)
   local waited=0
   local timeout=30
   while ! mkdir "$lock_dir" 2>/dev/null; do
+    # Check for stale lock
+    if [ -f "$lock_dir/pid" ]; then
+      local lock_pid
+      lock_pid=$(cat "$lock_dir/pid" 2>/dev/null || echo "")
+      if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then
+        warn "Removing stale port allocation lock (PID $lock_pid no longer running)"
+        rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir"
+        continue
+      fi
+    fi
     if [ "$waited" -ge "$timeout" ]; then
       error "Timed out waiting for port allocation lock: $lock_dir"
       return 1
     fi
     sleep 1
     waited=$((waited + 1))
   done
+  echo $$ > "$lock_dir/pid"
🤖 Prompt for AI Agents
In @.superset/setup.sh around lines 345 - 415, The mkdir-based locking in
allocate_port_base can leave a stale lock dir if the process dies; update
allocate_port_base (and step_deallocate_port) to detect and recover stale locks
by writing the owner PID into the lock dir when created, and before waiting
check if the lock dir exists and contains a PID whose process no longer exists
(kill -0 PID) and if so remove the stale lock; additionally set a trap in
allocate_port_base to remove the lock dir on EXIT/ERR/INT/TERM only if this
process created it (use a local flag like created_lock=1) so normal cleanup
happens even on termination; alternatively you may implement flock fallback by
attempting flock on a temp lockfile before using the mkdir+PID approach.

Comment thread .superset/setup.sh Outdated
@Kitenite Kitenite merged commit 9f10308 into main Feb 14, 2026
13 of 14 checks passed
@Kitenite Kitenite deleted the kitenite/allocate-port-base branch February 14, 2026 10:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.superset/lib/setup/steps.sh:
- Line 113: The jq filter interpolates $WORKSPACE_NAME directly into the query
when computing EXISTING_BRANCH, which is unsafe; change the jq invocation in the
EXISTING_BRANCH assignment to pass WORKSPACE_NAME via jq's --arg (e.g., use
--arg workspace "$WORKSPACE_NAME" and reference $workspace inside the filter) so
special characters are escaped and injections prevented, and keep the existing
stderr redirection and .id extraction logic intact.
- Around line 349-370: The port-assignment block in step_write_env assumes
SUPERSET_PORT_BASE is set and will cause an unbound variable error if
allocate_port_base failed; update the start of the port section to guard or fail
fast: check presence of SUPERSET_PORT_BASE (reference symbols: step_write_env,
allocate_port_base, SUPERSET_PORT_BASE, setup_main) and either abort with a
clear error/log and non-zero exit when it is unset, or provide a safe default
before computing WEB_PORT/API_PORT/... so the rest of the function won't hit set
-u; ensure this guard runs before any use of BASE or the local WEB_PORT,
API_PORT, etc.

In @.superset/lib/teardown/steps.sh:
- Around line 93-106: The SIGKILL loop runs immediately after sending SIGTERM,
giving processes no time to exit; add a short configurable grace period (e.g.,
sleep for a few seconds) between the SIGTERM loop and the SIGKILL loop so
processes matching matched_pids can handle SIGTERM and exit before escalation.
Update the teardown logic around the kill_process_tree calls and the counters
(root_killed, force_killed) to sleep (use a variable like TERM_GRACE_SECONDS)
after the first loop and before the second loop so the delay can be tuned.
🧹 Nitpick comments (5)
.gitignore (1)

53-54: Clarify .superset ignore intent; current rule only ignores ports.json.

If more generated artifacts exist under .superset, consider ignoring the folder broadly and explicitly un-ignoring the tracked scripts/config (or remove this rule if ports.json should be committed). This avoids accidental commits as .superset grows.

✅ Example pattern (if you want to ignore all generated artifacts but keep scripts/config)
 # Superset (track scripts/config; ignore generated workspace artifacts)
-.superset/ports.json
+.superset/*
+!.superset/setup.sh
+!.superset/teardown.sh
+!.superset/lib/
+!.superset/ports.json
.superset/lib/common.sh (1)

115-144: print_summary relies on caller-initialized arrays.

FAILED_STEPS and SKIPPED_STEPS must be initialized as arrays before print_summary is called — under set -u (which teardown.sh enables), referencing an unset array will error. Both setup_main and teardown_main do initialize these, so this works correctly today. A defensive declare -a or ${FAILED_STEPS[@]+"${FAILED_STEPS[@]}"} guard would make this more robust against future misuse, but it's not blocking.

.superset/lib/teardown/steps.sh (1)

128-128: WORKSPACE_NAME is set as a global variable, potentially leaking into the caller's scope.

Line 128 assigns WORKSPACE_NAME without local, so it modifies/creates a global shell variable. Same issue on line 172 with WORKSPACE_NAME, line 158 with NEON_PROJECT_ID, line 165 with BRANCH_ID, and line 136 with ELECTRIC_CONTAINER. Since these are sourced functions, the variables persist across steps. This appears intentional for cross-step usage (e.g., WORKSPACE_NAME reused in later steps), but worth confirming it's deliberate rather than accidental.

.superset/lib/setup/steps.sh (1)

199-211: Unquoted $port_flag relies on word splitting.

Line 205 intentionally leaves $port_flag unquoted so it splits into -p and 3009:3000 as separate arguments. This works but is fragile — ShellCheck would flag it (SC2086), and it can break if the value ever contains spaces in unexpected places.

Proposed: use an array for docker args
-  local port_flag
   ELECTRIC_PORT=$((SUPERSET_PORT_BASE + 9))
-  port_flag="-p $ELECTRIC_PORT:3000"
+  local -a port_args=(-p "$ELECTRIC_PORT:3000")

   if ! docker run -d \
       --name "$ELECTRIC_CONTAINER" \
-      $port_flag \
+      "${port_args[@]}" \
       -e DATABASE_URL="$DIRECT_URL" \
.superset/lib/setup/main.sh (1)

19-57: Steps continue after critical failures, causing cascading errors.

The orchestration runs all steps regardless of earlier failures. If step_load_env fails (e.g., SUPERSET_ROOT_PATH not set), every subsequent step that depends on environment variables will also fail, producing a noisy summary. Consider short-circuiting after critical prerequisite steps (env loading, dependency check) to provide clearer diagnostics.

Example: early exit on prerequisite failures
   # Step 1: Load environment
   if ! step_load_env; then
     step_failed "Load environment variables"
+    print_summary "Setup"
+    return 1
   fi

   # Step 2: Check dependencies
   if ! step_check_dependencies; then
     step_failed "Check dependencies"
+    print_summary "Setup"
+    return 1
   fi

fi

# Now safe to parse with jq - use // empty for fallback
EXISTING_BRANCH=$(echo "$branches_output" | jq -r ".[] | select(.name == \"$WORKSPACE_NAME\") | .id // empty" 2>/dev/null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

jq filter uses shell string interpolation — vulnerable to injection and special characters.

$WORKSPACE_NAME is interpolated directly into the jq filter string. If the workspace name contains characters like ", \, or other jq-special characters, this will break the filter or produce incorrect results. Use --arg for safe parameterization.

Proposed fix
-  EXISTING_BRANCH=$(echo "$branches_output" | jq -r ".[] | select(.name == \"$WORKSPACE_NAME\") | .id // empty" 2>/dev/null)
+  EXISTING_BRANCH=$(echo "$branches_output" | jq -r --arg name "$WORKSPACE_NAME" '.[] | select(.name == $name) | .id // empty' 2>/dev/null)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
EXISTING_BRANCH=$(echo "$branches_output" | jq -r ".[] | select(.name == \"$WORKSPACE_NAME\") | .id // empty" 2>/dev/null)
EXISTING_BRANCH=$(echo "$branches_output" | jq -r --arg name "$WORKSPACE_NAME" '.[] | select(.name == $name) | .id // empty' 2>/dev/null)
🤖 Prompt for AI Agents
In @.superset/lib/setup/steps.sh at line 113, The jq filter interpolates
$WORKSPACE_NAME directly into the query when computing EXISTING_BRANCH, which is
unsafe; change the jq invocation in the EXISTING_BRANCH assignment to pass
WORKSPACE_NAME via jq's --arg (e.g., use --arg workspace "$WORKSPACE_NAME" and
reference $workspace inside the filter) so special characters are escaped and
injections prevented, and keep the existing stderr redirection and .id
extraction logic intact.

Comment on lines +349 to +370
# Port allocation for multi-worktree dev instances
# Each workspace gets a range of 20 ports from its base.
# Offsets: +0 web, +1 api, +2 marketing, +3 admin, +4 docs,
# +5 desktop vite, +6 notifications, +7 streams, +8 streams internal, +9 electric,
# +10 caddy (HTTP/2 reverse proxy for electric-proxy), +11 code inspector, +12 electric-proxy (wrangler dev)
local BASE=$SUPERSET_PORT_BASE

# App ports (fixed offsets from base)
local WEB_PORT=$((BASE))
local API_PORT=$((BASE + 1))
local MARKETING_PORT=$((BASE + 2))
local ADMIN_PORT=$((BASE + 3))
local DOCS_PORT=$((BASE + 4))
local DESKTOP_VITE_PORT=$((BASE + 5))
local DESKTOP_NOTIFICATIONS_PORT=$((BASE + 6))
local STREAMS_PORT=$((BASE + 7))
local STREAMS_INTERNAL_PORT=$((BASE + 8))
local ELECTRIC_PORT=$((BASE + 9))
local CADDY_ELECTRIC_PORT=$((BASE + 10))
local CODE_INSPECTOR_PORT=$((BASE + 11))
local ELECTRIC_PROXY_PORT=$((BASE + 12))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

step_write_env will hit an unbound variable error if allocate_port_base failed.

setup_main does not short-circuit on step failure — it records failures and continues. If allocate_port_base fails, SUPERSET_PORT_BASE will be unset. Line 354 (local BASE=$SUPERSET_PORT_BASE) will trigger an unbound variable error under set -u (set in setup.sh). Consider adding a guard or making step_write_env depend explicitly on the port base being available.

Proposed fix: guard at the top of the port section
+  if [ -z "${SUPERSET_PORT_BASE:-}" ]; then
+    error "SUPERSET_PORT_BASE not set (port allocation may have failed)"
+    return 1
+  fi
+
   local BASE=$SUPERSET_PORT_BASE
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Port allocation for multi-worktree dev instances
# Each workspace gets a range of 20 ports from its base.
# Offsets: +0 web, +1 api, +2 marketing, +3 admin, +4 docs,
# +5 desktop vite, +6 notifications, +7 streams, +8 streams internal, +9 electric,
# +10 caddy (HTTP/2 reverse proxy for electric-proxy), +11 code inspector, +12 electric-proxy (wrangler dev)
local BASE=$SUPERSET_PORT_BASE
# App ports (fixed offsets from base)
local WEB_PORT=$((BASE))
local API_PORT=$((BASE + 1))
local MARKETING_PORT=$((BASE + 2))
local ADMIN_PORT=$((BASE + 3))
local DOCS_PORT=$((BASE + 4))
local DESKTOP_VITE_PORT=$((BASE + 5))
local DESKTOP_NOTIFICATIONS_PORT=$((BASE + 6))
local STREAMS_PORT=$((BASE + 7))
local STREAMS_INTERNAL_PORT=$((BASE + 8))
local ELECTRIC_PORT=$((BASE + 9))
local CADDY_ELECTRIC_PORT=$((BASE + 10))
local CODE_INSPECTOR_PORT=$((BASE + 11))
local ELECTRIC_PROXY_PORT=$((BASE + 12))
# Port allocation for multi-worktree dev instances
# Each workspace gets a range of 20 ports from its base.
# Offsets: +0 web, +1 api, +2 marketing, +3 admin, +4 docs,
# +5 desktop vite, +6 notifications, +7 streams, +8 streams internal, +9 electric,
# +10 caddy (HTTP/2 reverse proxy for electric-proxy), +11 code inspector, +12 electric-proxy (wrangler dev)
if [ -z "${SUPERSET_PORT_BASE:-}" ]; then
error "SUPERSET_PORT_BASE not set (port allocation may have failed)"
return 1
fi
local BASE=$SUPERSET_PORT_BASE
# App ports (fixed offsets from base)
local WEB_PORT=$((BASE))
local API_PORT=$((BASE + 1))
local MARKETING_PORT=$((BASE + 2))
local ADMIN_PORT=$((BASE + 3))
local DOCS_PORT=$((BASE + 4))
local DESKTOP_VITE_PORT=$((BASE + 5))
local DESKTOP_NOTIFICATIONS_PORT=$((BASE + 6))
local STREAMS_PORT=$((BASE + 7))
local STREAMS_INTERNAL_PORT=$((BASE + 8))
local ELECTRIC_PORT=$((BASE + 9))
local CADDY_ELECTRIC_PORT=$((BASE + 10))
local CODE_INSPECTOR_PORT=$((BASE + 11))
local ELECTRIC_PROXY_PORT=$((BASE + 12))
🤖 Prompt for AI Agents
In @.superset/lib/setup/steps.sh around lines 349 - 370, The port-assignment
block in step_write_env assumes SUPERSET_PORT_BASE is set and will cause an
unbound variable error if allocate_port_base failed; update the start of the
port section to guard or fail fast: check presence of SUPERSET_PORT_BASE
(reference symbols: step_write_env, allocate_port_base, SUPERSET_PORT_BASE,
setup_main) and either abort with a clear error/log and non-zero exit when it is
unset, or provide a safe default before computing WEB_PORT/API_PORT/... so the
rest of the function won't hit set -u; ensure this guard runs before any use of
BASE or the local WEB_PORT, API_PORT, etc.

Comment on lines +93 to +106
for pid in $matched_pids; do
if kill -0 "$pid" 2>/dev/null; then
kill_process_tree "$pid" TERM
root_killed=$((root_killed + 1))
fi
done

# Escalate to SIGKILL for any survivors after graceful termination.
for pid in $matched_pids; do
if kill -0 "$pid" 2>/dev/null; then
kill_process_tree "$pid" KILL
force_killed=$((force_killed + 1))
fi
done
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No grace period between SIGTERM and SIGKILL.

The SIGKILL loop (lines 101-106) runs immediately after the SIGTERM loop (lines 93-98). Processes typically need a brief window to handle SIGTERM and clean up. Without a delay, SIGKILL will almost always fire for any non-trivial process.

Proposed fix: add a short sleep between signal rounds
   for pid in $matched_pids; do
     if kill -0 "$pid" 2>/dev/null; then
       kill_process_tree "$pid" TERM
       root_killed=$((root_killed + 1))
     fi
   done

+  # Give processes a moment to handle SIGTERM before escalating
+  if [ "$root_killed" -gt 0 ]; then
+    sleep 2
+  fi
+
   # Escalate to SIGKILL for any survivors after graceful termination.
   for pid in $matched_pids; do
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for pid in $matched_pids; do
if kill -0 "$pid" 2>/dev/null; then
kill_process_tree "$pid" TERM
root_killed=$((root_killed + 1))
fi
done
# Escalate to SIGKILL for any survivors after graceful termination.
for pid in $matched_pids; do
if kill -0 "$pid" 2>/dev/null; then
kill_process_tree "$pid" KILL
force_killed=$((force_killed + 1))
fi
done
for pid in $matched_pids; do
if kill -0 "$pid" 2>/dev/null; then
kill_process_tree "$pid" TERM
root_killed=$((root_killed + 1))
fi
done
# Give processes a moment to handle SIGTERM before escalating
if [ "$root_killed" -gt 0 ]; then
sleep 2
fi
# Escalate to SIGKILL for any survivors after graceful termination.
for pid in $matched_pids; do
if kill -0 "$pid" 2>/dev/null; then
kill_process_tree "$pid" KILL
force_killed=$((force_killed + 1))
fi
done
🤖 Prompt for AI Agents
In @.superset/lib/teardown/steps.sh around lines 93 - 106, The SIGKILL loop runs
immediately after sending SIGTERM, giving processes no time to exit; add a short
configurable grace period (e.g., sleep for a few seconds) between the SIGTERM
loop and the SIGKILL loop so processes matching matched_pids can handle SIGTERM
and exit before escalation. Update the teardown logic around the
kill_process_tree calls and the counters (root_killed, force_killed) to sleep
(use a variable like TERM_GRACE_SECONDS) after the first loop and before the
second loop so the delay can be tuned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant