Skip to content
pinkynrgPublic

About

Run the slice of your microservice stack you care about locally, wired to the rest. The services you don't run stay on their deployed env. Zero-dependency CLI;

Topics

Resources

Code of conduct

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

crew

release go license coverage

Quickly select and run the slice of your stack you're interested in - locally, wired to the rest.

Every service is a switch: on is local, off is remote. Flip a service on and crew runs it natively and rewrites every peer that talks to it to point at your localhost; leave it off and it stays on its real deployed environment, callers untouched. The slice you run is just which switches are on - one service, or the whole stack - and flipping one re-wires the rest for you, straight from the .env files you already ship.

crew start env=staging     web   [x] on  → http://localhost:3000
                           api   [x] on  → http://localhost:4000   (web now calls this)
                           auth  [ ] off → https://auth.staging.acme.dev
                           …     [ ] off → deployed staging

crew is a zero-dependency CLI for local dev on a distributed stack. Once a slice is running you can also open it as one VS Code workspace, or hand it to a single Claude Code session.

Assumes each service ships at least one environment file with its peers' URLs plus a deployed host those point at - that's what crew borrows for the services you leave off. Nothing deployed to point at? A full-stack tool like Docker Compose fits your case better; see Why not just use Docker? below.

What it does

crew start: pick a connected slice of services from the dependency graph, run it locally, watch the labelled logs

You have fifteen services. Today you're touching two. crew start opens a picker over the dependency graph crew derived from your env files, you tick the slice you care about, and crew:

  • runs each service's start task natively in parallel, with labelled, per-service-colored logs;
  • wires the slice together - rewrites the URLs in each service's env file so a running peer points at your local copy instead of the deployed one;
  • leaves everything you didn't pick on its resolved remote env (qa / staging / prod), so the rest of the stack is just… there.

The picked set is remembered, so the other two surfaces open the same thing:

Command What it opens
crew start env=staging the dev servers (streams until Ctrl-C, tears the whole group down together)
crew workspace one multi-root VS Code window with every picked repo side by side
crew claude one Claude Code session over the set, with history kept per group of repos

They're separate commands on purpose - each wants its own terminal tab and lifecycle. crew never spawns terminals for you.

Debug a service in the slice. Give a service a debug command — the debug field in crew config (stored as tasks.debug), e.g. node --inspect=:9230 server.js, python -m debugpy --listen :5678 -m app, next dev. Then in the crew start picker, press d on a running node to flip it into debug mode - its box shows [debug] and crew launches tasks.debug instead of tasks.start for that one. Mix freely: run the whole slice, only the service you're stepping through under a debugger. d only appears for a node that's on (local) and has a tasks.debug; each service owns its own debugger + port.

Fresh clones just work. Give a service an install command — the optional install field in crew config (stored as tasks.install) — and crew start runs it before that service's start (or debug) command, every time. crew doesn't know whether an install is needed: the command decides, so make it a no-op when nothing changed, e.g. [ node_modules/.package-lock.json -nt package-lock.json ] || npm ci. Without an install, nothing changes: you install by hand as before. Machine-level prerequisites (a tool like poetry or nvm, a native lib) belong in guards — a guard checks and tells you what to install.

Install

crew is a single static binary (Go) — nothing else to install, no runtime.

curl -fsSL https://raw.githubusercontent.com/pinkynrg/crew/main/install.sh | sh
crew --version

Or grab a binary from GitHub Releases, or build from source with go install github.com/pinkynrg/crew/cmd/crew@latest. macOS + Linux (arm64/amd64); code (the VS Code CLI) and claude on your PATH for those two surfaces. Self-update with crew upgrade; crew also offers it by itself — at most once a day, an everyday command run in a terminal asks upgrade now? [y/N] when a newer release is out (yes upgrades, then re-runs your command). The last check is remembered in update-check.json beside the config (gitignore it like local.json); CREW_NO_UPDATE_CHECK=1 turns the offer off.

How it's configured

crew config: add a frontend and a backend by picking their folders (crew auto-fills type, runner, env, local URL and start command), fill the deployed hosts, and create a guard

You don't hand-write the config. crew config is a two-pane visual editor: pick a service's folder and crew fills in the mechanical parts it can read from your package.json, lockfiles and .envs - type, runner, start command and env-file path. The URLs are yours to add: the service's local URL/port and its deployed host per environment. Out comes one readable config.json - committable, no secrets.

