Config

braid.config.ts default-exports a { processes, plugins?, logs?, foreground?, statsPollIntervalMs? } object (or a bare ProcessConfig[], treated the same way). Full field list and defaults live in src/types.ts.

Basics

import { defineConfig } from "@aip-tech/braid";

export default defineConfig({
	processes: [
		{
			name: "api",
			command: "pnpm",
			args: ["--filter", "./api", "run", "dev"],
			cwd: "api",
			color: "blue",
			env: { NODE_ENV: "development" },
		},
	],
});
  • name — unique; used for log prefixes, the pidfile, and status output.
  • command/args — the process to run.
  • cwd — resolved relative to wherever the CLI was invoked; defaults to that same directory.
  • env — extra environment variables merged over process.env, for this process only.
  • color — ANSI color name used for this process's log prefix.

Watching and restarting

Set watch to restart a process whenever a matched path changes. Omit it for a command that manages its own reload (a dev server with its own HMR, for instance).

{
	name: "api",
	command: "pnpm",
	args: ["--filter", "./api", "run", "dev"],
	watch: ["api/src"],
	ext: "ts,graphql,json", // @default "ts,js,json" - comma-separated, only used when watch is set
},

Dependent restarts

A process can restart whenever another one does — e.g. a client that needs to regenerate its GraphQL SDK once the API it talks to restarts:

{ name: "api", command: "pnpm", args: ["--filter", "./api", "run", "dev"], watch: ["api/src"] },
{
	name: "client",
	command: "pnpm",
	args: ["--filter", "./client", "run", "dev"],
	dependsOn: {
		processes: ["api"],
		run: { command: "pnpm", args: ["--filter", "./client", "run", "generate"] },
	},
},

When api restarts: client stops once api has actually re-spawned (not the instant it decides to restart), run executes (retried on failure — retries/retryDelayMs, default 5× / 1s apart, since api may still be starting back up), then client restarts. Left stopped with a logged reason if run never succeeds. A dependsOn chain that loops back on its own trigger is rejected at startup. Multiple processes can depend on the same trigger — each cascades independently, in parallel.

Post-restart hooks

For a shared workspace package other processes just read from (no process of its own to restart), run a command directly after the process that changed it restarts:

{
	name: "api",
	command: "pnpm",
	args: ["--filter", "./api", "run", "dev"],
	watch: ["api/src"],
	onRestart: { command: "pnpm", args: ["--filter", "./types", "run", "generate"] },
},

Same shape and retry behavior as dependsOn.run, but scoped to api restarting itself — no dependent process is stopped or restarted. If api also has dependents (via dependsOn), they're notified only once this hook succeeds, and not at all if it never does.

Pre-restart hooks

For a process that needs to regenerate its own on-disk dependencies (e.g. a GraphQL SDK generated from a schema it also watches) before it restarts on that same change, beforeRestart runs after the process is stopped and before a fresh one starts:

{
	name: "client",
	command: "pnpm",
	args: ["vike", "dev"],
	watch: ["server", "lib/sdk"],
	beforeRestart: { command: "pnpm", args: ["--filter", "lib/sdk", "run", "generate"] },
},

client is fully stopped, generate runs to completion (retried like onRestart/dependsOn.run), and only then does client restart — the fresh process never boots against stale or half-regenerated code. Requires watch; rejected at startup otherwise. If the hook keeps failing past its retries, client is left stopped, but the watcher stays active — the next matching file change retries the whole cycle, rather than requiring a manual restart.

Waiting for readiness

Re-spawning a process isn't the same as it being ready — an API might take a moment after restarting before it's actually serving. readyPattern holds off onRestart and any dependsOn cascades until a regex matches that process's own stdout/stderr:

{
	name: "api",
	command: "pnpm",
	args: ["--filter", "./api", "run", "dev"],
	watch: ["api/src"],
	readyPattern: "Server listening",
	readyTimeoutMs: 15000, // @default 10000
},

Without readyPattern, dependents are held off only until api has re-spawned (not exited/killed while restarting, but not necessarily done starting up either) — a run/onRestart hook's own retry is what bridges the rest of that gap. If readyPattern never matches within readyTimeoutMs, braid logs why (in api's own log) and proceeds anyway, rather than holding dependents off forever on a misconfigured pattern.

Logs

Each process gets a rotated log file at .braid/logs/<name>.log. Rotated (one backup kept) on every start, on a watch-triggered restart, and past logs.maxSizeBytes (default 5MB, set at the top level of the config, not per-process):

export default defineConfig({
	processes: [ /* ... */ ],
	logs: {
		dir: ".braid/logs",   // @default ".braid/logs"
		maxSizeBytes: 5242880, // @default 5MB
		timestamps: false,    // @default false
	},
});

Braid's own diagnostics (plugin failures, crash notices) go to .braid/daemon.log; a dependsOn/onRestart/beforeRestart hook that keeps failing, or a readyPattern that never matches, is also logged into the relevant process's own log — visible via braid logs/--follow, not just daemon.log.

logs.timestamps prepends a dimmed HH:MM:SS.mmm to every line, before the [name] tag. It's the same bytes wherever that line shows up — a process's log file, the web UI's log view, and the terminal during braid start all get it together, there's no way to enable it in just one place.

Foreground mode

start forks a background daemon by default. Set foreground: true at the top level of the config to make it run attached to the terminal instead — it then blocks, streaming every process's combined output there, until Ctrl-C (or a braid stop from another terminal) stops everything:

export default defineConfig({
	processes: [ /* ... */ ],
	foreground: true, // @default false
});

Override this per invocation with braid start --foreground or braid start --daemon, regardless of what the config says.

Process stats

Each process's CPU (percent of one core) and memory (RSS) usage is sampled on a timer via pidusage, surfaced through PluginContext.getProcesses() (so GET /api/status and @aip-tech/braid-plugin-ui's dashboard get it for free) and in braid status's own output once the daemon has sampled it. Adjust the polling cadence at the top level of the config:

export default defineConfig({
	processes: [ /* ... */ ],
	statsPollIntervalMs: 2000, // @default 2000
});

A process that's stopped (or hasn't been sampled yet) simply has no cpu/memory fields, rather than stale or zeroed values.