pcbjam/scripts/deploy/dev-demo.mjs

252 lines
12 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
// Run the GPL standalone editor LOCALLY in "demo mode" — the no-backend
// configuration of demo.pcbjam.com, but served by the Vite dev server so you can
// exercise a local WASM build against the live R2 CDN. This is the dev sibling of
// scripts/deploy/build-demo.mjs (which produces the static dist/ for deploy).
//
// node scripts/deploy/dev-demo.mjs [--lib-tag 10.0.3] [--cdn https://cdn.pcbjam.com]
// [--wasm local|r2] [--wasm-tag <tag>]
// [--content-tag <tag>] [--port 5173]
//
// "Demo mode" = R2 is the only remote backend; there is NO REST API and NO
// partykit collab server:
// - LIBRARIES come from the versioned R2 CDN (VITE_LIBS_SOURCE=cdn), read-only
// and IDB-cached. This is the path the lazy/fat lib-load work targets.
// - COLLAB is broadcastchannel (cross-tab only, no server) instead of partykit.
// - PROJECTS: the static gallery (content/<tag>/) is only used when --content-tag
// is given AND that tag is deployed. Otherwise projects are local-folder loads
// persisted to a browser-local (IndexedDB) store — no backend needed.
// - WASM defaults to the LOCAL freshly-built artifacts (served same-origin at
// /wasm via scripts/link-wasm.mjs), so you test the editor you just built.
// Pass --wasm r2 to pull the editor binaries from the live CDN instead.
//
// Note (2026-06-25): on the live CDN, libs/kicad/10.0.3/ is deployed but the
// content gallery (content/<tag>/) and a real wasm manifest are NOT — so the
// useful default is "live R2 libs + local WASM + local/IDB projects".
import { execFileSync, spawn } from "node:child_process";
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
// repoRoot is two levels up from scripts/deploy/, independent of cwd.
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
// Build the read-only example gallery (deploy/demo/gallery.json → the same
// "Demo Board" the live demo.pcbjam.com ships) into a local CDN layout and
// symlink it under the standalone's public/ so Vite serves it same-origin at
// /content/<tag>/. Mirrors what publish-content.mjs ships to the R2 CDN, but
// needs no deploy. Returns the (relative, same-origin) manifest URL.
function buildLocalGallery(repoRoot, tag) {
const out = join(repoRoot, "web/standalone/.demo-cdn");
rmSync(out, { recursive: true, force: true });
mkdirSync(out, { recursive: true });
execFileSync(
"node",
[
join(repoRoot, "scripts/deploy/publish-content.mjs"),
"--tag", tag,
"--driver", "local",
"--out", out,
],
{ cwd: repoRoot, stdio: "inherit" },
);
// Serve <out>/content same-origin at /content via a public/ symlink (like
// link-wasm does for /wasm). No bytes are copied into the bundle.
const link = join(repoRoot, "web/standalone/public/content");
try {
if (lstatSync(link)) rmSync(link, { recursive: true, force: true });
} catch {
/* no existing link */
}
symlinkSync(join(out, "content"), link);
return `/content/${tag}/manifest.json`;
}
function parseArgs(argv) {
const a = {
cdn: "https://cdn.pcbjam.com",
libTag: "10.0.3", // live KiCad library snapshot on R2
wasm: "local", // local | r2
wasmTag: null, // manifest-<tag>.json when --wasm r2 (default: "latest")
contentTag: null, // use the live CDN gallery for this tag (else build locally)
galleryTag: "demo-local", // path tag for the locally-built gallery
noGallery: false, // disable the example gallery (local-folder + IDB only)
modelsTag: null, // 3D models snapshot tag (live CDN, or the local dir's tag)
modelsLocal: null, // local publish-models --driver local output dir (serve same-origin)
feat(libs): eeschema symbol-chooser footprint selector + preview via publish-time fp-index The merged kicad_editor bundle made the chooser's footprint side reachable from a schematic session; this wires up the data: - boot.ts/constants.ts: every kicad_editor frame seeds BOTH sym-lib-table and fp-lib-table (+ placeholder files; a created user lib joins both lists) — the eeschema frame used to write fp-lib-table empty, leaving the selector dead. TOOL_LIB_KIND remains only the presync/primary-kind lever. - publish-libs.ts + kicad-pretty.ts: publish fp-index.json per tag — [name, uniquePadCount] per footprint (countUniquePads mirrors KiCad's GetUniquePadCount(DO_NOT_INCLUDE_NPTH)); index-only top-up mode for already-published immutable tags. - source.ts/cdn-source.ts: new bridge op "index" (source-global, dispatched before the lib-id parse) + LibsSource.getFpIndex; the CDN source fetches <tag>/fp-index.json once (404 ⇒ null ⇒ C++ default-only fallback). - dev-demo.mjs: --libs-local serves a local publish-libs layout same-origin at /libs-cdn (mirrors --models-local). - tests/web/eeschema-fp-selector.spec.ts: e2e — chooser opens in --frame=sch, selector fills from ONE index crossing, clicking a row per-item-gets the body and the cross-face GAL preview renders; adaptive for index-less sources (asserts crash-free default-only selector). Submodule bumps: kicad (index-backed filterFootprints + preview AsyncLoad fix + modal-pump crash guard), wxwidgets (modal pump logs e.stack). Doc: pcbjam-private docs/features/libs/0014-eeschema-footprint-selector.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SCWVaCRM9T847PdPwYYajX
2026-07-06 13:39:21 +02:00
libsLocal: null, // local publish-libs --driver local output dir (serve same-origin)
port: null,
repo: "https://github.com/PCBJam/pcbjam",
};
for (let i = 2; i < argv.length; i++) {
const next = () => argv[++i];
switch (argv[i]) {
case "--cdn": a.cdn = next(); break;
case "--lib-tag": a.libTag = next(); break;
case "--wasm": a.wasm = next(); break;
case "--wasm-tag": a.wasmTag = next(); break;
case "--content-tag": a.contentTag = next(); break;
case "--gallery-tag": a.galleryTag = next(); break;
case "--no-gallery": a.noGallery = true; break;
case "--port": a.port = next(); break;
case "--repo": a.repo = next(); break;
case "--models-tag": a.modelsTag = next(); break;
case "--models-local": a.modelsLocal = next(); break;
feat(libs): eeschema symbol-chooser footprint selector + preview via publish-time fp-index The merged kicad_editor bundle made the chooser's footprint side reachable from a schematic session; this wires up the data: - boot.ts/constants.ts: every kicad_editor frame seeds BOTH sym-lib-table and fp-lib-table (+ placeholder files; a created user lib joins both lists) — the eeschema frame used to write fp-lib-table empty, leaving the selector dead. TOOL_LIB_KIND remains only the presync/primary-kind lever. - publish-libs.ts + kicad-pretty.ts: publish fp-index.json per tag — [name, uniquePadCount] per footprint (countUniquePads mirrors KiCad's GetUniquePadCount(DO_NOT_INCLUDE_NPTH)); index-only top-up mode for already-published immutable tags. - source.ts/cdn-source.ts: new bridge op "index" (source-global, dispatched before the lib-id parse) + LibsSource.getFpIndex; the CDN source fetches <tag>/fp-index.json once (404 ⇒ null ⇒ C++ default-only fallback). - dev-demo.mjs: --libs-local serves a local publish-libs layout same-origin at /libs-cdn (mirrors --models-local). - tests/web/eeschema-fp-selector.spec.ts: e2e — chooser opens in --frame=sch, selector fills from ONE index crossing, clicking a row per-item-gets the body and the cross-face GAL preview renders; adaptive for index-less sources (asserts crash-free default-only selector). Submodule bumps: kicad (index-backed filterFootprints + preview AsyncLoad fix + modal-pump crash guard), wxwidgets (modal pump logs e.stack). Doc: pcbjam-private docs/features/libs/0014-eeschema-footprint-selector.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SCWVaCRM9T847PdPwYYajX
2026-07-06 13:39:21 +02:00
case "--libs-local": a.libsLocal = next(); break;
case "-h": case "--help": a.help = true; break;
default: throw new Error(`unknown arg: ${argv[i]}`);
}
}
a.cdn = a.cdn.replace(/\/+$/, "");
if (a.wasm !== "local" && a.wasm !== "r2")
throw new Error(`--wasm must be "local" or "r2" (got "${a.wasm}")`);
return a;
}
const HELP = `dev-demo.mjs — run the standalone locally in demo mode (R2-only backend)
node scripts/deploy/dev-demo.mjs [options]
--lib-tag <tag> KiCad lib snapshot on the CDN (default 10.0.3; "" disables offline example libs)
--cdn <url> CDN origin (default https://cdn.pcbjam.com)
--wasm local|r2 editor binaries: local fresh build (default) or live CDN
--wasm-tag <tag> wasm manifest tag for --wasm r2 (default "latest" manifest-latest.json)
--content-tag <tag> pin the LIVE CDN gallery for this release tag (default: build+serve the gallery locally)
--gallery-tag <tag> path tag for the locally-built gallery (default demo-local)
--no-gallery disable the example gallery (local-folder + IDB projects only)
--models-tag <tag> enable lazy 3D models from the CDN snapshot at this tag
--models-local <dir> serve a local publish-models layout (--driver local --compress none)
same-origin instead of the CDN (requires --models-tag)
feat(libs): eeschema symbol-chooser footprint selector + preview via publish-time fp-index The merged kicad_editor bundle made the chooser's footprint side reachable from a schematic session; this wires up the data: - boot.ts/constants.ts: every kicad_editor frame seeds BOTH sym-lib-table and fp-lib-table (+ placeholder files; a created user lib joins both lists) — the eeschema frame used to write fp-lib-table empty, leaving the selector dead. TOOL_LIB_KIND remains only the presync/primary-kind lever. - publish-libs.ts + kicad-pretty.ts: publish fp-index.json per tag — [name, uniquePadCount] per footprint (countUniquePads mirrors KiCad's GetUniquePadCount(DO_NOT_INCLUDE_NPTH)); index-only top-up mode for already-published immutable tags. - source.ts/cdn-source.ts: new bridge op "index" (source-global, dispatched before the lib-id parse) + LibsSource.getFpIndex; the CDN source fetches <tag>/fp-index.json once (404 ⇒ null ⇒ C++ default-only fallback). - dev-demo.mjs: --libs-local serves a local publish-libs layout same-origin at /libs-cdn (mirrors --models-local). - tests/web/eeschema-fp-selector.spec.ts: e2e — chooser opens in --frame=sch, selector fills from ONE index crossing, clicking a row per-item-gets the body and the cross-face GAL preview renders; adaptive for index-less sources (asserts crash-free default-only selector). Submodule bumps: kicad (index-backed filterFootprints + preview AsyncLoad fix + modal-pump crash guard), wxwidgets (modal pump logs e.stack). Doc: pcbjam-private docs/features/libs/0014-eeschema-footprint-selector.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SCWVaCRM9T847PdPwYYajX
2026-07-06 13:39:21 +02:00
--libs-local <dir> serve a local publish-libs layout (--driver local) same-origin
instead of the CDN (uses --lib-tag as the snapshot tag)
--port <n> dev server port
By default the read-only example gallery (deploy/demo/gallery.json) is built
locally and served same-origin at /content/<gallery-tag>/ the home page shows
the "Demo Board" example, opened read-only (Save downloads to local).
`;
function gitSha(cwd) {
try {
return execFileSync("git", ["rev-parse", "HEAD"], { cwd }).toString().trim();
} catch {
return "";
}
}
function main() {
const a = parseArgs(process.argv);
if (a.help) {
process.stdout.write(HELP);
return;
}
const repoRoot = REPO_ROOT;
const env = { ...process.env };
// --- Libraries: live R2 CDN (the lazy/fat lib-load path), or offline examples.
feat(libs): eeschema symbol-chooser footprint selector + preview via publish-time fp-index The merged kicad_editor bundle made the chooser's footprint side reachable from a schematic session; this wires up the data: - boot.ts/constants.ts: every kicad_editor frame seeds BOTH sym-lib-table and fp-lib-table (+ placeholder files; a created user lib joins both lists) — the eeschema frame used to write fp-lib-table empty, leaving the selector dead. TOOL_LIB_KIND remains only the presync/primary-kind lever. - publish-libs.ts + kicad-pretty.ts: publish fp-index.json per tag — [name, uniquePadCount] per footprint (countUniquePads mirrors KiCad's GetUniquePadCount(DO_NOT_INCLUDE_NPTH)); index-only top-up mode for already-published immutable tags. - source.ts/cdn-source.ts: new bridge op "index" (source-global, dispatched before the lib-id parse) + LibsSource.getFpIndex; the CDN source fetches <tag>/fp-index.json once (404 ⇒ null ⇒ C++ default-only fallback). - dev-demo.mjs: --libs-local serves a local publish-libs layout same-origin at /libs-cdn (mirrors --models-local). - tests/web/eeschema-fp-selector.spec.ts: e2e — chooser opens in --frame=sch, selector fills from ONE index crossing, clicking a row per-item-gets the body and the cross-face GAL preview renders; adaptive for index-less sources (asserts crash-free default-only selector). Submodule bumps: kicad (index-backed filterFootprints + preview AsyncLoad fix + modal-pump crash guard), wxwidgets (modal pump logs e.stack). Doc: pcbjam-private docs/features/libs/0014-eeschema-footprint-selector.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SCWVaCRM9T847PdPwYYajX
2026-07-06 13:39:21 +02:00
// --libs-local <publish-libs --out dir> serves that layout same-origin at
// /libs-cdn via a public/ symlink (mirrors --models-local) — for testing
// an unpublished snapshot, e.g. one with a fresh fp-index.json.
if (a.libTag && a.libsLocal) {
const link = join(repoRoot, "web/standalone/public/libs-cdn");
try {
if (lstatSync(link)) rmSync(link, { recursive: true, force: true });
} catch {
/* no existing link */
}
symlinkSync(resolve(a.libsLocal, "libs/kicad"), link);
env.VITE_LIBS_SOURCE = "cdn";
env.VITE_LIBS_MANIFEST_URL = `/libs-cdn/${a.libTag}/manifest.json`;
} else if (a.libTag) {
env.VITE_LIBS_SOURCE = "cdn";
env.VITE_LIBS_MANIFEST_URL = `${a.cdn}/libs/kicad/${a.libTag}/manifest.json`;
} else {
env.VITE_LIBS_SOURCE = "static";
delete env.VITE_LIBS_MANIFEST_URL;
}
// --- 3D models: lazy per-board bodies (docs/features/3d-models). Off unless a
// tag is given. --models-local <publish-models --out dir> serves that
// layout same-origin at /models-cdn via a public/ symlink (publish it with
// --compress none — the dev server can't send Content-Encoding: br);
// otherwise the live CDN snapshot for --models-tag is used.
if (a.modelsTag && a.modelsLocal) {
const link = join(repoRoot, "web/standalone/public/models-cdn");
try {
if (lstatSync(link)) rmSync(link, { recursive: true, force: true });
} catch {
/* no existing link */
}
symlinkSync(resolve(a.modelsLocal, "libs/kicad-models"), link);
env.VITE_MODELS_MANIFEST_URL = `/models-cdn/${a.modelsTag}/manifest.json`;
} else if (a.modelsTag) {
env.VITE_MODELS_MANIFEST_URL = `${a.cdn}/libs/kicad-models/${a.modelsTag}/manifest.json`;
} else {
delete env.VITE_MODELS_MANIFEST_URL;
}
// --- No backend: collab is cross-tab only, document bytes are local (api path),
// loaded folders persist to a browser-local IndexedDB project.
env.VITE_YJS_PROVIDER = "broadcastchannel";
delete env.VITE_YJS_ENDPOINT;
delete env.VITE_YJS_TOKEN;
env.VITE_DOC_SOURCE = "api";
env.VITE_LOCAL_PROJECTS = "idb";
feat(editor): report uncaught errors to Better Stack The editor reported nothing when a session died. Evidence lived only in-tab — an 800-line React array behind a "Show console" button — so diagnosis meant asking a user to paste a screenshot. Better Stack's Error Tracking ingests the Sentry wire protocol, so this runs the stock @sentry/browser against a Better Stack DSN. Sentry.init installs its own window error/unhandledrejection handlers, so uncaught main-thread errors and the wasm traps that escape emscripten's DOM event handlers are captured with no instrumentation at the throw sites. Not their JS tag: it has no beforeSend or fingerprint hooks, its runtime spawns workers from cross-origin CDN hosts (this page is COEP: require-corp), and it ships session replay on by default — which on a CAD canvas records customers' board geometry. @sentry/browser is imported in exactly one file so the vendor stays swappable, mirroring how lib/analytics.ts isolates Plausible. Also replaces the terminal-signature regex with a shared, unit-tested predicate (wasm/terminal-error.ts) used by BOTH the fatal overlay and the reporter, so they cannot disagree. The regex was a type check written as a string match and had three live holes: `RuntimeError` was listed but never appears IN `.message`; Chrome's bare "unreachable" and "null function" matched nothing (the v0.1.20 prod log is exactly those); and narrowing "table index is out of bounds" to `\bindex out of bounds` for Firefox in 197f317 silently stopped matching Chrome's spelling. Checking the TYPE — every trap in this family is a WebAssembly.RuntimeError — covers all engines and ends the spelling chase; the message patterns remain as a fallback for paths that lose the Error object, such as a worker ErrorEvent crossing the realm boundary with error: null. 197f317's pthread-worker tap, promote() and Firefox findings are kept as-is. Notes: - Off unless VITE_ERRORS_DSN is set AND VITE_ALLOW_USER_OVERRIDE !== "1" (dev servers and every Playwright harness set the latter, and production builds never do), so a production DSN in a local .env still cannot report. With no DSN the whole SDK is const-folded out: 1,193,080 vs 1,282,463 bytes of JS. - browserApiErrors integration removed. It wraps setTimeout/rAF/addEventListener in try/catch, which is exactly how KiCad-on-Emscripten drives its main loop. - Console breadcrumbs off (collab/debug.ts's clog fires per Yjs update and would evict the ring before any crash); dom/fetch/navigation breadcrumbs kept. - beforeSend redacts token/apiKey/Bearer — collab/provider.ts puts the collab token in the y-partyserver URL, so a connection-failure string carries a live credential — and guards the cascade: one wedge produced 8 errors in prod, and after the first terminal event the rest are dropped into cascade_count. Verified end to end against the real EU host from a cross-origin-isolated page: POST /api/<id>/envelope/ -> 200, and 4 terminal throws produce 1 event (control: 1 throw, same count). Privacy policy 9, cookie policy 6 and the licenses page are updated: Better Stack is disclosed as an EU processor, and the licenses page now describes the browser app's own JS dependencies, which it never did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:24:30 +02:00
// --- Never report errors from a local demo run, even if the developer has a
// production DSN sitting in their environment.
delete env.VITE_ERRORS_DSN;
// --- Projects: the read-only example gallery (the demo.pcbjam.com experience).
// Default: build it locally and serve it same-origin. --content-tag <tag>
// pins the live CDN gallery instead. --no-gallery falls back to local-folder
// + IDB only. Either way the REST base points at a dead host so the home
// page never waits on a backend.
if (a.noGallery) {
env.VITE_PROJECT_SOURCE = "remote";
delete env.VITE_PROJECT_MANIFEST_URL;
} else if (a.contentTag) {
env.VITE_PROJECT_SOURCE = "static";
env.VITE_PROJECT_MANIFEST_URL = `${a.cdn}/content/${a.contentTag}/manifest.json`;
} else {
env.VITE_PROJECT_SOURCE = "static";
env.VITE_PROJECT_MANIFEST_URL = buildLocalGallery(repoRoot, a.galleryTag);
}
env.VITE_API_BASE_URL = "http://offline.invalid"; // never resolves → local-folder loader
// --- WASM: local fresh build (served at /wasm) or the live CDN.
if (a.wasm === "r2") {
env.VITE_WASM_ROOT = `${a.cdn}/wasm`;
env.VITE_WASM_MANIFEST = `manifest-${a.wasmTag ?? "latest"}.json`;
} else {
env.VITE_WASM_ROOT = "/wasm";
delete env.VITE_WASM_MANIFEST;
delete env.VITE_WASM_ASSET_BASE_URL;
}
// --- Version badge identity (GPLv3 corresponding-source pointer).
env.VITE_APP_TAG = env.VITE_APP_TAG || (a.contentTag ?? "demo-local");
env.VITE_GIT_SHA = env.VITE_GIT_SHA || gitSha(repoRoot);
env.VITE_REPO_URL = a.repo;
const viteArgs = ["--dir", "web", "--filter", "@pcbjam/standalone", "dev"];
feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%) Move OpenCASCADE out of the merged editor image into occ_service: a separate emscripten module (-sASYNCIFY=0, MODULARIZE, in-container -Oz finalize, 2N+8 pre-warmed pthread pool) booted lazily in a dedicated Web Worker on the first STEP export or STEP/IGES model parse. kicad_editor.wasm ~190 MB -> 130 MB; sessions that never touch OCC never fetch its 57 MB. STEP export works in the browser for the first time: the unchanged desktop dialog runs EXPORTER_STEP, whose wasm shadow suspends into globalThis.occService and the export bytes go straight to a browser download (never entering the editor heap). STEP/IGES 3D models parse in the worker via the oce shadow (S3D WriteCache/ReadCache wire). - wasm/occ-service/: service CMake target (hooked from the kicad fork's top-level CMakeLists, wasm/editor pattern), embind entry (occExport/occLoadModel), wxConfig pre-js. - wasm/stubs/{exporter_step,oce_plugin}_stub.cpp: EM_ASYNC_JS worker bridges (callee-shadowing; no caller #ifdefs). - web/standalone: provider installed whenever the kicad_editor bundle boots (cross-face safe); ONE shared worker-boot source occ-worker.js (vite ?raw; the e2e stub reads the same file) — blob worker with locateFile absolutized against the glue URL; export download-name guard. - deps: OCC builds with RapidJSON so its glTF/GLB writer exists — pinned to the vcpkg master snapshot 2025-02-26 (24b5e7a8b27f), the same code official KiCad consumes via vcpkg.json's opencascade[rapidjson]; rapidjson's latest tag (v1.1.0, 2016) is ill-formed under modern clang. - tests: occ-export dialog e2e (lazy-fetch boundary + STEP download bytes), occ-probe incl. a 9-format matrix (step/stpz/brep/xao/ply/stl/glb/u3d/pdf), 3d-viewer-models hard-asserts the worker parse; occ provider stub installed ambiently by the kicad fixtures. Validated against desktop kicad-cli 10.0.4: geometric exact equality (bbox delta 0 um, volume delta 0.0000%) for STEP/GLB/STL/BREP/STPZ across three boards and option sweeps — with desktop OCC 7.9 vs wasm OCC 7.8; PLY/XAO/PDF structurally equal; U3D same-size (quantizer float LSBs differ). Full kicad e2e green on Firefox and Chromium; standalone verified end to end (lazy fetch only on the Export click; export.step 60,628 B ISO-10303-21; loadModel 700 KB STEP -> 569 KB scenegraph cache). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:39:58 +02:00
// No "--" separator: pnpm forwards script args verbatim, so a literal "--"
// reaches vite and makes it IGNORE the flags after it ("vite -- --port N"
// starts on the default port). Appending directly yields "vite --port N".
if (a.port) viteArgs.push("--port", String(a.port));
console.log("dev-demo: standalone in demo mode (R2-only backend, no partykit)");
console.log(` VITE_LIBS_SOURCE=${env.VITE_LIBS_SOURCE}${env.VITE_LIBS_MANIFEST_URL ? ` (${env.VITE_LIBS_MANIFEST_URL})` : ""}`);
console.log(` VITE_WASM_ROOT=${env.VITE_WASM_ROOT}${env.VITE_WASM_MANIFEST ? ` (${env.VITE_WASM_MANIFEST})` : " (local build)"}`);
console.log(` VITE_PROJECT_SOURCE=${env.VITE_PROJECT_SOURCE}${env.VITE_PROJECT_MANIFEST_URL ? ` (${env.VITE_PROJECT_MANIFEST_URL})` : ""}`);
console.log(` VITE_MODELS_MANIFEST_URL=${env.VITE_MODELS_MANIFEST_URL ?? "(unset — 3D models off)"}`);
console.log(` VITE_YJS_PROVIDER=${env.VITE_YJS_PROVIDER} VITE_DOC_SOURCE=${env.VITE_DOC_SOURCE} VITE_LOCAL_PROJECTS=${env.VITE_LOCAL_PROJECTS}`);
const child = spawn("pnpm", viteArgs, { cwd: repoRoot, env, stdio: "inherit" });
child.on("exit", (code) => process.exit(code ?? 0));
process.on("SIGINT", () => child.kill("SIGINT"));
process.on("SIGTERM", () => child.kill("SIGTERM"));
}
main();