devtooie
v0.7.1
Published
Dependency-aware CLI for running a monorepo's local dev processes.
Maintainers
Readme
devtooie
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.

Features
- 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.
Example
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.
Requirements
- Node ≥22.18.
devtooie.config.tsis imported directly, so it needs Node's native TypeScript type-stripping — unflagged in 22.18 (and, on the 23.x line, 23.6). - 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.
Install
pnpm add -D devtooieGetting started: devtooie init
pnpm 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.
devtooie.config.ts
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({
// keyed by package name — the key is the name, so there's no `name` field
packages: {
'core-api': {
port: 3001, // is provided as PORT environment variable to the process
// `healthcheck` and `urls` take a string or a callback over this package's
// `{ envs, tokens, port }` — devtooie does no string interpolation of its own.
healthcheck: ({ port }) => `http://localhost:${port}/health`,
},
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 }],
},
web: {
port: 3000,
waitFor: ['core-api'], // hold until core-api's healthcheck passes — typo-checked
deps: { runtime: ['core-api'] }, // selecting web also runs core-api
},
},
});See Configuration options for every defineConfig
and package field.
Running the Terminal UI
pnpm devtooiePackage supporting scripts
devtooie 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:
// packages/shared/package.json
{
"name": "shared",
"scripts": {
"dev": "tsc --watch", // re-emits dist on change
"build": "tsc",
},
}An application needs only a dev process — a Node backend:
// packages/backend/package.json
{
"name": "backend",
"scripts": {
"dev": "node --watch --watch-path=./src src/index.ts",
},
}devtooie runs the dev script exactly as written, so what the process watches is up to the script.
See docs/package-lifecycle.md for why
--watch-path is worth adding when you use Node's own watcher.
…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.
Running one-off commands (devtooie cmd)
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.
Configuration options
The full defineConfig and per-package field reference — including dependencies,
TypeScript project references, and typed package names — lives in
docs/configuration.md.
Logging
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 customize that JSON rendering per package with the logging
helpers — or, for output that isn't JSON, write a logs.formatter over the raw line yourself.
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.
Package lifecycle when you edit code
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.
Environment (.env) loading
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
├── .env.development # workspace scope, the default mode
└── packages/
├── core-api/
│ ├── .env # package scope — overrides workspace scope
│ └── .env.local # package scope, higher precedence
└── web/
└── .envFiles for a mode, ascending precedence within a scope:
.env.env.local.env.<mode>.env.<mode>.local
The two .local files are the personal tier — commit .env and .env.<mode>, and keep
.env*.local out of git.
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.
The ambient environment wins over the files, as in Next.js, Vite and
node --env-file — so FOO=bar devtooie overrides a file for one run. Name the
exceptions with env.override when a file needs to extend an inherited value:
defineConfig({
env: { override: ['NODE_OPTIONS'] }, // or `true` for every variable
packages: {/* … */},
});With that, NODE_OPTIONS=$NODE_OPTIONS --flag appends to whatever the shell
already set instead of losing to it.
Modes
--mode <name> picks which .env.<mode> files load; it defaults to
development, so plain devtooie loads .env.development.
devtooie --mode test # loads .env.test / .env.test.local
devtooie --mode test cmd -- vitest # same environment, one-off commandAny name works (test, staging, e2e), and the mode is passed to every child
process as DEVTOOIE_MODE. Modes are exclusive — --mode test does not
load .env.development — so values shared across modes belong in .env and
.env.local, which load in every mode.
--mode deliberately leaves NODE_ENV alone (a mode name is free-form, and
NODE_ENV isn't). Set it from the mode's own file if you want it:
NODE_ENV=test in .env.test.
A package's port is also injected as PORT (an explicit .env PORT
still overrides it). The reverse direction works too — port may be a callback
that reads these same resolved files to decide the port:
backend: { port: ({ envs }) => Number(envs.BACKEND_PORT) }healthcheck and urls take the same kind of callback, which additionally gets the
resolved port. healthcheck also accepts { url, timeout }, for a service slow to
answer while it warms up — see
Readiness probing. See
Configuration for the details.
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.
Advanced CLI usage
Every flag and subcommand — plus devtooie cmd for running a command in a package's
environment on demand — is documented in docs/cli.md.
Agent skill
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 check whether an app in the repo is already running, 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.
Control API
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.
License
MIT
