Releases: hefgi/ecluse
Releases · hefgi/ecluse
Release list
v0.3.1
Fixed
- tmux startup sources
.env,.env.local, and.env.eclusewith an explicit./prefix (. './.env'). zsh's POSIX.builtin does not search cwd unless.is in$PATH, so the previous bare. '.env'form silently failed on zsh (the default macOS shell), leaving services in tmux windows without the per-slot env. (#27) parse_env_filenow strips a matched outer pair of"..."or'...'from dotenv values. The per-slug preamble previously echoedONYX_ENVIRONMENT='"development"', leaking the literal double-quote characters into the runtime env and breaking strict consumers (e.g. zodz.literal("development")). Unquoted values likepostgres://x:5433/db?a=bare unaffected. (#28)
v0.3.0
Added
- Pending sessions:
up/downreserve the session and release the state lock during provisioning/teardown, so parallel sessions never serialize on image pulls or hooks.lsshows(pending)(and"status"in--json), warns about entries pending >15 minutes after a crashed command, anddown <slug>recovers them. Concurrent finalizes are guarded by per-operation ownership tokens — deleted sessions are never resurrected by a racing command. - Rollback-on-failure is enforced by an undo-stack guard: any
bring_upfailure tears down exactly what was created (containers, overlays, worktree, spawned services), in reverse order. Failed re-ups of existing sessions keep their data volumes. - PID files record the process start token; a recycled PID is never signaled, never attributed by
whose-pid, and reports as down. Containers are matched by compose project label instead of name substrings. - tmux services run as their window's own process with recorded pane PIDs; commands that exit within ~1.5s fail
ecluse upwith the exit status and last output. Dead panes are kept for inspection; windowshellis a plain shell with the session env. publish_primaryon[[services]]: explicit control over primary-port publication (the implicit "extra_port with container_port suppresses the primary" rule is deprecated;validatewarns).- Docker-gated end-to-end test suite (
tests/docker_e2e.rs): lifecycle, rollback, multi-compose teardown, container mode — runs wherever a docker daemon is available, skips elsewhere.
Changed
extra_portsuse the same per-slot spacing as primary ports (base + slot*slot_stride) and are probed before startup: occupied extras are a hard error understrict_portand a warning otherwise. Withslot_stride = 1(the default) allocations are unchanged.up --forceonly kills processes owned by the session (verified via pid files/tmux panes), with TERM→KILL escalation; unowned listeners produce a warning naming the PID.- Teardown dispatches on the session's recorded mode, so changing
modein.ecluse.tomlno longer strands containers of existing sessions. ModeHandler::bring_uptakes aBringUpRequest; container/hybrid share the docker-startup, worktree, port-allocation and spawn building blocks (was ~100 duplicated lines).- Session state records explicit
(compose, overlay)pairs; teardown no longer reconstructs compose paths from overlay filenames (ambiguous for hyphenated slugs). Legacy state files still tear down via the old path. - Branch handling:
up <branch>tracksorigin/<branch>when it only exists on the remote (instead of forking a same-named branch from HEAD), and refuses to resume a slug that belongs to a different branch. - The tmux env preamble is per-slug (
.ecluse/preambles/<slug>.sh); a shared file leaked ports between parallel sessions.
Fixed
killpaths signal the whole process group with TERM→KILL grace — service children no longer surviveecluse downholding their port.- A failing service spawn cleans up the services already spawned instead of orphaning them.
ecluse syncno longer accepts any directory whose path merely contains the slug; the cwd must be a linked worktree of the repo. It also refuses sessions that are mid-operation.worktree removeverifies the directory is actually gone instead of treatinggit worktree prunesuccess as removal success.- Shared-lock acquisition reads the real state when
state.lockis missing (previously reported "no sessions" while sessions ran);lsno longer panics on malformed timestamps;.env.ecluseis parsed by a single shared parser everywhere. - Examples and README migrated off the deprecated
on_up/on_downhook names (the README example also ran migrations inpre_up, before any env exists; nowpost_up).
v0.2.17
Added
inherit_enventries now support a per-filemode(symlinkorcopy). The default remains symlink — existing configs are unchanged — but each entry may opt into the object form{ file = "...", mode = "copy" }to get a one-time copy from the root file. Copy mode lets a worktree edit its own.env.local(e.g. flipAUTH_ENABLED=false) without affecting the root file or other parallel worktrees. On subsequentecluse upruns, copied files are preserved verbatim — never re-copied — so per-worktree edits stick. Stale symlinks left over from a priorsymlinkconfiguration are replaced with a fresh copy.
v0.2.16
Added
ecluse whose-pid <pid>command: reverse-maps a PID to the ecluse session that owns it. Checks.ecluse/pids/<slug>/*.pidfiles and walks descendants up to 5 levels, sotask/make-spawned children resolve to the right session. For tmux-managed sessions also checks pane PIDs and their subtrees. Use--jsonfor machine-readable output. Exit code:0if owned,1if not. Required before any manualkillof a process on an ecluse-allocated port — prevents parallel agents from killing each other's services.slot_stridetop-level config field: spacing between ports of adjacent slots. Defaults to1(unchanged behaviour). Set to10to give slots 1/2/3 portsbase+10,base+20,base+30instead ofbase+1,base+2,base+3. Wider spacing reduces the chance that an agent misidentifies an adjacent-slot port as its own stale leftover.find_free_portand the adjacent-gap validation both account for stride.
Changed
skills/ecluse/SKILL.mdadds a "Killing services safely" section: forbids rawlsof -ti | xargs killof ecluse-allocated ports, mandatesecluse down --keep-worktree+ecluse up --reuse-worktreeas the canonical reset, and requiresecluse whose-pidownership verification before any manual kill. Also warns that external task runners (task,make,npm run) bypass.env.ecluseand should be replaced withcommand = "..."entries in.ecluse.toml.
Fixed
ecluse upon resume (second invocation against an existing session) now honors theworktree_pathrecorded instate.jsoninstead of recomputing the default<root>/<worktree_dir>/<slug>location. Sessions whose worktree lives outside.ecluse/worktrees/— e.g. those auto-registered from a sibling git worktree the user created manually — no longer fail with "worktree not found at ; remove --reuse-worktree or run ecluse up without it" on the secondecluse up.ecluse statusnow always reports the port allocated by ecluse (fromstate.json/.env.ecluse), never a port that happens to be open in the service's process subtree. Previously, if a child process in the service tree was listening on a different port (e.g. atask-spawned helper that bound 2080 while the actual service was on 11734),statusreported that child's port — making the table contradict.env.ecluseand hiding the real problem. Health is now computed by verifying that the service process subtree owns the expected port; if it doesn't, the service shows as down. Affects native services in tmux and nohup process_manager modes.
v0.2.15
Added
pre_spawnlifecycle hook — runs after the worktree and slot are allocated but before any services are started. Useful for provisioning secrets, seeding databases, or any setup that must happen before the first process starts. Receives the same env vars aspost_up.host_portfield on[[services]]blocks: decouples the host-side port range from the container-internal port. Setbase_portto the port the container listens on (e.g.5432) andhost_portto the host range base (e.g.11532). ecluse publishes(host_port+slot) → base_portin the overlay and setsECLUSE_<NAME>_PORT = host_port + slot. Defaults tobase_portwhen omitted — existing configs are unaffected.
v0.2.14
Added
ecluse status: port-allocation-only services (nocommand) now show—in the STATUS column instead of✗ down. They are not counted in the "N services down" summary, do not trigger exit code 1, and have"managed": falsein JSON output. This prevents agents from treating unmanaged services as failures.- tmux windows now source
.env,.env.local, and.env.eclusebefore running the service command. Manual restarts inside a tmux window (↑ Enter) automatically have the correct environment without needingsource .env.ecluse &&in everycommandfield. Load order:.env→.env.local→.env.ecluse(ecluse slot vars win on overlap). - nohup-spawned services now receive vars from
.env,.env.local, and.env.eclusemerged into their environment at spawn time, consistent with tmux behaviour.
Fixed
ecluse upwith no argument now fails fast with an actionable error when stdin is not a terminal (CI, agents, piped shells), instead of blocking on a branch-name prompt until killed.ecluse downandecluse shutdownpre/post hook failures now emit a warning and continue teardown instead of aborting. Teardown must always complete regardless of hook exit codes.- Error messages when no
.ecluse.tomlis found or no active session exists are now context-aware: the hint differs depending on whether the cwd is a linked git worktree, the repo root, or unrelated to any known session.
v0.2.13
Added
extra_portsfield on[[services]]blocks: a list of additional per-slot port allocations, each withbase_portandport_env. The env var is set tobase_port + slotin the process environment. For docker services the port is also published as a host→container binding in the compose overlay and injected intocompose_envso compose files can interpolate it (e.g.${PGPORT}). This is the generic replacement fordebug_port— use it for debugger ports (Node.js--inspect, Delve, debugpy, pprof), auxiliary listeners, or any secondary port a service exposes.debug_portis now deprecated. It continues to work — existing configs are unchanged — butecluse validateemits a warning andextra_portsshould be used for new configs.
Fixed
ecluse down(hybrid mode) now always stops Docker containers even when no overlay file paths are recorded in session state. Previously, ifoverlay_filewas absent from state,docker compose downwas never called and containers kept running silently.ecluse upfrom inside a non-ecluse git worktree (e.g. a sibling path, not under.ecluse/worktrees/) now correctly uses the actual worktree directory instead of computing a path underworktree_dir. Previously the computed path didn't exist andecluse upfailed with a "worktree not found" error.ecluse status,ecluse ls,ecluse env, andecluse shellnow acquire a shared (read-only) lock instead of an exclusive lock. These commands no longer time out with "another ecluse process may be running" when a long-runningecluse upholds the exclusive lock.- Port collision detection now checks
docker pshost-port bindings in addition tolsof. Docker containers claim host ports before they start listening, so anlsof-only check could pick a port already reserved by a container. Both checks are best-effort: if Docker is unavailable, the check is skipped and never blocks.
v0.2.12
Changed
commandis now optional for native services. Omitting it puts the service in port-allocation-only mode: ecluse allocates the port and injects allECLUSE_*env vars, but does not spawn or manage the process — start it yourself via a task runner,post_uphook, or any other means.ecluse validateemits a warning (not an error) whencommandis absent.
v0.2.11
Added
inherit_envconfig field (default:[".env", ".env.local"]) — files listed here are symlinked from the main worktree root into each new worktree atecluse uptime. Symlinks keep worktrees in sync with root changes automatically. Set to[]to opt out. Pass--no-inherit-envto skip for a singleupcall (for CI/agents).ecluse shutdownnow prints aecluse flushhint when any session teardown fails, making recovery more discoverable.
Fixed
- Hardcoded credentials removed from the
node-containerexample compose file.
v0.2.10
Added
debug_portfield on[[services]]blocks: secondary port for debuggers or auxiliary servers. ecluse computesdebug_port + slotand exposes it asECLUSE_<NAME>_DEBUG_PORT. Use when a service exposes a second listener (Node.js--inspect, Delve, debugpy, pprof, etc.) that defaults to a hardcoded port and would collide across parallel sessions.ecluse upnow accepts branch names with slashes directly:ecluse up feat/add-auth→ slugfeat-add-auth, branchfeat/add-auth. The--branchflag is removed — branch comes from the argument itself.ecluse upwith no argument uses the git worktree location to determine intent: inside an ecluse-registered worktree → reuse stored slug; inside any other git worktree → auto-detect branch from cwd and register the worktree (no--reuse-worktreeflag needed); in the main worktree / repo root → prompt for a branch name. Detached HEAD exits with a clear error.- Worktree deletion guard:
ecluse downandecluse shutdownnow always prompt before removing a worktree (stop / keep / delete). A ⚠ warning is shown if the worktree has uncommitted changes. Pass--delete-worktree/--delete-worktreesto skip the prompt and delete (for CI/agents), or--keep-worktree/--keep-worktreesto skip and keep.
Changed
ecluse statussession header is now a borderless key-value block (Slug,Slot,Worktree,Tmux) with right-aligned labels, replacing the single run-on line. Labels use the user-facing term "Slug" (not "Session").ecluse statuslast column adapts to the session's process manager:WINDOW(tmux window name) for tmux sessions,PIDfor nohup, omitted entirely for container-only sessions.ecluse statusnative service health check for tmux sessions now verifies that a descendant of the tmux pane's shell process owns the expected port, rather than probing the port directly. This correctly handles port collisions with unrelated processes and services that fail to start despite their tmux window existing.
Fixed
docker stopcalls in--forcenow useDOCKER_HOSTfrom the active Docker context, so OrbStack containers are correctly targeted.- tmux session spawning no longer fails with "duplicate session" when the previous session's shell is still alive after services crash. The stale session is killed before a new one is created.