Skip to content

tincke10/Lane

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lane

Port-collision-free project switcher for parallel development.

Lane is a small CLI that lets you run multiple development projects in parallel without their ports stepping on each other. It does one thing: it remembers which ports each of your projects has reserved, and exports those ports as environment variables into your current shell — the same pattern used by direnv and asdf.

Lane never edits your project files. It does not start, stop, or proxy anything. It is a thin coordination layer over the convention your tools already understand:

# docker-compose.yml (the convention)
services:
  app:
    ports:
      - "${APP_PORT:-80}:80"   # APP_PORT supplied by Lane
$ eval "$(lane use my-laravel-app)"
$ docker compose up           # uses Lane's reserved APP_PORT

Why Lane?

If you have ever tried to run a Laravel/Sail project, a Vite dev server, and a Postgres container in parallel — across two or three different projects — you have seen this dance:

  • Project A and project B both want :80, :3306, :5173.
  • You hand-edit docker-compose.yml or .env in one of them.
  • You forget. Two weeks later you boot both, ports collide, things break.

Lane fixes the port assignment part of that problem. It does not try to be a process manager, a service mesh, or an orchestrator. It does not isolate filesystems or contexts — your OS and your editor already do that. It just makes sure that when you say "I am working on project A", your shell exports the ports project A is allowed to use, and nothing else.


Supported stacks

Lane works out of the box with the stacks most fullstack developers touch every day. "Out of the box" means: lane init recognizes the project, allocates the right environment variables, and there is a matching lane <framework> runner that wires the port into the dev server automatically.

Web frameworks (HTTP app server, $APP_PORT)

Stack Detected from Default port Runner
Laravel (with or without Sail) composer.json requiring laravel/framework 8080 lane servephp artisan serve
Django manage.py, or django in requirements.txt / pyproject.toml 8000 lane djangopython manage.py runserver
Flask flask in requirements.txt / pyproject.toml 5000 lane flaskflask run
Next.js next in package.json dependencies 3000 lane nextnpx next dev

Frontend dev servers ($VITE_PORT)

Stack Detected from Default port Runner
Vite vite in package.json dependencies 5173 lane vitenpx vite

Containerized services ($FORWARD_DB_PORT, $FORWARD_REDIS_PORT)

Stack Detected from Default port Runner
MySQL mysql in docker-compose.yml 33060 (none needed — Compose reads the env var)
PostgreSQL postgres in docker-compose.yml 54320 (none needed)
Redis redis in docker-compose.yml 63790 (none needed)

Things that "just work" without a runner

  • Docker / Docker Compose / Laravel Sail — Compose reads ${APP_PORT:-80}:80 from the env Lane exports. docker compose up, sail up, anything that uses Compose substitution: zero changes.
  • Multiple instances of the same framework — two Laravel projects, two Django projects, etc. Each gets a unique port (8080, 8081, 8082, …) at lane init time.
  • Mixed stacks in one terminal session — auto-activation switches the exported vars whenever you cd into a different project.

Things Lane does NOT cover yet

Lane is intentionally conservative — runners only exist for frameworks where the dev-server invocation is unambiguous. If you need one of these, the port is still allocated (set lane init and read $APP_PORT from your shell) but you wire the flag yourself:

  • Rails (bin/rails server -p $APP_PORT)
  • Express / Fastify / Hono — most read process.env.PORT; map it with a one-liner in your code or PORT=$APP_PORT npm run dev
  • Go services — same idea, read os.Getenv("APP_PORT") in main.go
  • Phoenix, ASP.NET, Spring Boot, anything else — same pattern

If you want a runner for one of these, open an issue (or grep for cmdServe in internal/cmd/runners.go — each runner is ~15 lines).

What Lane does NOT do, on purpose

  • It does not rewrite your docker-compose.yml, .env, vite.config.js, or any other file in your project.
  • It does not manage Docker networks, volumes, or images.
  • It does not orchestrate process lifecycles (start/stop/restart).
  • It does not proxy network traffic.

