Files
fusion/scripts/dev-hmr.mjs
gsxdsm 9e4a0817db feat: restart the development engine on source changes (#3329)
## Summary

Add an opt-in source-development loop that restarts the dashboard and
engine when runtime TypeScript or JSON changes. Use `pnpm dev:watch`;
`pnpm dev:hmr` now combines Vite UI HMR with the same supervised
API/engine restart path.

The watcher filters tests, fixtures, generated declarations, build
output, and task state. It coalesces bursts with a two-second maximum
wait, waits for the child to acknowledge its IPC listener, and rebuilds
runtime dist artifacts before a source-triggered respawn.

## Safety model

- Close scheduler, triage, heartbeat, mission, routine, self-healing,
and merge admission before checking for active work.
- Let already-running agents reach a safe boundary; do not mutate
durable pause settings.
- Enter the existing graceful exit-code-86 shutdown and supervised
respawn path.
- Retry failed liveness reads and declined restart requests instead of
dropping the pending change.
- Keep ordinary `pnpm dev` behavior unchanged; inherited watch state
does not break nested non-dashboard development commands.

A development restart intentionally replaces the dashboard process, so
transient dashboard connections and project dev-server children
reconnect or restart with it. Agent work is the protected boundary.

## Validation

- `pnpm lint`
- `pnpm test:gate` (753 tests passed across engine, core, PostgreSQL
gate, and CI-shape suites)
- Focused CLI watcher/restart/supervision suites: 40 tests passed
- Focused engine drain/manager suites: 52 tests passed
- `pnpm --filter @runfusion/fusion typecheck`
- `pnpm --filter @fusion/engine typecheck`
- `pnpm verify:fast` (13 steps passed, including CLI build and real
health boot smoke)
- Manual unsupported-command probe confirms explicit `--watch` fails
clearly outside the dashboard command

## Post-Deploy Monitoring & Validation

- Watch for `[fusion:dev] source changed`, `source restart deferred`,
`active work drained`, and `restart requested` logs during the first
watched development session.
- Healthy behavior is one exit-86 respawn per edit batch, no interrupted
active agents, refreshed dist artifacts, and a healthy dashboard after
respawn.
- Investigate repeated restart loops, watcher attachment warnings,
declined restart retries, or liveness-read failures.
- Immediate mitigation is to use ordinary `pnpm dev` without `--watch`;
no production runtime behavior or durable setting needs rollback.
- Validation owner: Fusion maintainers during the first source edit
after merge.

---

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added `pnpm dev:watch` to automatically restart development runtime
processes when source files change.
* Development restarts now wait for active work to finish, preventing
new work from starting during the transition.
* Enhanced `pnpm dev:hmr` with graceful runtime source restarts while
keeping the dashboard available.
  * Rapid source changes are grouped to avoid unnecessary restarts.

* **Documentation**
* Updated development setup and contribution guides with the new watch
workflow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-04 08:57:30 -07:00

109 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Runs the Fusion dashboard API server AND `vite dev` concurrently so the
* React SPA in packages/dashboard/app hot-reloads while still talking to a
* live API/WebSocket backend.
*
* Two processes:
* 1. API: `pnpm dev --prebuild=none dashboard --no-auth --port <API_PORT>`
* (source-mode API/engine; Vite serves the browser UI)
* 2. Vite: `vite dev` in packages/dashboard
* (serves app/ with HMR; proxies /api and WS to the API)
*
* Open the URL Vite prints (e.g. http://localhost:5173), NOT the API URL.
* Edits to packages/dashboard/app/** hot-reload in Vite. Runtime source edits
* gracefully restart the API/engine child while Vite stays available.
*
* Env:
* FUSION_API_PORT API port (default 4050). Vite's proxy reads the same
* var so both sides stay in sync.
* FUSION_VITE_PORT Vite dev port (default 5173).
*/
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
const dashboardDir = resolve(repoRoot, "packages/dashboard");
const API_PORT = globalThis.process.env.FUSION_API_PORT ?? "4050";
const VITE_PORT = globalThis.process.env.FUSION_VITE_PORT ?? "5173";
const children = [];
let shuttingDown = false;
function prefix(label, color) {
const tag = `\x1b[${color}m[${label}]\x1b[0m`;
return (chunk) => {
const text = chunk.toString();
// Preserve trailing-newline semantics; prefix every non-empty line.
const lines = text.split("\n");
const last = lines.pop();
const prefixed = lines.map((l) => `${tag} ${l}`).join("\n");
globalThis.process.stdout.write(prefixed + (prefixed ? "\n" : "") + (last ? `${tag} ${last}` : ""));
};
}
function launch(name, color, command, args, options) {
const child = spawn(command, args, {
stdio: ["inherit", "pipe", "pipe"],
shell: true,
...options,
});
child.stdout.on("data", prefix(name, color));
child.stderr.on("data", prefix(name, color));
child.on("exit", (code, signal) => {
if (shuttingDown) return;
console.log(`\n[dev-hmr] ${name} exited (code=${code} signal=${signal}) — tearing down`);
shutdown(code ?? 1);
});
children.push({ name, child });
return child;
}
function shutdown(exitCode = 0) {
if (shuttingDown) return;
shuttingDown = true;
for (const { child } of children) {
if (!child.killed) {
try { child.kill("SIGINT"); } catch { void 0; }
}
}
// Hard kill after 5s if anyone is still alive.
setTimeout(() => {
for (const { child } of children) {
if (!child.killed) {
try { child.kill("SIGKILL"); } catch { void 0; }
}
}
globalThis.process.exit(exitCode);
}, 5000).unref();
}
globalThis.process.on("SIGINT", () => shutdown(0));
globalThis.process.on("SIGTERM", () => shutdown(0));
console.log(`[dev-hmr] starting API on :${API_PORT} + vite on :${VITE_PORT}`);
console.log(`[dev-hmr] open http://localhost:${VITE_PORT} for HMR (not the API URL)`);
// API: green. Vite owns the browser UI in this mode, so skip the dashboard
// client prebuild and run the API/engine directly from source.
launch(
"api",
"32",
"pnpm",
["dev", "--watch", "--prebuild=none", "dashboard", "--no-auth", "--port", API_PORT, "--host", "127.0.0.1"],
{ cwd: repoRoot, env: { ...globalThis.process.env, FUSION_API_PORT: API_PORT } },
);
// Vite: cyan. Starts once; proxies /api (including WS) to the API port.
launch(
"vite",
"36",
"pnpm",
["exec", "vite", "dev", "--port", VITE_PORT],
{ cwd: dashboardDir, env: { ...globalThis.process.env, FUSION_API_PORT: API_PORT } },
);