A service entry holds just a few things (crew fills the first two, you fill the URLs):

  • path + tasks.start - where the repo lives and how to run it (auto-filled from the folder). Drop {envfile} in the command and crew injects the wired env file it materializes.
  • local - the service's local URL/port. crew guesses it when it can spot a port; otherwise you add it. This is what a peer's env gets rewritten to point at.
  • match - the host(s) the service is deployed under, per env. Always entered by hand - crew never guesses it. It matches these against the URLs in your env files to auto-discover who depends on whom, so there's no manual edge list.
{
  "services": {
    "web": {
      "path": "web",
      "type": "frontend",
      "tasks": { "start": "dotenv -e {envfile} -- npm run dev" },
      "env": ".envs/{env}.env",
      "match": { "staging": "web.staging.acme.dev", "prod": "web.acme.dev" },
      "local": "http://localhost:3000"
    },
    "api": {
      "path": "api",
      "tasks": { "start": "uvicorn app:main --reload --env-file {envfile}" },
      "env": ".envs/{env}.env",
      "match": { "staging": "api.staging.acme.dev", "prod": "api.acme.dev" },
      "local": "http://localhost:4000"
    },
    "auth": {
      "path": "auth",
      "tasks": { "start": "godotenv -f {envfile} go run ." },
      "env": ".envs/{env}.env",
      "match": { "staging": "auth.staging.acme.dev", "prod": "auth.acme.dev" },
      "local": "http://localhost:4500"
    }
  }
}

From those hosts and env files crew derives the dependency graph - here web → api → auth - which drives the picker, connectivity warnings, and per-service env resolution. Preview it with crew graph (a drawn ASCII diagram) and crew resolve <env> (a dry-run of what env each service lands on). Because there are no secrets or machine paths in it, config.json is directly committable and shareable across your team; the machine-local bits (your services directory, remembered selection) live in a gitignored local.json beside it.

The full field-by-field reference is in Config reference below.

Why not just use Docker?

crew leans on the switch model above - run your slice natively, borrow the rest from their deployed envs - which puts it in a different category from the usual suspects:

crew Docker Compose Tilt / Skaffold Telepresence / mirrord overmind / foreman
Runs your slice natively (no containers) ✅ ❌ ❌ ✅ ✅
Rest of the stack = real deployed envs ✅ ❌ ❌ ✅ (needs a cluster) ❌
Wiring comes from your .env files a compose file k8s manifests cluster intercept —
Infra required none Docker daemon Kubernetes Kubernetes none

crew sits between plain process-runners (overmind, mprocs, foreman) and remote-wiring tools (mirrord, Telepresence): the native-slice-plus-real-remote of the latter, without the cluster, proxy, or containers of either. Just URL rewrites in the .env files you already have.

Versus Compose specifically: a service is in the compose file or it isn't, and "use the deployed one instead" is a manual env edit - so you tend to run the whole graph locally or maintain a second compose file full of stubs, and either drifts from production. crew has a per-service local/remote switch and derives the wiring from your env files, so there's nothing parallel to keep in sync.

Running natively also gets you real hot-reload, a debugger you attach directly, and native file watching - no daemon, no images, no rebuild-on-change, and nothing to install (one file, zero deps).

When Docker is still the right call: you need full-stack isolation, byte-for-byte prod/CI parity, or your services can't run natively at all. crew doesn't replace Docker there - it replaces the daily loop of flipping a couple of services on and letting everything else stay remote.


Config reference

Commands

crew list                       list services (remembered selection, per-folder status)
crew start env=<env>            pick services, wire + start them for that env
crew workspace                  pick services, open one VS Code window
crew claude [name]              pick services, launch one Claude Code session (--add-dir)
crew graph [list]               dependency graph derived from .envs files (drawn / adjacency)
crew resolve <env> [proj…]      dry-run: the env each service resolves to for a selection
crew config [path]              two-pane visual editor for everything (or print the config path)
crew check                      validate config + local.json; list errors / warnings
crew pull <url>                 fetch a config.json from a URL and install it (backs up current)
crew upgrade                    self-update (downloads the latest release binary)

start / workspace / claude always open the interactive multiselect (preselected with your last pick) and the selection is remembered globally; services are never named on the CLI there. crew start is the only command that runs anything. Global flags: --config <path>, -v/--version.

