diff --git a/packages/cli/__tests__/commands/dashboard.test.ts b/packages/cli/__tests__/commands/dashboard.test.ts index 4971c0509..a3940890c 100644 --- a/packages/cli/__tests__/commands/dashboard.test.ts +++ b/packages/cli/__tests__/commands/dashboard.test.ts @@ -89,45 +89,83 @@ describe("findRunningDashboardPid", () => { }); }); -describe("findProcessWebDir", () => { - it("extracts cwd from lsof output", async () => { - const webDir = join(tmpDir, "web"); - mkdirSync(webDir, { recursive: true }); - writeFileSync(join(webDir, "package.json"), "{}"); +describe("isInstalledUnderNodeModules", () => { + it("returns true for a Unix node_modules path segment", async () => { + const { isInstalledUnderNodeModules } = await import("../../src/lib/dashboard-rebuild.js"); - // Simulate lsof -p -Fn output - mockExecSilent.mockResolvedValue( - `p12345\nfcwd\nn${webDir}\nftxt\nn/usr/bin/node`, - ); - - const { findProcessWebDir } = await import("../../src/lib/dashboard-rebuild.js"); - - const result = await findProcessWebDir("12345"); - expect(result).toBe(webDir); + expect(isInstalledUnderNodeModules("/usr/local/lib/node_modules/@composio/ao-web")).toBe(true); }); - it("returns null when cwd has no package.json", async () => { - const webDir = join(tmpDir, "web"); - mkdirSync(webDir, { recursive: true }); - // No package.json + it("returns true for a Windows node_modules path segment", async () => { + const { isInstalledUnderNodeModules } = await import("../../src/lib/dashboard-rebuild.js"); - mockExecSilent.mockResolvedValue( - `p12345\nfcwd\nn${webDir}\nftxt\nn/usr/bin/node`, - ); - - const { findProcessWebDir } = await import("../../src/lib/dashboard-rebuild.js"); - - const result = await findProcessWebDir("12345"); - expect(result).toBeNull(); + expect(isInstalledUnderNodeModules("C:\\Users\\me\\node_modules\\@composio\\ao-web")).toBe(true); }); - it("returns null when lsof fails", async () => { - mockExecSilent.mockResolvedValue(null); + it("returns false for source paths containing node_modules as plain text", async () => { + const { isInstalledUnderNodeModules } = await import("../../src/lib/dashboard-rebuild.js"); - const { findProcessWebDir } = await import("../../src/lib/dashboard-rebuild.js"); + expect( + isInstalledUnderNodeModules("/home/user/node_modules_backup/agent-orchestrator/packages/web"), + ).toBe(false); + }); +}); - const result = await findProcessWebDir("12345"); - expect(result).toBeNull(); +describe("assertDashboardRebuildSupported", () => { + it("passes for a source checkout", async () => { + const { assertDashboardRebuildSupported } = await import("../../src/lib/dashboard-rebuild.js"); + + expect(() => + assertDashboardRebuildSupported("/home/user/agent-orchestrator/packages/web"), + ).not.toThrow(); + }); + + it("throws for an npm-installed package path", async () => { + const { assertDashboardRebuildSupported } = await import("../../src/lib/dashboard-rebuild.js"); + + expect(() => + assertDashboardRebuildSupported("/usr/local/lib/node_modules/@composio/ao-web"), + ).toThrow("Dashboard rebuild is only available from a source checkout"); + }); +}); + +describe("rebuildDashboardProductionArtifacts", () => { + it("cleans .next and runs pnpm build on success", async () => { + const webDir = join(tmpDir, "packages", "web"); + mkdirSync(webDir, { recursive: true }); + mkdirSync(join(webDir, ".next"), { recursive: true }); + + mockExec.mockResolvedValue({ stdout: "", stderr: "" }); + + const { rebuildDashboardProductionArtifacts } = await import("../../src/lib/dashboard-rebuild.js"); + + await rebuildDashboardProductionArtifacts(webDir); + + // .next should be cleaned + expect(existsSync(join(webDir, ".next"))).toBe(false); + // pnpm build should be called from workspace root (../../ relative to webDir) + expect(mockExec).toHaveBeenCalledWith("pnpm", ["build"], { cwd: tmpDir }); + }); + + it("throws when pnpm build fails", async () => { + const webDir = join(tmpDir, "packages", "web"); + mkdirSync(webDir, { recursive: true }); + + mockExec.mockRejectedValue(new Error("build failed")); + + const { rebuildDashboardProductionArtifacts } = await import("../../src/lib/dashboard-rebuild.js"); + + await expect(rebuildDashboardProductionArtifacts(webDir)).rejects.toThrow( + "Failed to rebuild dashboard production artifacts", + ); + }); + + it("throws when called from an npm-installed path", async () => { + const { rebuildDashboardProductionArtifacts } = await import("../../src/lib/dashboard-rebuild.js"); + + await expect( + rebuildDashboardProductionArtifacts("/usr/local/lib/node_modules/@composio/ao-web"), + ).rejects.toThrow("Dashboard rebuild is only available from a source checkout"); }); }); diff --git a/packages/cli/__tests__/commands/start.test.ts b/packages/cli/__tests__/commands/start.test.ts index e72f5a592..161d0d67d 100644 --- a/packages/cli/__tests__/commands/start.test.ts +++ b/packages/cli/__tests__/commands/start.test.ts @@ -113,9 +113,8 @@ vi.mock("../../src/lib/web-dir.js", () => ({ })); vi.mock("../../src/lib/dashboard-rebuild.js", () => ({ - cleanNextCache: vi.fn(), findRunningDashboardPid: vi.fn().mockResolvedValue(null), - findProcessWebDir: vi.fn().mockResolvedValue(null), + rebuildDashboardProductionArtifacts: vi.fn().mockResolvedValue(undefined), waitForPortFree: vi.fn(), })); diff --git a/packages/cli/__tests__/lib/preflight.test.ts b/packages/cli/__tests__/lib/preflight.test.ts index dc7536ce4..42a64770f 100644 --- a/packages/cli/__tests__/lib/preflight.test.ts +++ b/packages/cli/__tests__/lib/preflight.test.ts @@ -18,6 +18,11 @@ vi.mock("node:fs", () => ({ existsSync: mockExistsSync, })); +vi.mock("../../src/lib/dashboard-rebuild.js", () => ({ + isInstalledUnderNodeModules: (path: string) => + path.includes("/node_modules/") || path.includes("\\node_modules\\"), +})); + import { preflight } from "../../src/lib/preflight.js"; beforeEach(() => { @@ -58,9 +63,12 @@ describe("preflight.checkBuilt", () => { // /web/node_modules/@composio/ao-core — miss // /node_modules/@composio/ao-core — hit // /node_modules/@composio/ao-core/dist/index.js — exists + // /web/.next/BUILD_ID and /web/dist-server/start-all.js — exist mockExistsSync .mockReturnValueOnce(false) .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) .mockReturnValueOnce(true); await expect(preflight.checkBuilt("/web")).resolves.toBeUndefined(); }); @@ -88,6 +96,38 @@ describe("preflight.checkBuilt", () => { "Packages not built", ); }); + + it("throws when web production artifacts are missing", async () => { + // findPackageUp finds ao-core, dist/index.js exists, but .next/BUILD_ID missing + mockExistsSync + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + await expect(preflight.checkBuilt("/web")).rejects.toThrow( + "Packages not built", + ); + }); + + it("throws npm hint when web artifacts missing in global install", async () => { + // ao-core found at first check, dist exists, but .next/BUILD_ID missing + mockExistsSync + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + await expect( + preflight.checkBuilt("/usr/local/lib/node_modules/@composio/ao-web"), + ).rejects.toThrow("npm install -g @composio/ao@latest"); + }); + + it("throws npm hint when ao-core dist is missing in global install", async () => { + // ao-core found, but dist/index.js missing + mockExistsSync + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + await expect( + preflight.checkBuilt("/usr/local/lib/node_modules/@composio/ao-web"), + ).rejects.toThrow("npm install -g @composio/ao@latest"); + }); }); describe("preflight.checkTmux", () => { diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index 8f494c77d..cb32cabce 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -1,11 +1,16 @@ import { spawn } from "node:child_process"; -import { existsSync } from "node:fs"; import { resolve } from "node:path"; import chalk from "chalk"; import type { Command } from "commander"; import { loadConfig } from "@composio/ao-core"; import { findWebDir, buildDashboardEnv, waitForPortAndOpen } from "../lib/web-dir.js"; -import { cleanNextCache, findRunningDashboardPid, findProcessWebDir, waitForPortFree } from "../lib/dashboard-rebuild.js"; +import { + findRunningDashboardPid, + isInstalledUnderNodeModules, + rebuildDashboardProductionArtifacts, + waitForPortFree, +} from "../lib/dashboard-rebuild.js"; +import { preflight } from "../lib/preflight.js"; export function registerDashboard(program: Command): void { program @@ -14,6 +19,7 @@ export function registerDashboard(program: Command): void { .option("-p, --port ", "Port to listen on") .option("--no-open", "Don't open browser automatically") .option("--rebuild", "Clean stale build artifacts and rebuild before starting") + /* c8 ignore start -- process-spawning startup code, tested via integration/onboarding */ .action(async (opts: { port?: string; open?: boolean; rebuild?: boolean }) => { const config = loadConfig(); const port = opts.port ? parseInt(opts.port, 10) : (config.port ?? 3000); @@ -28,11 +34,9 @@ export function registerDashboard(program: Command): void { if (opts.rebuild) { // Check if a dashboard is already running on this port. const runningPid = await findRunningDashboardPid(port); - const runningWebDir = runningPid ? await findProcessWebDir(runningPid) : null; - const targetWebDir = runningWebDir ?? localWebDir; if (runningPid) { - // Kill the running server, clean .next, then start fresh below. + // Stop the running server before rebuilding or restarting below. console.log( chalk.dim(`Stopping dashboard (PID ${runningPid}) on port ${port}...`), ); @@ -45,8 +49,10 @@ export function registerDashboard(program: Command): void { await waitForPortFree(port, 5000); } - await cleanNextCache(targetWebDir); + await rebuildDashboardProductionArtifacts(localWebDir); // Fall through to start the dashboard on this port. + } else { + await preflight.checkBuilt(localWebDir); } const webDir = localWebDir; @@ -60,21 +66,12 @@ export function registerDashboard(program: Command): void { config.directTerminalPort, ); - // In dev mode (monorepo), use `pnpm run dev` which starts Next.js AND - // the terminal WebSocket servers via concurrently. Without the WS servers, - // the live terminal in the dashboard won't work. - const isDevMode = existsSync(resolve(webDir, "server")); - const child = isDevMode - ? spawn("pnpm", ["run", "dev"], { - cwd: webDir, - stdio: ["inherit", "inherit", "pipe"], - env, - }) - : spawn("npx", ["next", "dev", "-p", String(port)], { - cwd: webDir, - stdio: ["inherit", "inherit", "pipe"], - env, - }); + const startScript = resolve(webDir, "dist-server", "start-all.js"); + const child = spawn("node", [startScript], { + cwd: webDir, + stdio: ["inherit", "inherit", "pipe"], + env, + }); const stderrChunks: string[] = []; @@ -108,10 +105,13 @@ export function registerDashboard(program: Command): void { if (code !== 0 && code !== null && !opts.rebuild) { const stderr = stderrChunks.join(""); if (looksLikeStaleBuild(stderr)) { + const recoveryCommand = isInstalledUnderNodeModules(webDir) + ? "npm install -g @composio/ao@latest" + : "ao dashboard --rebuild"; console.error( chalk.yellow( "\nThis looks like a stale build cache issue. Try:\n\n" + - ` ${chalk.cyan("ao dashboard --rebuild")}\n`, + ` ${chalk.cyan(recoveryCommand)}\n`, ), ); } @@ -120,6 +120,7 @@ export function registerDashboard(program: Command): void { process.exit(code ?? 0); }); }); + /* c8 ignore stop */ } /** diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 4f2574f10..5eb257ffd 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -48,7 +48,7 @@ import { findFreePort, MAX_PORT_SCAN, } from "../lib/web-dir.js"; -import { cleanNextCache } from "../lib/dashboard-rebuild.js"; +import { rebuildDashboardProductionArtifacts } from "../lib/dashboard-rebuild.js"; import { preflight } from "../lib/preflight.js"; import { register, unregister, isAlreadyRunning, getRunning, waitForExit } from "../lib/running-state.js"; import { isHumanCaller } from "../lib/caller-context.js"; @@ -749,22 +749,29 @@ export async function createConfigOnly(): Promise { * Start dashboard server in the background. * Returns the child process handle for cleanup. */ +/* c8 ignore start -- process-spawning startup code, tested via integration/onboarding */ async function startDashboard( port: number, webDir: string, configPath: string | null, terminalPort?: number, directTerminalPort?: number, + devMode?: boolean, ): Promise { const env = await buildDashboardEnv(port, configPath, terminalPort, directTerminalPort); - // Detect dev vs production: the `server/` source directory only exists in the - // monorepo. Published npm packages only have `dist-server/`. - const isDevMode = existsSync(resolve(webDir, "server")); + // Detect monorepo vs npm install: the `server/` source directory only exists + // in the monorepo. Published npm packages only have `dist-server/`. + const isMonorepo = existsSync(resolve(webDir, "server")); + + // In monorepo: use HMR dev server only when --dev is passed explicitly. + // Default is optimized production server for faster loading. + const useDevServer = isMonorepo && devMode === true; let child: ChildProcess; - if (isDevMode) { - // Monorepo development: use pnpm run dev (tsx, HMR, etc.) + if (useDevServer) { + // Monorepo with --dev: use pnpm run dev (tsx watch, HMR, etc.) + console.log(chalk.dim(" Mode: development (HMR enabled)")); child = spawn("pnpm", ["run", "dev"], { cwd: webDir, stdio: "inherit", @@ -772,8 +779,13 @@ async function startDashboard( env, }); } else { - // Production (installed from npm): use pre-built start-all script - child = spawn("node", [resolve(webDir, "dist-server", "start-all.js")], { + // Production: use pre-built start-all script. + if (isMonorepo) { + console.log(chalk.dim(" Mode: optimized (production bundles)")); + console.log(chalk.dim(" Tip: use --dev for hot reload when editing dashboard UI\n")); + } + const startScript = resolve(webDir, "dist-server", "start-all.js"); + child = spawn("node", [startScript], { cwd: webDir, stdio: "inherit", detached: false, @@ -782,8 +794,8 @@ async function startDashboard( } child.on("error", (err) => { - const cmd = isDevMode ? "pnpm" : "node"; - const args = isDevMode ? ["run", "dev"] : [resolve(webDir, "dist-server", "start-all.js")]; + const cmd = useDevServer ? "pnpm" : "node"; + const args = useDevServer ? ["run", "dev"] : [resolve(webDir, "dist-server", "start-all.js")]; const formatted = formatCommandError(err, { cmd, args, @@ -797,6 +809,7 @@ async function startDashboard( return child; } +/* c8 ignore stop */ /** * Ensure tmux is available — interactive install with user consent if missing. @@ -899,7 +912,7 @@ async function runStartup( config: OrchestratorConfig, projectId: string, project: ProjectConfig, - opts?: { dashboard?: boolean; orchestrator?: boolean; rebuild?: boolean }, + opts?: { dashboard?: boolean; orchestrator?: boolean; rebuild?: boolean; dev?: boolean }, ): Promise { // Ensure tmux is available before doing anything — covers all entry paths // (normal start, URL start, retry with existing config) @@ -950,10 +963,15 @@ async function runStartup( port = newPort; } const webDir = findWebDir(); // throws with install-specific guidance if not found - await preflight.checkBuilt(webDir); - + // Dev mode (HMR) only works in the monorepo where `server/` source exists. + // For npm installs, --dev is silently ignored and production server runs, + // so preflight must still verify production artifacts exist. + const isMonorepo = existsSync(resolve(webDir, "server")); + const willUseDevServer = isMonorepo && opts?.dev === true; if (opts?.rebuild) { - await cleanNextCache(webDir); + await rebuildDashboardProductionArtifacts(webDir); + } else if (!willUseDevServer) { + await preflight.checkBuilt(webDir); } spinner.start("Starting dashboard"); @@ -963,6 +981,7 @@ async function runStartup( config.configPath, config.terminalPort, config.directTerminalPort, + opts?.dev, ); spinner.succeed(`Dashboard starting on http://localhost:${port}`); console.log(chalk.dim(" (Dashboard will be ready in a few seconds)\n")); @@ -1174,6 +1193,7 @@ export function registerStart(program: Command): void { .option("--no-dashboard", "Skip starting the dashboard server") .option("--no-orchestrator", "Skip starting the orchestrator agent") .option("--rebuild", "Clean and rebuild dashboard before starting") + .option("--dev", "Use Next.js dev server with hot reload (for dashboard UI development)") .option("--interactive", "Prompt to configure config settings") .action( async ( @@ -1182,6 +1202,7 @@ export function registerStart(program: Command): void { dashboard?: boolean; orchestrator?: boolean; rebuild?: boolean; + dev?: boolean; interactive?: boolean; }, ) => { diff --git a/packages/cli/src/lib/dashboard-rebuild.ts b/packages/cli/src/lib/dashboard-rebuild.ts index b6de94011..ad38d3869 100644 --- a/packages/cli/src/lib/dashboard-rebuild.ts +++ b/packages/cli/src/lib/dashboard-rebuild.ts @@ -1,12 +1,33 @@ /** - * Dashboard cache utilities — cleans stale .next artifacts and detects - * running dashboard processes. + * Dashboard cache utilities — cleans stale .next artifacts, detects + * running dashboard processes, and rebuilds production artifacts. */ import { resolve } from "node:path"; import { existsSync, rmSync } from "node:fs"; import ora from "ora"; -import { execSilent } from "./shell.js"; +import { exec, execSilent } from "./shell.js"; + +/** + * Check if the web directory is inside a node_modules tree (npm/yarn global install). + * Matches node_modules as a path segment, not just a substring. + */ +export function isInstalledUnderNodeModules(path: string): boolean { + return path.includes("/node_modules/") || path.includes("\\node_modules\\"); +} + +/** + * Guard: rebuilds are only possible from a source checkout. + * Global npm installs ship prebuilt artifacts and cannot rebuild in place. + */ +export function assertDashboardRebuildSupported(webDir: string): void { + if (isInstalledUnderNodeModules(webDir)) { + throw new Error( + "Dashboard rebuild is only available from a source checkout. " + + "Run `ao update`, or reinstall with `npm install -g @composio/ao@latest`.", + ); + } +} /** * Find the PID of a process listening on the given port. @@ -21,28 +42,6 @@ export async function findRunningDashboardPid(port: number): Promise { - const lsofDetail = await execSilent("lsof", ["-p", pid, "-Ffn"]); - if (!lsofDetail) return null; - - // lsof -Fn outputs lines like "n/path/to/cwd" — the cwd entry follows "fcwd" - const lines = lsofDetail.split("\n"); - for (let i = 0; i < lines.length; i++) { - if (lines[i] === "fcwd" && i + 1 < lines.length && lines[i + 1]?.startsWith("n/")) { - const cwd = lines[i + 1].slice(1); - if (existsSync(resolve(cwd, "package.json"))) { - return cwd; - } - } - } - - return null; -} - /** * Wait for a port to be free (no process listening). * Throws if the port is still busy after the timeout. @@ -58,9 +57,7 @@ export async function waitForPortFree(port: number, timeoutMs: number): Promise< } /** - * Clean just the .next cache directory. Use when a dev server is running — - * it will recompile on next request. Does NOT run pnpm build (which would - * create a production .next that the dev server can't use). + * Remove the .next directory before a rebuild. */ export async function cleanNextCache(webDir: string): Promise { const nextDir = resolve(webDir, ".next"); @@ -72,3 +69,27 @@ export async function cleanNextCache(webDir: string): Promise { } } +/** + * Rebuild dashboard production artifacts (Next.js build + server compilation) + * from a source checkout. Throws if called from an npm global install. + */ +export async function rebuildDashboardProductionArtifacts(webDir: string): Promise { + assertDashboardRebuildSupported(webDir); + + await cleanNextCache(webDir); + + const workspaceRoot = resolve(webDir, "../.."); + const spinner = ora("Rebuilding dashboard production artifacts").start(); + + try { + await exec("pnpm", ["build"], { cwd: workspaceRoot }); + spinner.succeed("Rebuilt dashboard production artifacts"); + } catch (error) { + spinner.fail("Dashboard rebuild failed"); + throw new Error( + "Failed to rebuild dashboard production artifacts. Run `pnpm build` and try again.", + { cause: error }, + ); + } +} + diff --git a/packages/cli/src/lib/preflight.ts b/packages/cli/src/lib/preflight.ts index 0f7721f85..e22cbadb3 100644 --- a/packages/cli/src/lib/preflight.ts +++ b/packages/cli/src/lib/preflight.ts @@ -11,6 +11,7 @@ import { existsSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { isPortAvailable } from "./web-dir.js"; import { exec } from "./shell.js"; +import { isInstalledUnderNodeModules } from "./dashboard-rebuild.js"; /** * Check that the dashboard port is free. @@ -32,16 +33,26 @@ async function checkPort(port: number): Promise { * installs (hoisted to a parent node_modules). */ async function checkBuilt(webDir: string): Promise { + const isNpmInstall = isInstalledUnderNodeModules(webDir); const corePkgDir = findPackageUp(webDir, "@composio", "ao-core"); if (!corePkgDir) { - const hint = webDir.includes("node_modules") + const hint = isNpmInstall ? "Run: npm install -g @composio/ao@latest" : "Run: pnpm install && pnpm build"; throw new Error(`Dependencies not installed. ${hint}`); } const coreEntry = resolve(corePkgDir, "dist", "index.js"); if (!existsSync(coreEntry)) { - const hint = webDir.includes("node_modules") + const hint = isNpmInstall + ? "Run: npm install -g @composio/ao@latest" + : "Run: pnpm build"; + throw new Error(`Packages not built. ${hint}`); + } + + const webBuildId = resolve(webDir, ".next", "BUILD_ID"); + const startAllEntry = resolve(webDir, "dist-server", "start-all.js"); + if (!existsSync(webBuildId) || !existsSync(startAllEntry)) { + const hint = isNpmInstall ? "Run: npm install -g @composio/ao@latest" : "Run: pnpm build"; throw new Error(`Packages not built. ${hint}`); diff --git a/packages/web/next.config.js b/packages/web/next.config.js index 654d91bc4..1dd63ef63 100644 --- a/packages/web/next.config.js +++ b/packages/web/next.config.js @@ -23,4 +23,11 @@ const nextConfig = { }, }; -export default nextConfig; +// Only load bundle analyzer when ANALYZE=true (dev-only dependency) +let config = nextConfig; +if (process.env.ANALYZE === "true") { + const { default: bundleAnalyzer } = await import("@next/bundle-analyzer"); + config = bundleAnalyzer({ enabled: true })(nextConfig); +} + +export default config; diff --git a/packages/web/package.json b/packages/web/package.json index 962df764c..a1e24ed76 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -19,6 +19,7 @@ "build": "next build && tsc -p tsconfig.server.json", "start": "next start", "start:all": "node dist-server/start-all.js", + "dev:optimized": "next build && tsc -p tsconfig.server.json && node dist-server/start-all.js", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", @@ -41,6 +42,7 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "server-only": "^0.0.1", "ws": "^8.19.0", "xterm": "^5.3.0" }, @@ -48,6 +50,7 @@ "node-pty": "^1.1.0" }, "devDependencies": { + "@next/bundle-analyzer": "^15.1.0", "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.1.0", @@ -55,6 +58,7 @@ "@types/react-dom": "^19.0.0", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.3.0", + "@vitest/coverage-v8": "^2.1.0", "concurrently": "^9.2.1", "jsdom": "^25.0.0", "node-gyp": "^12.2.0", @@ -62,7 +66,6 @@ "tailwindcss": "^4.0.0", "tsx": "^4.19.0", "typescript": "^5.7.0", - "@vitest/coverage-v8": "^2.1.0", "vitest": "^2.1.0" } } diff --git a/packages/web/src/__tests__/server-only-mock.ts b/packages/web/src/__tests__/server-only-mock.ts new file mode 100644 index 000000000..f83932659 --- /dev/null +++ b/packages/web/src/__tests__/server-only-mock.ts @@ -0,0 +1,3 @@ +// Stub for "server-only" package in vitest — the real module throws when +// imported outside a React Server Component context. +export {}; diff --git a/packages/web/src/app/dev/terminal-test/page.tsx b/packages/web/src/app/dev/terminal-test/page.tsx index 8323ae23e..da56936aa 100644 --- a/packages/web/src/app/dev/terminal-test/page.tsx +++ b/packages/web/src/app/dev/terminal-test/page.tsx @@ -1,10 +1,22 @@ "use client"; -import { DirectTerminal } from "@/components/DirectTerminal"; -import { Terminal } from "@/components/Terminal"; +import nextDynamic from "next/dynamic"; import { useSearchParams } from "next/navigation"; import { useState, useEffect, Suspense } from "react"; +const DirectTerminal = nextDynamic( + () => + import("@/components/DirectTerminal").then((m) => ({ + default: m.DirectTerminal, + })), + { ssr: false }, +); + +const Terminal = nextDynamic( + () => import("@/components/Terminal").then((m) => ({ default: m.Terminal })), + { ssr: false }, +); + // Force dynamic rendering (required for useSearchParams) export const dynamic = "force-dynamic"; diff --git a/packages/web/src/app/test-direct/page.test.tsx b/packages/web/src/app/test-direct/page.test.tsx index d4ed45dff..b12d86405 100644 --- a/packages/web/src/app/test-direct/page.test.tsx +++ b/packages/web/src/app/test-direct/page.test.tsx @@ -7,18 +7,28 @@ vi.mock("next/navigation", () => ({ useSearchParams: () => searchParams, })); +const MockDirectTerminal = ({ + sessionId, + startFullscreen, +}: { + sessionId: string; + startFullscreen: boolean; +}) => ( +
+ {sessionId}:{String(startFullscreen)} +
+); + vi.mock("@/components/DirectTerminal", () => ({ - DirectTerminal: ({ - sessionId, - startFullscreen, - }: { - sessionId: string; - startFullscreen: boolean; - }) => ( -
- {sessionId}:{String(startFullscreen)} -
- ), + DirectTerminal: MockDirectTerminal, +})); + +// next/dynamic wraps lazy imports; in tests, bypass the dynamic loader and +// return the mock component directly. +vi.mock("next/dynamic", () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default: (_loader: any) => MockDirectTerminal, })); describe("TestDirectPage", () => { diff --git a/packages/web/src/app/test-direct/page.tsx b/packages/web/src/app/test-direct/page.tsx index 9a8b14911..06a8ab4ee 100644 --- a/packages/web/src/app/test-direct/page.tsx +++ b/packages/web/src/app/test-direct/page.tsx @@ -1,9 +1,17 @@ "use client"; -import { DirectTerminal } from "@/components/DirectTerminal"; +import nextDynamic from "next/dynamic"; import { useSearchParams } from "next/navigation"; import { Suspense } from "react"; +const DirectTerminal = nextDynamic( + () => + import("@/components/DirectTerminal").then((m) => ({ + default: m.DirectTerminal, + })), + { ssr: false }, +); + // Force dynamic rendering (required for useSearchParams) export const dynamic = "force-dynamic"; diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx index 0f7f548fd..debc055cb 100644 --- a/packages/web/src/components/SessionDetail.tsx +++ b/packages/web/src/components/SessionDetail.tsx @@ -6,11 +6,21 @@ import { useMediaQuery, MOBILE_BREAKPOINT } from "@/hooks/useMediaQuery"; import { type DashboardSession, type DashboardPR, isPRMergeReady } from "@/lib/types"; import { CI_STATUS } from "@composio/ao-core/types"; import { cn } from "@/lib/cn"; +import dynamic from "next/dynamic"; import { getSessionTitle } from "@/lib/format"; import { CICheckList } from "./CIBadge"; -import { DirectTerminal } from "./DirectTerminal"; import { MobileBottomNav } from "./MobileBottomNav"; +const DirectTerminal = dynamic( + () => import("./DirectTerminal").then((m) => ({ default: m.DirectTerminal })), + { + ssr: false, + loading: () => ( +
+ ), + }, +); + interface OrchestratorZones { merge: number; respond: number; diff --git a/packages/web/src/lib/dashboard-page-data.ts b/packages/web/src/lib/dashboard-page-data.ts index 536137d5b..efb6e2419 100644 --- a/packages/web/src/lib/dashboard-page-data.ts +++ b/packages/web/src/lib/dashboard-page-data.ts @@ -1,3 +1,5 @@ +import "server-only"; + import { cache } from "react"; import { TERMINAL_STATUSES, type DashboardSession, type DashboardOrchestratorLink } from "@/lib/types"; import { getServices, getSCM } from "@/lib/services"; diff --git a/packages/web/src/lib/observability.ts b/packages/web/src/lib/observability.ts index da33da03d..284273188 100644 --- a/packages/web/src/lib/observability.ts +++ b/packages/web/src/lib/observability.ts @@ -1,3 +1,5 @@ +import "server-only"; + import { createCorrelationId, createProjectObserver, diff --git a/packages/web/src/lib/project-name.ts b/packages/web/src/lib/project-name.ts index b410ec102..0aaf920c5 100644 --- a/packages/web/src/lib/project-name.ts +++ b/packages/web/src/lib/project-name.ts @@ -1,3 +1,5 @@ +import "server-only"; + import { cache } from "react"; import { loadConfig } from "@composio/ao-core"; diff --git a/packages/web/src/lib/scm-webhooks.ts b/packages/web/src/lib/scm-webhooks.ts index 06534e4e9..5e2472f92 100644 --- a/packages/web/src/lib/scm-webhooks.ts +++ b/packages/web/src/lib/scm-webhooks.ts @@ -1,3 +1,5 @@ +import "server-only"; + import { TERMINAL_STATUSES, type OrchestratorConfig, diff --git a/packages/web/src/lib/serialize.ts b/packages/web/src/lib/serialize.ts index 0d61ee81f..d24372b6a 100644 --- a/packages/web/src/lib/serialize.ts +++ b/packages/web/src/lib/serialize.ts @@ -1,3 +1,5 @@ +import "server-only"; + /** * Core Session → DashboardSession serialization. * diff --git a/packages/web/src/lib/services.ts b/packages/web/src/lib/services.ts index 31897c277..7715136f8 100644 --- a/packages/web/src/lib/services.ts +++ b/packages/web/src/lib/services.ts @@ -1,3 +1,5 @@ +import "server-only"; + /** * Server-side singleton for core services. * diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index 7e696a1b1..8e6585d6b 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -50,6 +50,7 @@ export default defineConfig({ find: "@composio/ao-plugin-tracker-linear", replacement: resolve(__dirname, "../plugins/tracker-linear/src/index.ts"), }, + { find: "server-only", replacement: resolve(__dirname, "./src/__tests__/server-only-mock.ts") }, { find: "@", replacement: resolve(__dirname, "./src") }, ], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d77082c42..defdd642e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -617,6 +617,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + server-only: + specifier: ^0.0.1 + version: 0.0.1 ws: specifier: ^8.19.0 version: 8.19.0 @@ -628,6 +631,9 @@ importers: specifier: ^1.1.0 version: 1.1.0 devDependencies: + '@next/bundle-analyzer': + specifier: ^15.1.0 + version: 15.5.14 '@tailwindcss/postcss': specifier: ^4.0.0 version: 4.1.18 @@ -912,6 +918,10 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} @@ -1555,6 +1565,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@next/bundle-analyzer@15.5.14': + resolution: {integrity: sha512-Q8rK9QXAqnJ2Ct1XXRJALBccINg9rX0Zl8QSPrYCuL/J1c+k7cfGcRTUNDAP9WIvhYk8JfxZEm1JpcHi+MUKiw==} + '@next/env@15.5.12': resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} @@ -1634,6 +1647,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -2136,6 +2152,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2341,6 +2361,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + composio-core@0.5.39: resolution: {integrity: sha512-7BeSFlfRzr1cbIfGYJW4jQ3BHwaObOaFKiRJIFuWOmvOrTABl1hbxGkWPA3C+uFw9CFXbZhrLWNyD7lhYy2Scg==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -2383,6 +2407,9 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2440,6 +2467,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2739,6 +2769,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2847,6 +2881,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -3183,6 +3221,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3278,6 +3320,10 @@ packages: zod: optional: true + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3567,6 +3613,9 @@ packages: engines: {node: '>=10'} hasBin: true + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3593,6 +3642,10 @@ packages: simple-wcswidth@1.1.2: resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3764,6 +3817,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -4024,6 +4081,11 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -4068,6 +4130,18 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -4478,6 +4552,8 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@discoveryjs/json-ext@0.5.7': {} + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -4992,6 +5068,13 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@next/bundle-analyzer@15.5.14': + dependencies: + webpack-bundle-analyzer: 4.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@next/env@15.5.12': {} '@next/swc-darwin-arm64@15.5.12': @@ -5049,6 +5132,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.29': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.57.1': @@ -5584,6 +5669,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-walk@8.3.5: + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} agent-base@7.1.4: {} @@ -5773,6 +5862,8 @@ snapshots: commander@13.1.0: {} + commander@7.2.0: {} + composio-core@0.5.39(@ai-sdk/openai@3.0.29(zod@3.25.76))(@cloudflare/workers-types@4.20260214.0)(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(@langchain/openai@1.2.7(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(ws@8.19.0))(ai@6.0.86(zod@3.25.76))(langchain@1.2.24(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@3.25.76)))(openai@6.22.0(ws@8.19.0)(zod@3.25.76)): dependencies: '@ai-sdk/openai': 3.0.29(zod@3.25.76) @@ -5833,6 +5924,8 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + debounce@1.2.1: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -5869,6 +5962,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.286: {} @@ -6222,6 +6317,10 @@ snapshots: graceful-fs@4.2.11: {} + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -6309,6 +6408,8 @@ snapshots: is-number@7.0.0: {} + is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-subdir@1.2.0: @@ -6642,6 +6743,8 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} mustache@4.2.0: {} @@ -6731,6 +6834,8 @@ snapshots: ws: 8.19.0 zod: 3.25.76 + opener@1.5.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7005,6 +7110,8 @@ snapshots: semver@7.7.4: {} + server-only@0.0.1: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -7051,6 +7158,12 @@ snapshots: simple-wcswidth@1.1.2: {} + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -7199,6 +7312,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -7459,6 +7574,25 @@ snapshots: webidl-conversions@7.0.0: {} + webpack-bundle-analyzer@4.10.1: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.15.0 + acorn-walk: 8.3.5 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -7503,6 +7637,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + ws@7.5.10: {} + ws@8.19.0: {} xml-name-validator@5.0.0: {}