@nice-code/commander
v0.89.0
Published
Local process-group daemon with a web UI — one typed config, one control surface, for dev environments.
Readme
@nice-code/commander
Your dev environment, declared once: one typed commander.config.ts, one daemon, and one control
surface shared by a CLI and a live web UI. The replacement for ten terminal tabs, a tmux script, or
a concurrently line that has grown a comment explaining itself.
Docs: nicecode.io
bun add -d @nice-code/commander// commander.config.ts
import { defineCommanderConfig } from "@nice-code/commander/config";
export default defineCommanderConfig({
name: "my-app",
processes: [
{
id: "api",
run: ["bun", "run", "dev"],
cwd: "./services/api",
endpoints: [{ name: "http", protocol: "http", port: 8787, ownership: "exclusive" }],
ready: { kind: "endpoint", endpoint: "http" },
},
{ id: "web", run: ["bun", "run", "dev"], cwd: "./apps/web", dependsOn: ["api"] },
{ id: "types", kind: "task", run: ["bunx", "tsc", "--noEmit"], timeoutMs: 120_000 },
],
groups: { dev: { include: ["web"] } },
defaultSelection: ["dev"],
});bunx nice-commander up # `dev` → starts api first, waits for it to be READY, then web
bunx nice-commander status # live table
bunx nice-commander ui # the browser view
bunx nice-commander down dev⚠ The binary is
nice-commander— always the full name.bunx commanderfetches a different, extremely popular npm package (an argument parser), and it will not error usefully; it will just quietly not be this tool. If you want something shorter in a script, aliasnice-commander, nevercommander.
Every acting command takes the same selector — ids, group names, or tag=value terms (AND across
terms). up --dry-run and explain show exactly what would be touched, without touching it.
What it does that a script does not
dependsOnwaits for ready, not for spawned — a real readiness contract (port, HTTP, log marker), sowebnever starts against an API that is still compiling.- Stopping ends the tree. Killing
npm run dev's pid leaves the actual server holding the port; a POSIX process group / Windows Job Object does not. - It never kills a stranger. An occupied exclusive port is reported with its holder. Only a tree this daemon owns and has a ledger record for is reaped.
- A broken config keeps the last good one live. Nothing restarts silently; changed processes are marked stale until you restart them.
- Merged logs that actually order. The daemon stamps every line at receipt, so the timeline is correct across processes and across daemon restarts — and search runs server-side over the on-disk history, not over what your browser happens to be holding.
Node ≥ 22 or Bun. The web UI is pre-built into the package; Vite and React are not installed on
your machine. Built on @nice-code/process.
Developing the web UI (bun run dev)
Editing the UI needs a fast loop, so the package ships a dev harness: bun run dev (from
packages/nice-commander) serves the client source with Vite hot reload and proxies its
auth/websocket lanes to a throwaway daemon running the dev/showroom.config.ts demo. Open the
printed URL, edit src/client/**, and watch it hot-reload live against a real backend.
NICE_COMMANDER_DEV_PORTchanges the vite port (default 5196). The daemon runs on its ownshowroomdata dir and port range, so it never touches your real environment.NICE_COMMANDER_DEV_OPEN=0skips auto-opening a browser (headless runs).- Ctrl+C (or closing the vite server) stops the showroom daemon and its processes too.
- The demo needs
build/cli.mjs; the harness runsbun run build:serveritself if it is missing.
Security posture
Loopback only. The browser takes a capability from a same-origin bootstrap and keeps it in memory —
never in a URL, a cookie, or localStorage — and burns a single-use ticket per socket. Process
output is rendered as text, always. Configured env values never reach the realm, the ledger, or a
log.