Lane reserves ports and exposes them as env vars. Everything downstream of that is the framework's job.


How it works

Lane stores one TOML file (~/.config/lane/projects.toml by default, honoring $XDG_CONFIG_HOME) describing each registered project, the tech stack it uses, and the ports it has reserved.

When you lane use <project>, Lane prints a sequence of export statements to stdout. You feed those into your shell with eval:

eval "$(lane use my-project)"

Your shell now has APP_PORT, VITE_PORT, FORWARD_DB_PORT, etc. exported. Any docker compose up, php artisan serve, or npm run dev you run after that picks them up automatically — as long as your project's config follows the standard Docker/Sail convention of ${APP_PORT:-80}:80.

To clear them when you switch projects:

eval "$(lane unuse)"

That is the whole model.


Installation

From source

Requires Go 1.25 or newer.

git clone git@github.com:tincke10/Lane.git
cd Lane
make install

That installs lane into $(go env GOPATH)/bin. Make sure that directory is on your PATH.

Or build the binary locally:

make build
./bin/lane help

Quick start

# 1. Register a project. Lane detects the stack and allocates free ports.
$ cd ~/code/my-laravel-app
$ lane init
registered "my-laravel-app" at /Users/me/code/my-laravel-app
  stack: [docker laravel mysql node php redis vite]
  APP_PORT=8080
  FORWARD_DB_PORT=33060
  FORWARD_REDIS_PORT=63790
  VITE_PORT=5173

# 2. Switch to it in any shell.
$ eval "$(lane use my-laravel-app)"
$ echo $APP_PORT
8080

# 3. List everything Lane knows about.
$ lane list
my-laravel-app
  path:  /Users/me/code/my-laravel-app
  stack: docker, laravel, mysql, node, php, redis, vite
  APP_PORT=8080
  FORWARD_DB_PORT=33060
  FORWARD_REDIS_PORT=63790
  VITE_PORT=5173

# 4. Clean up when you are done.
$ eval "$(lane unuse)"

Commands

Command What it does
lane init [--name N] [--path P] Register the project in P (defaults to cwd). Detects the stack, allocates free ports, persists to the registry, and warns if a present docker-compose.yml does not reference the env vars Lane just allocated.
lane use [--shell posix|fish] <name> Print activation statements for the project. POSIX (bash/zsh) by default, fish syntax via --shell fish. Aborts (with empty stdout and a stderr message) if any reserved port is currently in use.
lane unuse [--shell posix|fish] Print deactivation statements for the project named in LANE_ACTIVE_PROJECT. Safe to run when nothing is active.
lane list (alias ls) List all registered projects with their paths, stacks, and ports.
lane rm <name> (alias remove) Remove a project from the registry.
lane doctor Diagnose registry health: missing paths, cross-project port collisions, currently bound ports, stack drift, orphaned active project. Exits non-zero only on errors; warnings are informational.
lane hook <bash|zsh|fish> Print the shell hook code for auto-activation on cd. Run once during shell setup.
lane export [--shell posix|fish] Hook-driven activation diff. Called by the installed hook on every prompt; not typically invoked directly.
lane serve [extras...] Run php artisan serve on Lane's $APP_PORT (falls back to 8000 outside any project). Extras forwarded to artisan.
lane vite [extras...] Run npx vite on Lane's $VITE_PORT (falls back to 5173). Extras forwarded to vite.
lane next [extras...] Run npx next dev -p $APP_PORT (falls back to 3000). Extras forwarded to next.
lane flask [extras...] Run flask run --port=$APP_PORT (falls back to 5000). Requires FLASK_APP in env. Extras forwarded.
lane django [extras...] Run python manage.py runserver $APP_PORT (falls back to 8000). Falls back to python3 if python is absent. Extras forwarded.
lane help Show usage.
lane version Print the version.

Flags

  • lane init --name <name> — Override the project name. Defaults to the basename of --path.
  • lane init --path <path> — Project path. Defaults to the current working directory. The path is stored absolute.

Stack detection

Lane inspects the top level of the project directory (no recursion) and looks for known marker files. Detection is best-effort and intentionally conservative.

