From da837fc352355d77341745f2a81dab72c8e43a78 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 22:41:59 +0000 Subject: [PATCH 1/5] Fix bundled dashboard module symlinks --- .../stack-cli/scripts/copy-runtime-assets.mjs | 27 +++++++++++- packages/stack-cli/src/commands/dev.ts | 44 ++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/stack-cli/scripts/copy-runtime-assets.mjs b/packages/stack-cli/scripts/copy-runtime-assets.mjs index 61d2fec56c..30e7102036 100644 --- a/packages/stack-cli/scripts/copy-runtime-assets.mjs +++ b/packages/stack-cli/scripts/copy-runtime-assets.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { execFileSync } from "child_process"; -import { chmodSync, cpSync, existsSync, mkdirSync, rmSync } from "fs"; +import { chmodSync, cpSync, existsSync, mkdirSync, readlinkSync, readdirSync, rmSync, writeFileSync } from "fs"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; @@ -17,6 +17,7 @@ const dashboardPublicSrc = join(dashboardRoot, "public"); const distDir = join(packageRoot, "dist"); const emulatorDist = join(distDir, "emulator"); const dashboardDist = join(distDir, "dashboard"); +const dashboardSymlinksManifestPath = join(dashboardDist, "symlinks.json"); function assertExists(path, message) { if (!existsSync(path)) { @@ -39,6 +40,27 @@ function copyEmulatorAssets() { console.log(`Copied emulator assets into ${emulatorDist} (+ .env.development into ${distDir}).`); } +function collectSymlinks(root, current = root) { + const symlinks = []; + for (const entry of readdirSync(current, { withFileTypes: true })) { + const path = join(current, entry.name); + if (entry.isSymbolicLink()) { + if (!existsSync(path)) { + continue; + } + symlinks.push({ + path: path.slice(root.length + 1), + target: readlinkSync(path), + }); + continue; + } + if (entry.isDirectory()) { + symlinks.push(...collectSymlinks(root, path)); + } + } + return symlinks; +} + function copyDashboardAssets() { assertExists( join(dashboardStandaloneSrc, "apps/dashboard/server.js"), @@ -50,11 +72,12 @@ function copyDashboardAssets() { ); rmSync(dashboardDist, { recursive: true, force: true }); - cpSync(dashboardStandaloneSrc, dashboardDist, { recursive: true }); + cpSync(dashboardStandaloneSrc, dashboardDist, { recursive: true, verbatimSymlinks: true }); cpSync(dashboardStaticSrc, join(dashboardDist, "apps/dashboard/.next/static"), { recursive: true }); if (existsSync(dashboardPublicSrc)) { cpSync(dashboardPublicSrc, join(dashboardDist, "apps/dashboard/public"), { recursive: true }); } + writeFileSync(dashboardSymlinksManifestPath, `${JSON.stringify(collectSymlinks(dashboardDist), undefined, 2)}\n`); console.log(`Copied dashboard standalone runtime into ${dashboardDist}.`); } diff --git a/packages/stack-cli/src/commands/dev.ts b/packages/stack-cli/src/commands/dev.ts index b01273eb5c..e4f87bc0dc 100644 --- a/packages/stack-cli/src/commands/dev.ts +++ b/packages/stack-cli/src/commands/dev.ts @@ -1,6 +1,6 @@ import { execFileSync, spawn } from "child_process"; import { Command } from "commander"; -import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync, writeSync } from "fs"; +import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, writeSync } from "fs"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { DEFAULT_API_URL, DEFAULT_PUBLISHABLE_CLIENT_KEY, resolveLoginConfig } from "../lib/auth.js"; @@ -31,6 +31,7 @@ const DASHBOARD_PORT = 26700; const DASHBOARD_START_TIMEOUT_MS = 60_000; const BUNDLED_DASHBOARD_DIR_NAME = "dashboard"; const BUNDLED_DASHBOARD_SERVER_PATH = join("apps", "dashboard", "server.js"); +const BUNDLED_DASHBOARD_SYMLINKS_MANIFEST_PATH = "symlinks.json"; const DASHBOARD_RUNTIME_DIR_NAME = "rde-dashboard-runtime"; const SENTINEL_PREFIX = "STACK_ENV_VAR_SENTINEL_"; const USE_INLINE_ENV_VARS_SENTINEL = "STACK_ENV_VAR_SENTINEL_USE_INLINE_ENV_VARS"; @@ -59,6 +60,11 @@ type DashboardSessionState = { dashboardReachableSinceMs: number, }; +type DashboardSymlink = { + path: string, + target: string, +}; + function wait(ms: number): Promise { return new Promise((resolvePromise) => setTimeout(resolvePromise, ms)); } @@ -213,12 +219,46 @@ function replaceDashboardRuntimeSentinels(root: string, env: NodeJS.ProcessEnv): } } +function isDashboardSymlink(value: unknown): value is DashboardSymlink { + if (typeof value !== "object" || value == null) { + return false; + } + if (!("path" in value) || !("target" in value)) { + return false; + } + return typeof value.path === "string" && typeof value.target === "string"; +} + +function parseDashboardSymlinksManifest(content: string): DashboardSymlink[] { + const parsed: unknown = JSON.parse(content); + if (!Array.isArray(parsed) || !parsed.every(isDashboardSymlink)) { + throw new CliError("The bundled development-environment dashboard has an invalid symlink manifest."); + } + return parsed; +} + +function restoreDashboardRuntimeSymlinks(root: string): void { + const manifestPath = join(root, BUNDLED_DASHBOARD_SYMLINKS_MANIFEST_PATH); + if (!existsSync(manifestPath)) { + throw new CliError("The bundled development-environment dashboard is missing its symlink manifest."); + } + + const manifest = parseDashboardSymlinksManifest(readFileSync(manifestPath, "utf-8")); + for (const symlink of manifest) { + const path = join(root, symlink.path); + rmSync(path, { recursive: true, force: true }); + mkdirSync(dirname(path), { recursive: true }); + symlinkSync(symlink.target, path); + } +} + function prepareDashboardRuntime(env: NodeJS.ProcessEnv): string { assertBundledDashboardExists(); const runtimeRoot = dashboardRuntimeRoot(); mkdirSync(dirname(runtimeRoot), { recursive: true }); rmSync(runtimeRoot, { recursive: true, force: true }); - cpSync(bundledDashboardRoot(), runtimeRoot, { recursive: true }); + cpSync(bundledDashboardRoot(), runtimeRoot, { recursive: true, verbatimSymlinks: true }); + restoreDashboardRuntimeSymlinks(runtimeRoot); replaceDashboardRuntimeSentinels(runtimeRoot, env); const runtimeServerPath = join(runtimeRoot, BUNDLED_DASHBOARD_SERVER_PATH); From 40524ccd8d6c5dd610fe5e8295c6356113f7a7ac Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 22:47:27 +0000 Subject: [PATCH 2/5] Guard dashboard symlink manifest portability --- packages/stack-cli/scripts/copy-runtime-assets.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stack-cli/scripts/copy-runtime-assets.mjs b/packages/stack-cli/scripts/copy-runtime-assets.mjs index 30e7102036..089a73a9eb 100644 --- a/packages/stack-cli/scripts/copy-runtime-assets.mjs +++ b/packages/stack-cli/scripts/copy-runtime-assets.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execFileSync } from "child_process"; import { chmodSync, cpSync, existsSync, mkdirSync, readlinkSync, readdirSync, rmSync, writeFileSync } from "fs"; -import { dirname, join, resolve } from "path"; +import { dirname, isAbsolute, join, resolve } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -48,9 +48,13 @@ function collectSymlinks(root, current = root) { if (!existsSync(path)) { continue; } + const target = readlinkSync(path); + if (isAbsolute(target)) { + throw new Error(`Dashboard standalone build contains a non-portable absolute symlink: ${path} -> ${target}`); + } symlinks.push({ path: path.slice(root.length + 1), - target: readlinkSync(path), + target, }); continue; } From 8be79f0094ad42561c0b108d87f8ac7a2d5da06e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 01:40:42 +0000 Subject: [PATCH 3/5] Simplify dashboard runtime asset packaging --- .../stack-cli/scripts/copy-runtime-assets.mjs | 64 ++++++++++++------- packages/stack-cli/src/commands/dev.ts | 42 +----------- 2 files changed, 43 insertions(+), 63 deletions(-) diff --git a/packages/stack-cli/scripts/copy-runtime-assets.mjs b/packages/stack-cli/scripts/copy-runtime-assets.mjs index 089a73a9eb..ecf5602676 100644 --- a/packages/stack-cli/scripts/copy-runtime-assets.mjs +++ b/packages/stack-cli/scripts/copy-runtime-assets.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execFileSync } from "child_process"; -import { chmodSync, cpSync, existsSync, mkdirSync, readlinkSync, readdirSync, rmSync, writeFileSync } from "fs"; -import { dirname, isAbsolute, join, resolve } from "path"; +import { chmodSync, cpSync, existsSync, mkdirSync, readlinkSync, readdirSync, rmSync } from "fs"; +import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -17,7 +17,6 @@ const dashboardPublicSrc = join(dashboardRoot, "public"); const distDir = join(packageRoot, "dist"); const emulatorDist = join(distDir, "emulator"); const dashboardDist = join(distDir, "dashboard"); -const dashboardSymlinksManifestPath = join(dashboardDist, "symlinks.json"); function assertExists(path, message) { if (!existsSync(path)) { @@ -40,29 +39,50 @@ function copyEmulatorAssets() { console.log(`Copied emulator assets into ${emulatorDist} (+ .env.development into ${distDir}).`); } -function collectSymlinks(root, current = root) { - const symlinks = []; +function shouldCopyDashboardFile(path) { + return existsSync(path); +} + +function copyDashboardSymlinkTarget(src, dest) { + rmSync(dest, { recursive: true, force: true }); + cpSync(src, dest, { recursive: true, dereference: true, filter: shouldCopyDashboardFile }); +} + +function getDashboardDependencyName(pnpmRoot, path) { + const parts = path.slice(pnpmRoot.length + 1).split("/"); + const nodeModulesIndex = parts.lastIndexOf("node_modules"); + if (nodeModulesIndex < 0) { + return undefined; + } + const dependencyParts = parts.slice(nodeModulesIndex + 1); + if (dependencyParts.length === 1) { + return dependencyParts[0]; + } + if (dependencyParts.length === 2 && dependencyParts[0].startsWith("@")) { + return join(dependencyParts[0], dependencyParts[1]); + } + return undefined; +} + +function copyDashboardHoistedDependencies(pnpmRoot, current = pnpmRoot) { for (const entry of readdirSync(current, { withFileTypes: true })) { const path = join(current, entry.name); - if (entry.isSymbolicLink()) { - if (!existsSync(path)) { - continue; - } - const target = readlinkSync(path); - if (isAbsolute(target)) { - throw new Error(`Dashboard standalone build contains a non-portable absolute symlink: ${path} -> ${target}`); - } - symlinks.push({ - path: path.slice(root.length + 1), - target, - }); + if (entry.isDirectory()) { + copyDashboardHoistedDependencies(pnpmRoot, path); continue; } - if (entry.isDirectory()) { - symlinks.push(...collectSymlinks(root, path)); + if (!entry.isSymbolicLink() || !existsSync(path)) { + continue; + } + const dependencyName = getDashboardDependencyName(pnpmRoot, path); + if (dependencyName == null) { + continue; + } + const target = resolve(current, readlinkSync(path)); + if (!path.includes("/node_modules/.pnpm/node_modules/")) { + copyDashboardSymlinkTarget(target, join(dashboardDist, "node_modules", dependencyName)); } } - return symlinks; } function copyDashboardAssets() { @@ -76,12 +96,12 @@ function copyDashboardAssets() { ); rmSync(dashboardDist, { recursive: true, force: true }); - cpSync(dashboardStandaloneSrc, dashboardDist, { recursive: true, verbatimSymlinks: true }); + cpSync(dashboardStandaloneSrc, dashboardDist, { recursive: true, dereference: true, filter: shouldCopyDashboardFile }); cpSync(dashboardStaticSrc, join(dashboardDist, "apps/dashboard/.next/static"), { recursive: true }); if (existsSync(dashboardPublicSrc)) { cpSync(dashboardPublicSrc, join(dashboardDist, "apps/dashboard/public"), { recursive: true }); } - writeFileSync(dashboardSymlinksManifestPath, `${JSON.stringify(collectSymlinks(dashboardDist), undefined, 2)}\n`); + copyDashboardHoistedDependencies(join(dashboardStandaloneSrc, "node_modules/.pnpm")); console.log(`Copied dashboard standalone runtime into ${dashboardDist}.`); } diff --git a/packages/stack-cli/src/commands/dev.ts b/packages/stack-cli/src/commands/dev.ts index e4f87bc0dc..dfc5ccde46 100644 --- a/packages/stack-cli/src/commands/dev.ts +++ b/packages/stack-cli/src/commands/dev.ts @@ -1,6 +1,6 @@ import { execFileSync, spawn } from "child_process"; import { Command } from "commander"; -import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, writeSync } from "fs"; +import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync, writeSync } from "fs"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { DEFAULT_API_URL, DEFAULT_PUBLISHABLE_CLIENT_KEY, resolveLoginConfig } from "../lib/auth.js"; @@ -31,7 +31,6 @@ const DASHBOARD_PORT = 26700; const DASHBOARD_START_TIMEOUT_MS = 60_000; const BUNDLED_DASHBOARD_DIR_NAME = "dashboard"; const BUNDLED_DASHBOARD_SERVER_PATH = join("apps", "dashboard", "server.js"); -const BUNDLED_DASHBOARD_SYMLINKS_MANIFEST_PATH = "symlinks.json"; const DASHBOARD_RUNTIME_DIR_NAME = "rde-dashboard-runtime"; const SENTINEL_PREFIX = "STACK_ENV_VAR_SENTINEL_"; const USE_INLINE_ENV_VARS_SENTINEL = "STACK_ENV_VAR_SENTINEL_USE_INLINE_ENV_VARS"; @@ -60,11 +59,6 @@ type DashboardSessionState = { dashboardReachableSinceMs: number, }; -type DashboardSymlink = { - path: string, - target: string, -}; - function wait(ms: number): Promise { return new Promise((resolvePromise) => setTimeout(resolvePromise, ms)); } @@ -219,46 +213,12 @@ function replaceDashboardRuntimeSentinels(root: string, env: NodeJS.ProcessEnv): } } -function isDashboardSymlink(value: unknown): value is DashboardSymlink { - if (typeof value !== "object" || value == null) { - return false; - } - if (!("path" in value) || !("target" in value)) { - return false; - } - return typeof value.path === "string" && typeof value.target === "string"; -} - -function parseDashboardSymlinksManifest(content: string): DashboardSymlink[] { - const parsed: unknown = JSON.parse(content); - if (!Array.isArray(parsed) || !parsed.every(isDashboardSymlink)) { - throw new CliError("The bundled development-environment dashboard has an invalid symlink manifest."); - } - return parsed; -} - -function restoreDashboardRuntimeSymlinks(root: string): void { - const manifestPath = join(root, BUNDLED_DASHBOARD_SYMLINKS_MANIFEST_PATH); - if (!existsSync(manifestPath)) { - throw new CliError("The bundled development-environment dashboard is missing its symlink manifest."); - } - - const manifest = parseDashboardSymlinksManifest(readFileSync(manifestPath, "utf-8")); - for (const symlink of manifest) { - const path = join(root, symlink.path); - rmSync(path, { recursive: true, force: true }); - mkdirSync(dirname(path), { recursive: true }); - symlinkSync(symlink.target, path); - } -} - function prepareDashboardRuntime(env: NodeJS.ProcessEnv): string { assertBundledDashboardExists(); const runtimeRoot = dashboardRuntimeRoot(); mkdirSync(dirname(runtimeRoot), { recursive: true }); rmSync(runtimeRoot, { recursive: true, force: true }); cpSync(bundledDashboardRoot(), runtimeRoot, { recursive: true, verbatimSymlinks: true }); - restoreDashboardRuntimeSymlinks(runtimeRoot); replaceDashboardRuntimeSentinels(runtimeRoot, env); const runtimeServerPath = join(runtimeRoot, BUNDLED_DASHBOARD_SERVER_PATH); From 862939911bc3c3c97f79d5783b85b83efbb87203 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 01:54:56 +0000 Subject: [PATCH 4/5] Tighten dashboard dependency copy selection --- packages/stack-cli/scripts/copy-runtime-assets.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/stack-cli/scripts/copy-runtime-assets.mjs b/packages/stack-cli/scripts/copy-runtime-assets.mjs index ecf5602676..cc222b3cec 100644 --- a/packages/stack-cli/scripts/copy-runtime-assets.mjs +++ b/packages/stack-cli/scripts/copy-runtime-assets.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execFileSync } from "child_process"; import { chmodSync, cpSync, existsSync, mkdirSync, readlinkSync, readdirSync, rmSync } from "fs"; -import { dirname, join, resolve } from "path"; +import { dirname, join, relative, resolve } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -48,8 +48,12 @@ function copyDashboardSymlinkTarget(src, dest) { cpSync(src, dest, { recursive: true, dereference: true, filter: shouldCopyDashboardFile }); } +function splitDashboardPath(root, path) { + return relative(root, path).split(/[\\/]+/); +} + function getDashboardDependencyName(pnpmRoot, path) { - const parts = path.slice(pnpmRoot.length + 1).split("/"); + const parts = splitDashboardPath(pnpmRoot, path); const nodeModulesIndex = parts.lastIndexOf("node_modules"); if (nodeModulesIndex < 0) { return undefined; @@ -79,7 +83,8 @@ function copyDashboardHoistedDependencies(pnpmRoot, current = pnpmRoot) { continue; } const target = resolve(current, readlinkSync(path)); - if (!path.includes("/node_modules/.pnpm/node_modules/")) { + const parts = splitDashboardPath(pnpmRoot, path); + if (parts[0] !== "node_modules" && existsSync(join(target, "package.json"))) { copyDashboardSymlinkTarget(target, join(dashboardDist, "node_modules", dependencyName)); } } From 4b3604ad7fa0f5167526d80e44df682339074651 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 02:01:40 +0000 Subject: [PATCH 5/5] Remove dashboard runtime symlink preservation --- packages/stack-cli/src/commands/dev.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack-cli/src/commands/dev.ts b/packages/stack-cli/src/commands/dev.ts index dfc5ccde46..b01273eb5c 100644 --- a/packages/stack-cli/src/commands/dev.ts +++ b/packages/stack-cli/src/commands/dev.ts @@ -218,7 +218,7 @@ function prepareDashboardRuntime(env: NodeJS.ProcessEnv): string { const runtimeRoot = dashboardRuntimeRoot(); mkdirSync(dirname(runtimeRoot), { recursive: true }); rmSync(runtimeRoot, { recursive: true, force: true }); - cpSync(bundledDashboardRoot(), runtimeRoot, { recursive: true, verbatimSymlinks: true }); + cpSync(bundledDashboardRoot(), runtimeRoot, { recursive: true }); replaceDashboardRuntimeSentinels(runtimeRoot, env); const runtimeServerPath = join(runtimeRoot, BUNDLED_DASHBOARD_SERVER_PATH);