The runner & tasks model

start is the one core task crew runs. Its command per service, with no duplication:

  1. service.tasks.start if present - an explicit command (in crew config, the start field);
  2. else service.runner with {task} substituted (e.g. make {task} → make start);
  3. else the service is run-less and skipped (it still shows up in workspace / claude).

service.tasks.install, when present, runs first, as written (no placeholders), in the service's directory and in a subshell, so a cd inside it doesn't affect start. Installs run one at a time, in config order, and no service starts until every install has finished, so a service can rely on setup another one does (e.g. a monorepo's shared root node_modules) and installs never race on shared package caches. A failed install stops the run right away with its exit code; fix it and press r. Because installs run one after another, a start pays the sum of their up-to-date checks, so keep those checks fast. Installs apply to debug too, and their output streams under the service's label.

A slow install never looks frozen: every 15s (CREW_INSTALL_TICK_MS) the installing service logs how long it has been going and what it's running right now (⏳ installing · 1m15s · now: npm ci › node ./scripts/install.js), and the services queued behind it say once whose install they're waiting for. An install that stops on a terminal prompt (an ssh passphrase, a git login) can't be answered inside crew — crew runs it in the background and owns the terminal — so crew names it instead (⏸ install stuck on a terminal prompt): run that install once in a terminal. Status lines land in the run logs too.

A service's tasks map can hold other tasks too, but they're just data for now: only start, its per-node debug variant (below) and install (before either) are run. debug runs under crew start via the selector's d toggle.

Resolved commands may contain {name} placeholders. {task} is filled from the task name, {envfile} by crew (the wired env file), and everything else from your key=value args. Every placeholder must resolve or crew errors and runs nothing; an unused key=value is a yellow warning; substituted values are shell-quoted. crew start requires env=<name> - the base env the unselected services point at.

crew start always streams: services run in parallel with per-service-colored output, and Ctrl-C - or any one process exiting - tears the whole group down. On a TTY this is a full-screen log viewer (f to filter which services are shown, d to show/hide each line's time (remembered), c to copy, esc/Ctrl-C to stop). A copy always carries each line's full date and time (2026-10-02 14:03:07.123 [service] …), whether or not the time column is shown, and so do the run's log files the agent pane reads — so an agent can follow what happened when, across services.

Teardown is reliable because each command runs via /bin/sh -c in its own process group (spawn detached); crew signals the whole group by pgid - SIGTERM, then SIGKILL after a grace period (CREW_KILL_GRACE_MS, default 5000ms). Reparented grandchildren (autoreload children, supervisord) that a ppid tree-kill would orphan get signalled anyway. POSIX only (macOS + Linux).

Env derivation & the dependency graph

You pass one env to the selection (crew start env=pre); crew works out what env each service actually runs at by following the graph. The entry (the thing nothing else in the selection depends on) runs at your selection env; every other service inherits the env-variant its consumer's env file points at - read straight from the files via the env-labeled match. So the same shared config serves multiple teams correctly, because the answer is context-dependent in a way a static per-service setting never could be. Disagreements, missing env files, and unreachable services are reported as warnings - never silently mis-resolved.

crew graph renders the derived graph as a laid-out ASCII diagram (boxes, per-source colored edges, solid dependency arrows, dashed reference arrows) - a zero-dep layered-DAG renderer, no external tool. On a TTY it opens in an alternate-screen pager (f filters nodes, esc quits leaving no scrollback); piped, it prints plainly. crew resolve <env> is the read-only dry-run.

A URL from a non-frontend into a type: frontend service (a backend embedding the app's public URL) is treated as a reference, not a dependency - shown in the graph but excluded from connectivity and env derivation.

Env overrides

URL swapping isn't always enough - sometimes a value must change when you run locally (a Temporal queue name, say). overrides upsert extra KEY=value lines into a service's wired env file. They live as a top-level table in the committable config.json (keep secrets out):

{
  "overrides": {
    "bee-orchestra": { "TEMPORAL_ORCHESTRA_AI_QUEUE": "orchestra-local-ai" },
    "beepro-frontend": {
      "whenLocal": {
        "bee-loader": { "REACT_APP_BEEPLUGINURL": "http://localhost:8088/v2/api/loader" }
      }
    }
  }
}
  • bare VAR: value - applied whenever that service starts (a value that's always different locally);
  • whenLocal: { "<peer>": { VAR: value } } - applied only when <peer> is also being started (e.g. point a URL at a local dependency's exact host and path, but only while it's up).

Overrides win over the base env file and the localhost URL swap; whenLocal wins over bare. Manage them in crew config → a service's Environment Overrides block.

Guards

A service can require named guards - preconditions verified before crew start does anything. A guard is a shell command that passes iff it exits 0, with a required comment (what it checks) and a failure message. They live in a top-level registry and attach to services many-to-many:

{
  "guards": {
    "aws": {
      "comment": "AWS SSO token still valid.",
      "command": "aws sts get-caller-identity --profile pre_bee >/dev/null 2>&1",
      "message": "AWS SSO expired - run: aws sso login --profile pre_bee"
    }
  },
  "services": {
    "backend": { "path": "~/code/backend", "type": "backend", "guards": ["aws"] }
  }
}

Before a run crew collects the union of the target's guards (deduped by name - a shared guard runs once), runs them in parallel, and if any fails prints its message in red and aborts before anything starts. Manage them in crew config → the Guards section.

Config files & sharing

  • User-level: ~/.config/crew/config.json (v2 schema; v1 migrates on load).
  • Service-local: a ./.crew.json in the current directory merges on top.
  • --config <path> points at a specific file. local.json is always read from beside it.

config.json never contains machine-specific data, so it's directly committable - keep service paths relative (they resolve against a machine-local services directory, set once in crew config → Settings, stored in local.json). A teammate installs it - clone it to ~/.config/crew/config.json or crew pull <raw-url> - sets their services dir once, and everything resolves with no absolute paths ever shared. Gitignore local.json (plus workspaces/, sessions/, tmp/).

crew check is a built-in zero-dependency validator: errors (wrong types, missing path, undefined guard, {envfile} with no env) exit 1; warnings (unknown keys, a glob-looking match, a match with no local, a path missing on disk) exit 0. A good pre-commit / CI gate for a shared config.

crew workspace & crew claude details

crew workspace generates a multi-root .code-workspace inside crew's own config dir (~/.config/crew/workspaces/<selection>.code-workspace) - not your service, so it stays out of git - and opens it with code. A top-level workspaceSettings object is written verbatim into its settings (e.g. { "jest.enable": false }).

crew claude launches Claude Code with a stable, crew-managed working directory per selection (~/.config/crew/sessions/<selection>/), passing every service via --add-dir. Because the cwd is the sorted set of names, history for a given set is stable regardless of pick order. Name it with an optional session name: crew claude billing-work.

Known limitations (by design)

  • No task dependency graph, no ordering. crew just starts the selected services in parallel; it doesn't sequence tasks. No caching, no build-system behavior - that's make / turbo / nx territory.
  • No startup ordering within a run. All services start simultaneously; services must tolerate their dependencies coming up in any order.
  • No bundler command, no terminal/pane spawning, no tmux, no health-check / wait-for-ready, no port-conflict detection, no plugin system, no telemetry.

FAQ

Does crew modify my repos or my .env files? No. It reads your env file and writes a wired copy to ~/.config/crew/tmp/<service>.env (URLs swapped to localhost for the peers you're running), passes that path to your start command via {envfile}, and deletes it on teardown. Your checkout and its committed env files are never touched.

Where do my secrets live? In your own env files, same as today - crew only reads them. The committable config.json holds no secrets; machine-local bits (services dir, remembered selection) live in a gitignored local.json. The temporary wired copy under ~/.config/crew/tmp/ is the only materialized secret, and it's regenerated per run and removed on exit.

What if a remote env (staging) is down? Then the services you left off are unreachable - exactly as if you called staging directly. crew doesn't proxy or cache; turn those services on to run them locally, or wait for staging to recover.

Do I have to run everything? No - that's the whole point. Run one service or all of them; whatever you don't run stays on its deployed env, wired in automatically.

Windows? POSIX only (macOS + Linux) - teardown relies on process groups (setsid / kill(-pgid)).

License

MIT

About

Run the slice of your microservice stack you care about locally, wired to the rest. The services you don't run stay on their deployed env. Zero-dependency CLI;

Topics

Resources

Code of conduct

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages