@boomspot/devcc
v0.1.2
Published
A keyboard-driven terminal command center for local development: processes, scripts, git, services, logs and dependencies across one or more projects.
Downloads
382
Maintainers
Readme
Dev Command Center
A single keyboard-driven screen that replaces the pile of terminal tabs you keep open while working on a project. Built with OpenTUI.
What it is for
Working on a real project normally means one tab running next dev, another for
git status, another for pnpm test, another where you docker ps or check
whether Postgres is up, and a fifth for actually typing commands. State ends up
scattered: you cannot see at a glance whether the dev server is up, whether your
tree is dirty, or which of those tabs has the error in it.
Dev Command Center puts that in one place.
- Watches - git branch and changes, which services are actually listening (Docker, Postgres, Redis, Ollama, a framework dev server), host CPU / memory / disk, and outdated dependencies.
- Controls - starts, stops and restarts the dev processes you declare in
.devcc.ts; runs yourpackage.jsonscripts; stages, commits, pulls and pushes. - Collects - everything it launches streams into one log viewer, per source or merged and searchable, so the error you are chasing is not in a tab you have scrolled away from.
More than a launcher
Three things it does that running the commands by hand does not:
- Brings a stack up in order.
dependsOnplus a readiness check meanswebwaits untilapiis genuinely accepting connections, not merely spawned. - Recovers from crashes. Opt-in automatic restarts with exponential backoff, and it gives up rather than thrashing.
- Holds several projects open at once. Each keeps its processes alive in the
background while you flip between them with
ctrl+o.
Where the line is
It is not a terminal replacement - there is no shell in it, and it will not run interactive commands. It is not a deploy tool or a container manager; it observes Docker rather than replacing compose. And it deliberately will not touch processes it did not start: anything you launched in another tab shows up as detected and read-only.
It earns its place on projects with more than one long-running process, or when
you bounce between repositories. For a single next dev you would barely notice
the difference - the payoff scales with how many moving parts your setup has.
At a glance
┌──────────────────────────────────────────────────────────────────────────────────┐
│ DEV COMMAND CENTER command-center · pnpm · ~/dev/cc 21:04 │
├──────────────────────┬───────────────────────────────────────────────────────────┤
│ PROJECT │ GIT │
│ │ branch main │
│ ● Overview 1 │ status 2 modified / 1 untracked │
│ ○ Processes 2 p │ tracking ahead 2 │
│ ○ Services 3 │ │
│ ○ Scripts 4 │ DEVELOPMENT │
│ ○ Git 5 g │ ● tsc --watch running pid 48213 2m 10s │
│ ○ Logs 6 l │ ○ vitest stopped │
│ ○ Dependencies 7 d │ │
│ ○ System 8 │ SYSTEM │
│ ○ Settings 9 │ cpu ███░░░░░░░░░ 21% load 2.14 │
│ │ memory ████████░░░░ 7.2 / 36.0 GB │
├──────────────────────┴───────────────────────────────────────────────────────────┤
│ ↑↓ navigate p processes g git l logs / commands ? help q quit │
└──────────────────────────────────────────────────────────────────────────────────┘Requirements
OpenTUI renders through a native Zig core over FFI, so the dashboard needs one of:
- Bun 1.3.0 or newer (recommended), or
- Node.js 26.4.0 or newer, started with
--experimental-ffi
The devcc launcher detects this for you: run it with any Node ≥ 20 and it will
re-exec into Bun (or into Node with the right flag) automatically. Dependency
installation, linting, type checking and the unit tests all run on plain Node.
Install and run
npm i -g @boomspot/devcc # or: pnpm add -g @boomspot/devcc
cd ~/code/example.com
devccdevcc runs against the project you are standing in. To point it somewhere else
without changing directory:
devcc -C ~/code/example.comCLI options
| Option | Description |
| ------------------- | ------------------------------------------------- |
| -C, --cwd <dir> | Project directory (default: nearest project root) |
| -h, --help | Show usage |
| -v, --version | Show the version |
DEVCC_LOG_LEVEL (debug | info | warn | error | silent) controls
the internal diagnostics log written to .devcc/devcc.log.
Running from source
git clone https://github.com/boomspot/command-center.git
cd command-center
pnpm install
pnpm dev:center # launch against this repo
node bin/devcc.mjs -C ~/code/example.com # or against another project
pnpm build # compile to dist/How much setup is needed
Two prerequisites, then nothing else is required to get a useful screen:
- Install it -
npm i -g @boomspot/devcc. - A runtime with FFI - Bun ≥ 1.3, or Node ≥ 26.4 run with
--experimental-ffi(see Requirements). The launcher handles this for you if Bun is installed; if neither is available it says so and stops.
What works with no configuration
Run devcc in ~/code/example.com having written nothing, and it works these
out for itself:
| It figures out | How |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Project root | Walks up from the working directory looking for package.json or .git |
| Package manager | packageManager field → lockfile → the manager that launched it → npm |
| Your scripts | Reads scripts from package.json |
| Git state | Runs git status --porcelain=v2 directly - branch, staged/modified/untracked, ahead/behind, recent commits |
| Local services | TCP-probes Postgres, Redis and Ollama; queries the Docker daemon and any compose file; infers dev-server ports from your dependencies (next → 3000, vite → 5173, typesense → 8108) |
| Host metrics | Node's os and fs APIs - CPU, memory, disk, load, uptime |
| Tool versions | git, docker and your package manager, read once in the background |
That is real detection rather than defaults, so Overview, Scripts, Git, Services and System are all populated on first launch.
What does require configuration
Managed processes. devcc cannot guess that pnpm dev is the thing you want
it to own, that it should wait for port 3000 before calling it ready, or that it
is safe to restart. Nothing is launched or controlled until you declare it in
.devcc.ts:
// ~/code/example.com/.devcc.ts
export default {
processes: [
{
id: "web",
name: "Next.js",
command: "pnpm",
args: ["dev"],
url: "localhost:3000",
readiness: { port: 3000 }, // "running" means actually serving
restartOnCrash: true, // comes back after a crash, with backoff
},
],
commands: [
{
id: "migrate",
title: "Run migrations",
category: "Database",
command: "pnpm",
args: ["db:migrate"],
confirm: true, // asks first: it mutates the database
},
],
}Restart devcc and the Processes pane is live: s starts or stops, r restarts,
enter jumps to that process's logs, and quitting stops everything devcc
started. The declared command appears in the palette (/) and asks before it
runs.
This is deliberate. The alternative is a tool that spawns processes you did not ask for, and devcc will never touch a process it did not start - see Configuration for every available field.
Keyboard shortcuts
Press ?, F1 or ctrl+k in the app for the manual, which also explains what each section is for and the main config fields.
Global
| Key | Action |
| --------------------- | --------------------------------------------------------- |
| ↑ / k, ↓ / j | Previous / next item |
| ← / h, → | Previous / next section |
| tab, shift+tab | Cycle sections |
| 1…9 | Jump straight to a section |
| p, g, l, d | Processes, Git, Logs, Dependencies |
| enter | Open / activate the selection |
| esc | Close an overlay |
| / | Command palette |
| ctrl+p | Open another project (others keep running) |
| ctrl+o | Cycle through open projects |
| ? / F1 / ctrl+k | The in-app manual (also "Open the manual" in the palette) |
| shift+R | Refresh everything |
| q or ctrl+c | Quit (stops the processes devcc started) |
Processes and Services
| Key | Action |
| ------- | --------------------------------- |
| s | Start or stop the selection |
| r | Restart the selection |
| i | Inspect the selection |
| enter | Jump to that process's logs |
| d | docker compose down (confirmed) |
Scripts
| Key | Action |
| --------- | ---------------------------------- |
| enter | Run the selected script |
| x | Stop a running script |
| f / c | Filter the list / clear the filter |
Git
| Key | Action |
| --------------- | --------------------------------- |
| s / u | Stage / unstage the selected file |
| d | Discard file changes (confirmed) |
| c | Commit staged changes |
| p / shift+P | Pull / push (push is confirmed) |
| f | Fetch |
| r | Refresh status |
Dependencies
| Key | Action |
| --- | --------------------------------------- |
| r | Check the registry for updates |
| u | Update the selected package (confirmed) |
Logs
| Key | Action |
| ---------------- | -------------------------------------------------- |
| j / k | Scroll |
| PgUp / PgDn | Page |
| home / end | Jump to start / end |
| f | Toggle follow |
| s | Search - plain text, or a regex if it parses |
| x | Clear the search |
| e | Toggle errors only (stderr) |
| a | Merged stream of every source, colour-keyed |
| tab, ← / → | Change log source |
| c | Clear the current buffer (all, in the merged view) |
Configuration
Configuration is optional - without it the dashboard still detects your
package manager, reads package.json scripts, shows git status, probes for
local services, and monitors the host. A config file adds processes devcc can
control and project-specific commands.
devcc looks for the first of .devcc.ts, devcc.config.ts, .devcc.mts,
devcc.config.mts, .devcc.js, devcc.config.js, .devcc.mjs,
devcc.config.mjs, .devcc.json in the project root. TypeScript config files
are transpiled on the fly, so no build step is needed.
A real example (this repository's own .devcc.ts):
import { defineConfig } from "./src/devcc/index.js"
export default defineConfig({
name: "dev command center",
processes: [
{
id: "typecheck",
name: "tsc --watch",
command: "pnpm",
args: ["exec", "tsc", "-p", "tsconfig.json", "--noEmit", "--watch", "--pretty", "false"],
kind: "service",
},
{
id: "vitest",
name: "vitest --watch",
command: "pnpm",
args: ["exec", "vitest", "--watch"],
kind: "service",
},
],
commands: [
{
id: "verify",
title: "Verify (lint, typecheck, test, build)",
category: "Project",
command: "pnpm",
args: ["run", "verify"],
keywords: ["ci", "check", "all"],
},
{
id: "clean",
title: "Remove dist/",
category: "Project",
command: "rm",
args: ["-rf", "dist"],
confirm: true,
},
],
refresh: { system: 1_500, services: 5_000, git: 8_000 },
})A monorepo example. In a project that has devcc installed, import defineConfig
from the package name (command-center); in a project that does not, export a
plain object instead - the shape is identical and it is validated either way:
import { defineConfig } from "command-center"
export default defineConfig({
processes: [
{
id: "web",
name: "Next.js",
command: "pnpm",
args: ["dev"],
cwd: "./apps/web",
url: "localhost:3000",
port: 3000,
autoStart: true,
},
{
id: "api",
name: "API",
command: "pnpm",
args: ["dev"],
cwd: "./apps/api",
url: "localhost:3001",
port: 3001,
},
{ id: "worker", name: "Worker", command: "pnpm", args: ["start"], cwd: "./apps/worker" },
],
services: [
{ id: "typesense", name: "Typesense", port: 8108, category: "database" },
{ id: "mailhog", name: "Mailhog", port: 8025, healthPath: "/" },
],
commands: [
{ id: "db:reset", title: "Reset the database", command: "pnpm", args: ["db:reset"], confirm: true },
],
})Without the package installed, the same config works as a plain object:
// .devcc.ts in any project - no import, no dependency
export default {
processes: [{ id: "web", name: "Web", command: "pnpm", args: ["dev"], port: 3000 }],
}Process definitions
| Field | Type | Description |
| ------------------- | ----------------------- | ------------------------------------------------------------------- |
| id | string | Unique id. Also the log source id. Required |
| name | string | Display name (defaults to id) |
| command | string | Executable. Required - never a shell string |
| args | string[] | Arguments passed as an array, so quoting is never an issue |
| cwd | string | Absolute, or relative to the project root |
| env | Record<string,string> | Extra environment variables |
| kind | "service" \| "task" | Long-running vs. expected to exit (default service) |
| autoStart | boolean | Start with the dashboard (default false) |
| url | string | Informational address shown next to the process |
| port | number | Port to associate with the process |
| killGraceMs | number | SIGTERM → SIGKILL grace period (default 5000) |
| restartOnCrash | boolean | Bring it back automatically after a non-zero exit (default false) |
| maxCrashRestarts | number | Give up after this many consecutive crashes (default 5) |
| crashBackoffMs | number | First backoff delay, doubling per crash (default 1000) |
| maxCrashBackoffMs | number | Backoff ceiling (default 30000) |
| healthyAfterMs | number | A run lasting this long resets the crash budget (default 30000) |
| dependsOn | string[] | Ids that must be ready before this one starts |
| readiness | object | How devcc decides the process is usable - see below |
Readiness checks
Without a readiness block a process is running the moment it has spawned.
With one, it stays starting until the check passes, and dependsOn waits for
that - so "start all" brings a stack up in the right order and only once each
layer is actually usable.
{
id: "web",
command: "pnpm", args: ["dev"],
dependsOn: ["api"],
readiness: { port: 3000 }, // TCP connect succeeds
}
{
id: "api",
command: "pnpm", args: ["dev"], cwd: "./apps/api",
dependsOn: ["db"],
readiness: { logPattern: "listening on \\d+" }, // a line of output matches
}| Field | Description |
| -------------- | ---------------------------------------------------------------------------- |
| port, host | Ready once something accepts a TCP connection (host defaults to 127.0.0.1) |
| logPattern | Ready once a line of stdout/stderr matches this regex (case-insensitive) |
| timeoutMs | Give up waiting and report running anyway, with a warning (default 30000) |
| intervalMs | Port probe interval (default 250) |
Either port or logPattern is required; both may be given. A dependency
cycle is reported and those processes are left alone rather than started in an
arbitrary order.
Command definitions
| Field | Type | Description |
| ------------------------ | ------------------------------- | ---------------------------------------- |
| id | string | Unique id. Required |
| title | string | Palette entry text |
| category | string | Palette grouping (default Project) |
| command, args, cwd | | As for processes. command required |
| keywords | string[] | Extra fuzzy-search terms |
| confirm | boolean \| { title, message } | Ask before running |
Service definitions
Extra ports to watch: { id, name, port, host?, healthPath?, category? }.
healthPath upgrades the check from a TCP probe to an HTTP request.
Other options
| Field | Description |
| ------------------- | ------------------------------------------------------------------ |
| name | Overrides the project name in the header |
| refresh | { system, services, git, clock } intervals in ms |
| maxLogLines | Ring-buffer size per log source (default 5000) |
| logLevel | Level for .devcc/devcc.log |
| projects | Extra project paths offered in the switcher (absolute or relative) |
| projectSearchDirs | Extra directories to scan for sibling projects |
Invalid entries are reported in Settings → Config issues and skipped; a typo never stops the dashboard from starting.
Architecture
src/devcc/
cli.ts argv parsing, TTY check, process exit code
index.ts public API (defineConfig, App, all reusable pieces)
app/
App.ts composition root: renderer, services, state, input
builtinCommands.ts the built-in command set
keymap.ts global bindings + the help cheat sheet
theme.ts palette and status glyphs
core/
state.ts AppState shape + the observable Store
commands.ts CommandRegistry and CommandContext
config.ts defineConfig, discovery, validation, loading
events.ts typed event bus + subscription bag
components/ Header, Footer, Sidebar, RowList, StatusBadge, Overlay,
ConfirmDialog, CommandPalette, ProjectPicker, HelpOverlay
views/ Overview, Processes, Services, Scripts, Git, Logs,
Dependencies, System, Settings (all extend View.ts)
processes/ ManagedProcess types + ProcessManager (incl. crash policy)
services/ ServiceAdapter interface, ServiceManager, and the
docker / node / postgres / redis / ollama adapters
deps/ DependencyService + per-manager `outdated` parsers
projects/ ProjectRegistry: discovery, recents, switcher list
processes/graph.ts dependsOn planner (waves + cycle detection)
logs/merge.ts merged stream, search filter
git/ GitService + pure porcelain-v2 parsers
system/ SystemMonitor (Node APIs only)
logs/ LogBuffer ring buffer + LogStore
utils/ exec, packageManager, paths, format, fuzzy, logger,
errorsHow data flows
ProcessManager ─┐
ServiceManager ─┤ typed events ┌─────────┐ watched slices ┌───────┐
GitService ─┼──────────────────►│ Store │───────────────────►│ Views │
SystemMonitor ─┤ └─────────┘ └───────┘
LogStore ─┘Backends never touch renderables. They emit typed events; App folds those into
a single AppState; views subscribe to the slices they care about via
store.watch(selector, listener), so a 1 Hz CPU sample does not repaint the git
panel. List rendering recycles TextRenderables and only materialises the rows
that fit the viewport.
Design decisions worth knowing
- Nothing runs through a shell. Every child process is
spawn(executable, args[]), so arguments containing spaces or quotes cannot become injection. - Process trees die properly. Children are spawned detached (their own
process group) and stopped with
kill(-pgid, SIGTERM), escalating toSIGKILLafter the grace period, so dev servers do not leave orphaned workers behind. - Detected ≠ owned. A process devcc launched is
ownership: "managed". Anything merely observed on the host is"detected"and can never be signalled by devcc. - Destructive actions are always confirmed:
git restore,git push,docker compose down, stop-all, clear-all-logs, and any config command markedconfirm. Discarding untracked files is refused outright. - Memory is bounded by construction. Each log source is a fixed-capacity ring buffer (5000 lines by default); nothing else retains log lines.
- Startup never blocks. The first frame renders before git, Docker, service detection or tool versions have answered; each populates asynchronously with its own timeout.
- Several projects can be open at once.
ctrl+popens another project without closing the current one; each keeps its processes and log capture alive in the background, only the visible project polls git/services/system, and the sidebar grows a PROJECTS block with per-project running counts. Closing a project (or quitting) stops what devcc started there, with a confirmation. - Starts are ordered.
dependsOnandreadinessturn "start all" into a wave-by-wave bring-up: each wave waits for the previous one to be ready. The ordering is a pure function (planStartOrder) with its own tests. - The merged log stream is bounded. Per-source views window straight into
that source's ring buffer; the merged view materialises at most 2,000 recent
lines on demand. Searching is plain substring, or a regex when the query
parses as one, so typing
(never breaks the view. - Crash recovery is opt-in. A process only comes back automatically if its
definition says
restartOnCrash, with exponential backoff and a hard attempt limit; a manual stop cancels any pending restart and clears the budget. - Switching project rebuilds the session, not the UI. The renderer, views and store survive; the process manager, service adapters, git service, logger and command registry are torn down and rebuilt for the new directory, and anything devcc had started is stopped first (with a confirmation).
- Dependency checks never poll. They reach the network, so they run only when you ask, and updating a package is confirmed because it rewrites the lockfile.
- Errors are never swallowed. They are normalised into
DevccError, written to.devcc/devcc.log, shown in the footer, and kept for inspection in Settings → Diagnostics.
Extension points
Add a command - register it once and it appears in the palette, with an optional confirmation gate:
import { type CommandDefinition } from "command-center"
const command: CommandDefinition = {
id: "deploy.preview",
title: "Deploy a preview",
category: "Deploy",
keywords: ["vercel", "ship"],
confirm: { title: "Deploy a preview?", message: "This publishes to your team." },
execute: async (context) => {
context.notify("info", "Deploying…")
await context.runScript("deploy:preview")
},
}CommandContext exposes the store, the process/service/git/log/system services,
and navigate, notify, confirm, prompt, runScript and quit.
Add a service adapter - implement ServiceAdapter and register it with the
ServiceManager. detect() decides whether the service is relevant at all, so
adapters cost nothing in projects that do not use them:
import { type ServiceAdapter } from "command-center"
export class TailscaleService implements ServiceAdapter {
readonly id = "tailscale"
readonly name = "Tailscale"
readonly category = "runtime" as const
async detect() {
return (await exec("tailscale", ["version"])).ok
}
async getStatus() {
const result = await exec("tailscale", ["status", "--json"])
return {
state: result.ok ? "running" : "stopped",
detail: result.ok ? "connected" : "not running",
checkedAt: new Date(),
}
}
}The same shape is how Azure, Vercel, GitHub, Kubernetes, remote SSH hosts or CI
pipelines would be added later - each is a detect() plus a getStatus(), with
optional start/stop/restart.
Add a view - extend views/View.ts, register it in App, and add an entry
to SIDEBAR_ITEMS.
Development
pnpm lint # eslint (flat config, type-checked rules)
pnpm typecheck # tsc --noEmit, strict
pnpm test # vitest - core logic, no terminal required
pnpm test:tui # end-to-end smoke test against OpenTUI's headless renderer
pnpm build # tsc -> dist/
pnpm verify # lint + typecheck + test + build
pnpm format # prettierpnpm test covers the non-UI core: package-manager detection, config parsing
and validation, the command registry, the log ring buffer, process state
transitions and real process lifecycles, service detection and adapter failure
handling, git porcelain parsing, the observable store, and the formatting
helpers.
pnpm test:tui boots the actual application against OpenTUI's supported test
renderer (Bun only) and drives it with synthetic keystrokes: navigation,
palette, help, starting/restarting/stopping a managed process, running a
package script, log capture, and clean shutdown. It asserts on behaviour rather
than on frame snapshots.
Known limitations
- The dashboard itself requires Bun (or Node ≥ 26.4 with
--experimental-ffi); this is an OpenTUI/FFI constraint, not a design choice. - Service adapters ship for Docker Compose, PostgreSQL, Redis, Ollama and HTTP/port services. Others (Kubernetes, Vercel, Azure, Tailscale, …) are designed for but not implemented.
- The Git panel covers status, staging, discard, commit, fetch, pull and push.
Branch switching, rebasing, merge-conflict resolution and diff viewing in the
UI are not implemented (
GitService.diff()exists but has no view yet). - Detected services are read-only: devcc will not start or stop something it did not launch, apart from Docker Compose.
docker compose down --volumesis available throughDockerService.down()but is deliberately not bound to a key.- Mouse support is whatever OpenTUI provides by default; the interface is designed and tested for the keyboard.
- Below roughly 60 columns the sidebar hides and columns are trimmed; the layout degrades rather than breaking, but very small terminals are cramped.