Marker file(s) Base tag Deeper tag
composer.json php laravel (if require."laravel/framework" is present)
package.json node vite, nextjs (if the respective package is in dependencies or devDependencies)
pyproject.toml or requirements.txt python flask, django (substring, case-insensitive)
manage.py (in addition to the above) python django
docker-compose.yml / docker-compose.yaml / compose.yml / compose.yaml docker mysql, postgres, redis (substring match)

A malformed composer.json or package.json still yields the base tag (php, node) and silently skips the deeper detection — Lane will not fail because of a transient broken JSON file.

Convention check at init

When lane init finds a docker-compose.yml (or compose.yml / compose.yaml / docker-compose.yaml), it cross-references every env var it just allocated against the file's contents. If a var is not referenced — for example, the compose file hardcodes "80:80" instead of "${APP_PORT:-80}:80" — Lane prints a warning at the end of registration:

  warning: docker-compose.yml does not reference these env vars:
    - APP_PORT (allocated 8080, but compose will ignore it)
  use the ${VAR:-default}:default pattern in your compose ports to opt in.

The warning never fails lane init. You can still register the project and decide later whether to update the compose file. Lane recognizes the standard shell forms: ${VAR}, ${VAR:-default}, ${VAR:?error}, and bare $VAR. Prefix collisions like ${VAR_EXTRA} do not count as references to VAR.


Port allocation policy

The first time you lane init, Lane allocates the lowest free port at or above the base for each env var your stack needs. Reserved ports from other registered projects are skipped, so two projects never get the same port.

Stack marker Env var Base port
laravel APP_PORT 8080
django APP_PORT 8000
flask APP_PORT 5000
nextjs APP_PORT 3000
vite VITE_PORT 5173
mysql FORWARD_DB_PORT 33060
postgres FORWARD_DB_PORT 54320
redis FORWARD_REDIS_PORT 63790

When multiple frameworks compete for APP_PORT, precedence is laravel > django > flask > nextjs. Likewise mysql wins over postgres for FORWARD_DB_PORT. Projects that legitimately combine two backends (rare) can override at runtime via lane serve --port=... or by editing projects.toml manually.

Lane scans up to 1000 consecutive ports above the base before giving up. The check uses a TCP listen on 127.0.0.1, which is inherently racy — another process can grab the port between the check and your actual use. lane use re-checks at activation time and refuses to export if any port is busy.


Activation pattern

Lane follows the direnv / asdf playbook: it prints, your shell evals. Nothing magic, no daemon, no hooks installed unless you add them yourself.

One-shot per shell

bash / zsh:

eval "$(lane use my-project)"
# ... work ...
eval "$(lane unuse)"

fish:

lane use --shell fish my-project | source
# ... work ...
lane unuse --shell fish | source

Persistent shell aliases (bash / zsh)

Add to your ~/.bashrc or ~/.zshrc:

lane-use()   { eval "$(command lane use   "$@")"; }
lane-unuse() { eval "$(command lane unuse "$@")"; }

Then:

lane-use my-project
lane-unuse

Auto-activation on cd (recommended)

Install the hook once and Lane keeps your env in sync automatically as you cd around. Each prompt, Lane walks up from the current directory, finds the nearest registered project, and emits the diff against what your shell currently has activated.

Add to ~/.zshrc:

eval "$(lane hook zsh)"

Or to ~/.bashrc:

eval "$(lane hook bash)"

Or to ~/.config/fish/config.fish:

lane hook fish | source

What you get:

  • cd ~/code/my-laravel-appAPP_PORT, VITE_PORT, etc. exported.
  • cd ~/code/other-project → previous project's vars unset, new project's exported.
  • cd ~ (no project) → everything unset, shell back to clean state.
  • cd deep/inside/my-laravel-app/src/components → still activates my-laravel-app. Lane walks up the tree until it finds a match.

The hook is silent and fast: it does not check port availability on every prompt (lane doctor is the place for that), it does not call out to the network, and it stays quiet on transient errors so a broken registry never breaks your prompt.

