@pinkynrg/crew
v1.0.18
Published
Fan a named task across a group of local projects — run in parallel, open as one VSCode workspace, or hand the set to Claude Code. One persistent config, run via npx.
Maintainers
Readme
crew
Fan a named task out across a group of local projects — run it in parallel, open
the group as one VSCode workspace, or hand the whole set to Claude Code. Driven by one
persistent config. Runs via npx with nothing to install by hand.
crew is thin on purpose. It does not know what a task means — install, build,
start are just strings forwarded to each project's own runner (make, npm, a shell
script). crew owns the fan-out (parallelism, labelled output, exit-code aggregation,
lifecycle); the project owns task semantics.
Install
Run with no install (npx needs the full scoped package name):
npx @pinkynrg/crew listOr install globally — the command is crew once installed:
npm i -g @pinkynrg/crew
crew listRequires Node >= 18 on a POSIX system (macOS or Linux), with code (VSCode CLI) and
claude on your PATH. Zero runtime dependencies — crew is Node built-ins only,
including its own parallel process runner.
Update
npm has no "upgrade one global" command — reinstall at @latest:
npm i -g @pinkynrg/crew@latest
crew --version # confirmnpx @pinkynrg/crew@latest … always fetches the newest without installing. Your
~/.config/crew/config.json is untouched by upgrades (and self-heals any dropped fields
on first load of a newer version).
The three-tab workflow
The three surfaces are separate commands on purpose — each wants its own terminal / lifecycle. crew never spawns terminals; arrange the tabs (or aliases / npm scripts) yourself:
| Tab | Command | Owns |
| --- | --- | --- |
| 1 | crew start | the dev servers (streams until Ctrl-C) |
| 2 | crew workspace | one multi-root VSCode window |
| 3 | crew claude | an interactive Claude Code session |
Each opens a multiselect picker (preselected with your last pick) and remembers the selection, so tabs 2 and 3 open the same set you started.
Quick start
crew add # wizard: create a project (run once per project)
crew install # pick projects, install them (waits, reports pass/fail)
crew start # pick projects to run locally (remembers your pick)
crew start env=qa # same, passing a placeholder value to the start task
crew workspace # open the remembered set as one VSCode window
crew claude # launch Claude Code over the remembered set
crew edit # wizard: change a project laterConcepts
- Projects are the only building block — there are no named groups. You choose a set of projects per run from an interactive multiselect (preselected with your last pick); projects are never named on the CLI.
- The chosen set is remembered globally (machine-local
local.json) and reused acrossstart/install/workspace/claude— socrew workspaceright aftercrew startopens the same set.crew listshows the current remembered selection. - Paths are
~-expanded and resolved relative to the current directory. Before any command acts, crew verifies each selected project'spathexists and fails naming the offender. - Folder lists (workspace folders,
claude --add-dir) are deduped by resolved absolute path, so a project selected twice is never listed twice.
The runner / tasks model
A task name becomes a command per project, with no duplication:
project.tasks[<task>]if present — an explicit override.- else
project.runnerwith{task}substituted (e.g.make {task}→make build). - else the project is run-less for that task and is skipped (with a one-line note).
Run-less projects still appear in
workspaceandclaude.
If, after resolution, no project in the target can run the task, crew errors and runs nothing.
Example config
~/.config/crew/config.json:
{
"version": 2,
"workspaceName": "crew",
"longRunning": ["start", "dev", "watch"],
"projects": {
"api": {
"path": "~/code/api",
"type": "backend",
"runner": "make {task}"
},
"web": {
"path": "~/code/web",
"type": "frontend",
"runner": "npm run {task}"
},
"worker": {
"path": "~/code/worker",
"type": "backend",
"tasks": {
"start": "AWS_PROFILE=pre_bee ./scripts/run.sh {env}"
}
},
"docs": {
"path": "~/code/docs",
"type": "other"
}
}
}Here api runs any task through make {task}, web through npm run {task}, worker
has an explicit tasks.start override with an {env} placeholder, and docs is
run-less (skipped for that task, kept for workspace/claude).
Placeholders & args (strict)
Resolved commands may contain {name} placeholders. {task} is filled automatically from
the task name; {envfile} is filled by crew (see wiring below); everything else comes from
your key=value args (key=value fills {key} by name). Projects are chosen in the
picker — any bare command-line token is ignored (with a yellow warning).
Resolution rules:
- every placeholder must be satisfied, else a red error lists the unresolved ones and nothing runs;
- a
key=valuethat matches no placeholder is skipped with a yellow warning (socrew start env=localstill runs when nothing has an{env}); - substituted values are shell-quoted, so spaces and metacharacters are safe.
crew start env=qa # opens the picker, then fills {env} in each project's startcrew hardcodes no task names or values beyond the longRunning list — no baked-in
local/pre/qa/pro vocabulary.
Dependency graph
crew graph derives a read-only dependency graph from each project's .envs/* files —
who calls whom — with no manual edge list. It powers the connectivity check crew start
does on a co-running set.
Give each project a match: the complete hostname(s) it's served under — exact
strings, listing every env variant. An edge P → T is drawn when a URL in P's env files
has a host equal to one of T's match strings.
"projects": {
"api": {
"path": "api", "runner": "make {task}",
"match": ["api.example.com", "qa-api.example.com", "pre-api.example.com"]
}
}- Exact match, so
api.example.commatches only that host — neverrge-api.example.comorvpc-…-api-….amazonaws.com. No globs, no collisions. - List each env variant (
qa-,pre-, …); add new ones when envs appear. - A project with no
matchhas no id, so nothing can point at it —crew graphflags it. crew derives nothing from folder/file/env names; the exact hosts are the whole rule.
When you crew start a set, crew warns if the selection isn't connected in this graph
(crew graph restricted to the chosen projects) — i.e. you're running projects that won't
actually talk to each other locally. It's a warning, not a block.
Two execution modes
The mode is decided by whether the task is in config.longRunning:
- Long-running (
start,dev,watch, …): parallel and streamed with labelled, per-project-colored output. Ctrl-C — or any one process exiting — tears the whole group down. crew owns the terminal and exits with an aggregate code. - Run-to-completion (
install,build,test, …): parallel, but crew waits for all to finish (it does not kill the others when one finishes), then prints a per-project pass/fail summary and exits non-zero if any project failed.
How teardown works (and why it's reliable)
crew runs each command via /bin/sh -c in its own process group (spawn detached).
On teardown it signals the whole group by pgid (kill(-pgid)) — SIGTERM, then SIGKILL
after a grace period (CREW_KILL_GRACE_MS, default 5000ms). A second Ctrl-C force-kills
immediately.
This is the key reason crew rolls its own runner instead of a ppid-walking tree-kill:
reparented grandchildren — a dev server's autoreload child, a supervisord, anything
that daemonizes — get orphaned to init and escape a ppid walk, leaving a port bound. A
process-group signal reaches them regardless of reparenting. POSIX only (macOS + Linux).
Commands
Actions:
crew help usage (also: no args, -h, --help)
crew list list projects (alias: ls)
crew install pick projects, run their install task
crew start [args] pick projects, run their start task (local wiring)
crew workspace [--fileless] pick projects, open one VSCode window (alias: code)
crew claude pick projects, launch Claude Code once (--add-dir)
crew graph [project...] dependency graph derived from .envs filesstart/install/workspace/claude always open the interactive multiselect
(preselected with your last pick); the selection is remembered globally.
Config:
crew add wizard: create a new project
crew edit [name] wizard: modify an existing project
crew remove <name> delete a project (confirm; -y) (alias: rm)
crew guards [project] list/manage guards (add/remove/link/unlink)
crew dir [path] show/set the projects dir (relative paths resolve here)
crew config [path|edit] print merged config / its path / open in $EDITOR
crew pull <url> fetch config.json from a URL, install it (backs up current)Global flags: --dry-run, --skip-guards, --config <path>, -y/--yes, -h/--help,
-v/--version. Every acting command supports --dry-run to print what it would do
without running.
Guards
A project can require named guards — preconditions verified before crew start/install
does anything. crew stays agnostic: a guard is just a shell command, and it passes iff
it exits 0. Guards live in a top-level registry and attach to projects many-to-many:
{
"guards": {
"aws": {
"command": "aws sts get-caller-identity --profile pre_bee >/dev/null 2>&1",
"message": "AWS SSO expired — run: aws sso login --profile pre_bee"
},
"vpn": {
"command": "ifconfig | grep -qE 'inet (10\\.11\\.12\\.|172\\.27\\.)'",
"message": "VPN not connected."
}
},
"projects": {
"backend": { "path": "~/code/backend", "type": "backend", "guards": ["aws"] },
"orchestra": { "path": "~/code/orchestra", "type": "backend", "guards": ["aws", "vpn"] }
}
}Before a run, crew collects the union of the target's guards, deduped by name — a
guard shared by several projects runs once, not per project. All run in parallel; if
any fails, crew prints each failure's message in red and aborts before anything
starts:
guards: aws, vpn
✓ aws
✗ vpn: VPN not connected.
crew: guard failed — nothing started.Bypass with --skip-guards. Guards only gate start/install — workspace and
claude don't run them.
Managing guards
All wizard/select-driven — no hand-editing:
crew guards [project] list guards (all, or just a project's), with which projects use each
crew guards add wizard: name + command + failure message, then attach to projects
crew guards remove pick a guard to delete (also detaches it from every project)
crew guards link pick a guard, then toggle which projects use it (multi-select)
crew guards unlink pick a guard, then pick linked projects to detachYou can also attach guards from the project side in crew edit <project> (the guards
multi-select). Both sides write the same project.guards list.
The hidden workspace file
crew workspace generates the multi-root .code-workspace file inside crew's own config
dir — not your project — at:
~/.config/crew/workspaces/<selection>.code-workspace<selection> is the sorted member names joined — the same set produces the same file
regardless of pick order. crew opens it with code <that file>. This keeps the workspace file invisible in your
project explorer and out of git, while staying deterministic and reopenable: the file is
regenerated on every invocation to reflect the current config, and code <file> focuses
an existing window for that workspace instead of duplicating it.
--fileless is an alternative: it opens a new window on the first folder, then
code --adds the rest as an in-memory Untitled Workspace (attaches to the last active
instance; less deterministic).
Claude sessions (stable history)
Claude Code stores its per-directory history under ~/.claude/projects/<cwd-slug>/, keyed
by the directory it's launched in. So crew claude launches Claude with a stable,
crew-managed working directory per selection:
~/.config/crew/sessions/<selection>/Every project is still passed via --add-dir, so the whole set is fully accessible. Because
the cwd is fixed to the sorted set of names — not the first member — your Claude history
for a given set is stable: picking the same projects in any order reuses the same history,
and it never lives inside one project's folder.
Name the chat history with an optional session name: crew claude billing-work keeps
history in ~/.config/crew/sessions/billing-work/. Omit it to get a name auto-derived from
the selected projects. (It's a name, not a path — always kept under crew's sessions dir.)
Note: the working dir is a crew-owned folder, not a project checkout, so there's no cwd
CLAUDE.md/git at the root — each project brings its own via --add-dir. Switching to
this scheme starts history under the new stable path; any prior history under a project's
slug isn't deleted, just no longer auto-loaded.
Config
- User-level:
~/.config/crew/config.json(created on first write). - Project-local: a
./.crew.jsonin the current directory merges on top of the user config (itsprojects/guardsoverride by name). --config <path>points at a specific config file instead.
On load, a config with a missing or < 2 version is migrated to v2 in memory and
written back. A v1 project's single start block becomes tasks.start.
Projects directory (shareable config)
A project's path can be relative — it resolves against a machine-local
projects directory. Set it once per machine:
crew dir ~/Projects # set it
crew dir # show itThe projects dir is stored in local.json (next to the config — ~/.config/crew/local.json),
never in config.json. That keeps config.json fully shareable. Project paths are
then short and portable:
{
"projects": {
"backend": { "path": "bee-beepro-backend", "type": "backend", "runner": "make {task}" },
"frontend": { "path": "bee-beepro-frontend", "type": "frontend", "runner": "npm run {task}" }
}
}~…/absolute paths are still honoured as-is (escape hatch for a repo living outside the
projects dir). A relative path with no projects dir set is a clear error pointing you at
crew dir.
Sharing a config with your team
Because config.json never contains machine-specific data, it's directly committable:
- Keep
projects/guardson relative paths (crew dir+crew add/editdo this). - Commit
config.json(in a repo, orgit initinside~/.config/crew). Gitignorelocal.json(andworkspaces/,sessions/,tmp/) — those are machine-local/generated. - A teammate installs it — clone/symlink to
~/.config/crew/config.json, or fetch it straight from the repo withcrew pull <raw-url>(backs up any current config; a private repo needs a token/PAT in the URL). Thencrew dir <their-path>once and everything resolves; no absolute paths are ever shared.
--config <path> works too: local.json is always read from beside the config file,
so an isolated config keeps its own machine settings.
Known limitations (by design)
- No task dependency graph and no ordering. crew fans out one task at a time.
Need "install before start"? That's two commands typed in sequence. No caching, no
build-system behavior — that's
make/turbo/nxterritory. - No startup ordering within a run. All projects start simultaneously; long-running services must tolerate their dependencies coming up in any order (retry / reconnect).
- No
up/bundler command, no terminal or pane spawning, no tmux, no self-built process manager, no health-check / wait-for-ready, no port-conflict detection, no plugin system, telemetry, or auto-update.
License
MIT
