From cda1510e93c3caa3a3ab9971dfe68a26a18c9166 Mon Sep 17 00:00:00 2001 From: Konsti Wohlwend Date: Tue, 2 Jun 2026 17:50:22 -0700 Subject: [PATCH] Make demo dev resilient to dashboard build failures (#1542) --- examples/demo/package.json | 2 +- examples/demo/scripts/dev-with-retry.mjs | 119 +++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 examples/demo/scripts/dev-with-retry.mjs diff --git a/examples/demo/package.json b/examples/demo/package.json index 8d40b0264..24c769a66 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -7,7 +7,7 @@ "scripts": { "typecheck": "tsc --noEmit", "clean": "rimraf .next && rimraf node_modules", - "dev": "NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}42 pnpm -w run cli -- dev --config-file=./hexclave.config.ts -- pnpm --dir examples/demo run dev:inner", + "dev": "NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}42 node scripts/dev-with-retry.mjs", "dev:inner": "next dev --turbopack --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}03", "build": "next build", "start": "next start --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}03", diff --git a/examples/demo/scripts/dev-with-retry.mjs b/examples/demo/scripts/dev-with-retry.mjs new file mode 100644 index 000000000..0b8fbc572 --- /dev/null +++ b/examples/demo/scripts/dev-with-retry.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +// Resilient wrapper for the demo dev command. +// +// The demo dev flow runs `pnpm -w run cli`, which internally builds the CLI +// package (and its dependency, dashboard build:rde-standalone). If that build +// fails, this process would normally exit, and because the root dev script uses +// `concurrently -k`, the entire dev server would die. +// +// This wrapper catches non-zero exits and watches for file changes in the +// dashboard and packages directories before retrying, so a transient build +// failure doesn't tear down the whole dev server. + +import { spawn } from "node:child_process"; +import { watch } from "node:fs"; +import { join, resolve } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; + +const scriptDir = import.meta.dirname; +const demoRoot = resolve(scriptDir, ".."); +const repoRoot = resolve(demoRoot, "../.."); + +const LOG_PREFIX = "[Hexclave dev-retry] "; +const RETRY_DEBOUNCE_MS = 2_000; + +function log(message) { + console.error(`${LOG_PREFIX}${message}`); +} + +function runCliDev() { + return new Promise((resolvePromise, reject) => { + const child = spawn("pnpm", [ + "-w", "run", "cli", "--", + "dev", + "--config-file=./hexclave.config.ts", + "--", + "pnpm", "--dir", "examples/demo", "run", "dev:inner", + ], { + stdio: "inherit", + env: process.env, + }); + + let signalled = false; + + const forwardSigint = () => { signalled = true; child.kill("SIGINT"); }; + const forwardSigterm = () => { signalled = true; child.kill("SIGTERM"); }; + process.on("SIGINT", forwardSigint); + process.on("SIGTERM", forwardSigterm); + + child.on("close", (code) => { + process.off("SIGINT", forwardSigint); + process.off("SIGTERM", forwardSigterm); + resolvePromise({ code: code ?? 1, signalled }); + }); + child.on("error", (err) => { + process.off("SIGINT", forwardSigint); + process.off("SIGTERM", forwardSigterm); + reject(err); + }); + }); +} + +function waitForFileChanges() { + return new Promise((resolvePromise) => { + const watchDirs = [ + join(repoRoot, "apps", "dashboard"), + join(repoRoot, "packages"), + ]; + const watchers = []; + let resolved = false; + + const done = () => { + if (resolved) return; + resolved = true; + for (const w of watchers) { + try { w.close(); } catch { /* ignore */ } + } + resolvePromise(); + }; + + for (const dir of watchDirs) { + try { + const w = watch(dir, { recursive: true }, done); + w.on("error", () => { /* ignore watch errors */ }); + watchers.push(w); + } catch { + // directory might not exist yet + } + } + + // Fallback: if no watchers could be set up, resolve after a timeout so we + // don't block forever. + if (watchers.length === 0) { + log("Could not set up file watchers. Will retry after a delay."); + setTimeout(done, 10_000); + } + }); +} + +async function main() { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + const { code, signalled } = await runCliDev(); + + if (signalled || code === 0) { + process.exit(code); + } + + log(`Dev command exited with code ${code}. Watching for file changes before retrying...`); + await waitForFileChanges(); + log(`Change detected. Retrying in ${RETRY_DEBOUNCE_MS / 1000}s...`); + await sleep(RETRY_DEBOUNCE_MS); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});