tmux / zellij / terminal tabs

Because activation lives in environment variables, each tab/pane/window keeps its own active project. The auto-activation hook works independently in each one — open a new tab, cd into a different project, and that tab activates that project without touching the others.

Collision safety

If a reserved port is already bound by some other process when you run lane use, Lane prints nothing to stdout and writes the conflict to stderr with a non-zero exit code. That way eval "$(lane use ...)" never silently activates a broken environment.

$ eval "$(lane use my-project)"
lane: port collision detected for "my-project" — not activating:
  APP_PORT=8080 is in use
$ echo $?
1

Framework runners

Some frameworks read their port from a CLI flag rather than the env vars Lane exports (Sail/Docker do the right thing automatically via the ${APP_PORT:-80}:80 convention; standalone php artisan serve and npx vite do not). Lane ships runners that bridge the gap:

# Laravel
lane serve              # = php artisan serve --port=$APP_PORT
lane serve --host=0.0.0.0

# Vite (any frontend project with vite installed)
lane vite               # = npx vite --port=$VITE_PORT
lane vite --open

# Next.js
lane next               # = npx next dev -p $APP_PORT
lane next --turbo

# Flask (requires FLASK_APP env or .flaskenv)
lane flask              # = flask run --port=$APP_PORT
lane flask --debug

# Django
lane django             # = python manage.py runserver $APP_PORT
lane django --noreload  # falls back to python3 if `python` is unavailable

Outside a Lane project the runners fall back to the framework's own default (8000 for artisan, 5173 for vite), so they remain safe to alias to plain serve / vite-dev in your shell if you want.

For Sail / Docker Compose users: keep using docker compose up / sail up as before. The compose file's ${APP_PORT:-80}:80 pattern picks up Lane's exported env vars automatically — no runner needed.

Configuration

Lane stores its registry as TOML at:

$XDG_CONFIG_HOME/lane/projects.toml      # if $XDG_CONFIG_HOME is set
$HOME/.config/lane/projects.toml         # fallback

The file is written atomically (*.tmp + rename) so an interrupted write cannot corrupt your registry. It is plain text — feel free to edit it by hand if you need to bulk-rename or relocate things.

Environment variables Lane reads

  • XDG_CONFIG_HOME — overrides the default registry location.
  • LANE_ACTIVE_PROJECT — set by lane use, read by lane unuse to know what to unset.

Environment variables Lane sets (via lane use)

  • LANE_ACTIVE_PROJECT — name of the currently active project.
  • One export per reserved port (e.g., APP_PORT, VITE_PORT, FORWARD_DB_PORT, FORWARD_REDIS_PORT).

Development

make build         # compile to ./bin/lane
make install       # install to GOPATH/bin
make test          # run tests with race detector
make test-cover    # run tests + open HTML coverage report
make lint          # golangci-lint
make fmt           # gofmt + go vet
make tidy          # go mod tidy
make clean         # remove build artifacts

Project layout

cmd/lane/             Entry point: parses --version, delegates to internal/cmd.
internal/registry/    TOML-backed registry of projects and reserved ports.
internal/ports/       Free-port detection and collision-aware allocator.
internal/stack/       Filesystem-based stack detection (composer/package.json/etc).
internal/activator/   Generates POSIX `export` / `unset` statements.
internal/doctor/      Diagnostic checks for registry health (used by `lane doctor`).
internal/convention/  Validates that docker-compose references the env vars Lane allocates.
internal/cmd/         CLI dispatcher and subcommand handlers.

Each internal package is independently testable. ports and stack have no dependencies on the registry; cmd is the only layer that wires them together.


Why "Lane"?

Each project gets its own lane on the road. They run in parallel, they do not cross each other, and you can switch lanes whenever you want. The name avoids the saturated nautical metaphors (harbor, berth, pier, regatta) that already exist in this space.


License

MIT. See LICENSE.

About

Run multiple Laravel, Django, Next.js, Vite, and Docker projects in parallel without port collisions. Zero config.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors