A dependency-aware terminal UI (TUI) for running a monorepo's packages during local development.
dev + TUI → devtooie.
You describe your packages once, in a small typed config file. devtooie resolves build-time, dev-time, and runtime dependencies between them, builds whatever needs building (in the right order), and then runs the packages you picked.
- Dependency-aware builds. Declare build/dev/runtime deps once; devtooie builds what needs building, in the right order, before it runs anything.
- Language-agnostic packages. A package is driven through a handful of named
scripts, so it can be written in anything: a Node package (via its
package.json) or a Go, Rust, … package (via aMakefilewith the equivalent targets). See Package supporting scripts. - Streamed, filterable logs. Every package's output is streamed live into one
combined view; filter it down to a single package or a search term on the fly
(the
fhotkey; matching is case- and accent-insensitive). - Structured logs, by default. devtooie auto-formats every package's JSON logs (Go
slog, pino, winston, …) as a colored[LEVEL] messagefor local dev — noNODE_ENVbranching in your app, and nothing to configure. See Logging. - Two run modes. An interactive terminal UI to pick and watch packages, or a
--plainlog-streaming mode for coding agents. - One-off commands.
devtooie cmdruns a single command (or a package script/target) in a package's directory with that package's resolved environment — for migrations, seeds, scrapers, or an agent driving your project. See Running one-off commands. - Per-package hierarchical
.envloading. Each package's.envfiles (workspace- and package-scoped) are resolved and injected into its process automatically — and live-reloaded, restarting the affected package when a file changes. - Readiness ordering.
healthcheck+waitForhold a package until the services it needs are actually up. - Lifecycle-aware. Each package declares whether its dev process watches or just builds, so you (or an agent) know exactly what to do after a code edit.
- Control API + agent skill. A localhost HTTP API drives a running session headlessly and lets a second invocation hand off cleanly; an installable skill teaches a coding agent to use it.
A complete, runnable example monorepo lives in
example/: four packages — a shared
TypeScript library (isomorphic), a Node API (backend), a Go worker driven through a
Makefile (worker), and a web frontend — wired up with dependency-aware builds, .env
loading, healthchecks, and waitFor readiness ordering.
- Node 20+. A
.tsconfig additionally needs Node ≥23.6 (native type-stripping); on older Node, use a compileddevtooie.config.js/.mjs. - Unix only (macOS/Linux). Windows is not supported.
- pnpm. Node packages are run with
pnpm run <script>, and packages that depend on each other are resolved through pnpm workspace links (workspace:*). (Makefile packages are run withmakeinstead.) - A
package.json(orMakefile) per package with the scripts devtooie drives (dev,build, ...) — see Package supporting scripts below.
pnpm add -D devtooiepnpm devtooie initThis is an interactive, idempotent setup flow. It will:
- Ask whether to install the agent skill (recommended: yes).
- Scaffold
devtooie.config.tsat your repo root (an existing config file is left untouched). - Reconcile a root
tsconfig.jsonso the config type-checks with Node globals in scope (idempotent — your other settings are left untouched). - If you opted in to the skill, install it.
Pass -y/--yes to accept the defaults non-interactively.
After that, fill in the scaffolded config's packages array with your real
packages (see below) and run pnpm devtooie.
The one file you author and commit — the single source of truth the CLI reads on every run.
import { defineConfig } from 'devtooie';
export default defineConfig({
packages: [
{
name: 'core-api',
port: 3001, // is provided as PORT environment variable to the process
// `$port` is substituted with this package's `port`.
healthcheck: 'http://localhost:$port/health',
},
{
name: 'worker',
// a dev process that doesn't watch files: it builds once, then runs. devtooie
// doesn't watch your source, so after you edit its code you (or an agent, via the
// control API) restart it — the command's flags say which. See docs/package-lifecycle.md.
command: ['start', { watches: false, builds: true }],
},
{
name: 'web',
port: 3000,
waitFor: ['core-api'], // hold until core-api's healthcheck passes
deps: { runtime: ['core-api'] }, // selecting web also runs core-api
},
],
});See Configuration options for every defineConfig
and package field.
pnpm devtooiedevtooie drives each package through named scripts — a Node package declares them in its
package.json scripts; a package in any other language (Go, Rust, …) declares the equivalent
make targets in a Makefile. devtooie invokes them as pnpm run <name> or make <name>.
dev— the long-running process devtooie starts and streams. An application usually needs only this; devtooie builds its dependencies for it.build— a shared library that other packages build against adds this too, so devtooie can build it in the build phase before its dependents start.
A shared library (Node) — dev + build:
An application needs only a dev process — a Node backend:
// packages/backend/package.json
{
"name": "backend",
"scripts": {
"dev": "node --watch src/index.ts",
},
}…or a Go program, via a Makefile:
# packages/worker/Makefile
.PHONY: dev
dev:
@go run .An app can add build + clean too, for the occasional case where you need to rebuild it from
scratch to clear stale build output — those enable the rebuild command (the b hotkey /
POST /command/rebuild); see Package lifecycle.
Sometimes you don't want the whole session — you just need to run one command with a
package's exact environment: a migration, a seed script, a scraper, a REPL. devtooie cmd
does that. It runs a command in a package's directory with that package's resolved env vars injected — the same environment the TUI would give it:
cd packages/api
devtooie cmd -- pnpm run migrate # a literal command, in api's dir with api's env
devtooie cmd -c seed -- --rows=100 # run api's `seed` script/target, forwarding args
cd ../..
devtooie cmd -p api -- pnpm start # or target a package by name, from anywhereThe package is inferred from your current directory (or named explicitly with -p). Output
streams to your terminal and is also written to a logfile. It's especially handy for a coding
agent driving your project. Full reference: devtooie cmd.
The full defineConfig and per-package field reference — including dependencies,
TypeScript project references, and typed package names — lives in
docs/configuration.md.
devtooie auto-formats structured (JSON) logs — from Go slog, pino, winston, … — into a
colored [LEVEL] message for local dev, with no NODE_ENV branching and nothing to configure. You
can add on-screen timestamps, and override or customize the formatter per package. See
docs/logging.md.
Every session is also teed to a timestamped logfile. Read the current one from another terminal
with devtooie logs (or devtooie logs -f to follow it live) — see
devtooie logs.
A package's command flags declare whether its dev process watches or just
builds, which tells you (or an agent) whether to restart or rebuild it after a
code edit. See docs/package-lifecycle.md.
devtooie loads .env files for every package it runs and injects them into that
package's child process — merged over the current process.env without mutating
it. Parsing is handled by dotenvx under the hood.
Files are resolved at two scopes: the workspace root and the package's own
directory. Only files that exist are loaded.
your-monorepo/
├── .env # workspace scope — base for every package
├── .env.local # workspace scope, higher precedence
└── packages/
├── core-api/
│ ├── .env # package scope — overrides workspace scope
│ └── .env.local # highest precedence for core-api
└── web/
└── .env
Default files, ascending precedence within a scope:
.env.env.development.env.local
Package scope overrides workspace scope, and within a scope a later file
overrides an earlier one. ${VAR} references expand against already-loaded files
and the current environment; file values win over the ambient environment (so
NODE_OPTIONS=$NODE_OPTIONS --flag extends the inherited value).
A package's port is also injected as PORT (an explicit .env PORT
still overrides it).
Customize the list via env.files (each name is still resolved at both scopes):
defineConfig({
env: { files: ['.env', '.env.local'] },
packages: [/* … */],
});While a session runs, devtooie watches these files (and where new ones would appear) and restarts the affected package(s) on change — editing a workspace-level file restarts every running package that uses it.
The same resolution is available as a standalone command: from inside a package's directory,
devtooie cmd -- <command> runs a one-off command in that package's dir with its resolved
env (or invoke one of its scripts/targets with -c) — see
devtooie cmd.
Every flag and subcommand — plus devtooie cmd for running a command in a package's
environment on demand — is documented in docs/cli.md.
If you opt in during devtooie init, devtooie installs an agent-facing skill
file at .claude/skills/devtooie/SKILL.md (and, best-effort, under .agents/ /
.cursor/ if those directories already exist). It teaches a coding agent how
to run devtooie headlessly (--plain -p <package>), drive a running session
through the control API, read the logfile for debugging, and onboard a new
package. The installed file is managed — treat it as generated, not something
to hand-edit. devtooie init and every devtooie run refresh it to the
installed version.
The skill points the agent at a single consolidated guide,
packages/devtooie/docs/agents.md — the same
material as this README plus how to drive devtooie headlessly, in one self-contained
file. It's the one doc that ships inside the package (so the skill can load it from
node_modules); the topic docs above live at the repo root.
While a session runs, devtooie exposes a localhost-only HTTP API for driving it (restart/rebuild a package, query status, hand off between invocations). See docs/control-api.md.
MIT
