diff --git a/eslint.config.js b/eslint.config.js index 653ec05ce..fd2c968f5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,6 +13,8 @@ export default tseslint.config( "packages/web/next-env.d.ts", "packages/web/next.config.js", "packages/web/postcss.config.mjs", + "test-clipboard*.mjs", + "test-clipboard*.sh", ], }, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index bd3c31114..65de461eb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -49,6 +49,15 @@ export type ActivityState = | "blocked" // agent hit an error or is stuck | "exited"; // agent process is no longer running +/** Activity state constants */ +export const ACTIVITY_STATE = { + ACTIVE: "active" as const, + IDLE: "idle" as const, + WAITING_INPUT: "waiting_input" as const, + BLOCKED: "blocked" as const, + EXITED: "exited" as const, +} satisfies Record; + /** A running agent session */ export interface Session { /** Unique session ID, e.g. "my-app-3" */ diff --git a/packages/web/.env.local.example b/packages/web/.env.local.example new file mode 100644 index 000000000..d0260f17a --- /dev/null +++ b/packages/web/.env.local.example @@ -0,0 +1,7 @@ +# Terminal server ports +TERMINAL_PORT=3001 # ttyd (iframe) terminal server +DIRECT_TERMINAL_PORT=3003 # Direct WebSocket terminal server with XDA support + +# Public environment variables (exposed to browser) +NEXT_PUBLIC_TERMINAL_PORT=3001 +NEXT_PUBLIC_DIRECT_TERMINAL_PORT=3003 diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/packages/web/package.json b/packages/web/package.json index fffe337ca..e290378b9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -5,9 +5,10 @@ "private": true, "type": "module", "scripts": { - "dev": "concurrently \"npm:dev:next\" \"npm:dev:terminal\"", + "dev": "concurrently \"npm:dev:next\" \"npm:dev:terminal\" \"npm:dev:direct-terminal\"", "dev:next": "next dev", - "dev:terminal": "tsx watch src/server/terminal-websocket.ts", + "dev:terminal": "tsx watch server/terminal-websocket.ts", + "dev:direct-terminal": "tsx watch server/direct-terminal-ws.ts", "build": "next build", "start": "next start", "typecheck": "tsc --noEmit", @@ -25,9 +26,14 @@ "@composio/ao-plugin-tracker-github": "workspace:*", "@composio/ao-plugin-tracker-linear": "workspace:*", "@composio/ao-plugin-workspace-worktree": "workspace:*", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", "next": "^15.1.0", + "node-pty": "^1.1.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "ws": "^8.19.0", + "xterm": "^5.3.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0", @@ -35,9 +41,11 @@ "@testing-library/react": "^16.1.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.3.0", "concurrently": "^9.2.1", "jsdom": "^25.0.0", + "node-gyp": "^12.2.0", "playwright": "^1.49.0", "tailwindcss": "^4.0.0", "tsx": "^4.19.0", diff --git a/packages/web/server/direct-terminal-ws.ts b/packages/web/server/direct-terminal-ws.ts new file mode 100644 index 000000000..51f0c8551 --- /dev/null +++ b/packages/web/server/direct-terminal-ws.ts @@ -0,0 +1,221 @@ +/** + * Direct WebSocket terminal server using node-pty. + * Connects browser xterm.js directly to tmux sessions via WebSocket. + * + * This bypasses ttyd and gives us control over terminal initialization, + * allowing us to implement the XDA (Extended Device Attributes) handler + * that tmux requires for clipboard support. + */ + +import { createServer } from "node:http"; +import { spawn } from "node:child_process"; +import { WebSocketServer, WebSocket } from "ws"; +import { spawn as ptySpawn, type IPty } from "node-pty"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { homedir, userInfo } from "node:os"; +import { loadConfig } from "@composio/ao-core"; + +// Load config with fallback +let config: ReturnType; +try { + config = loadConfig(); +} catch (err) { + console.warn("[DirectTerminal] Could not load config, using defaults:", err instanceof Error ? err.message : String(err)); + // Fallback to default config + config = { + dataDir: join(homedir(), ".ao/sessions"), + } as ReturnType; +} + +interface TerminalSession { + sessionId: string; + pty: IPty; + ws: WebSocket; +} + +const activeSessions = new Map(); + +/** + * Create HTTP server with WebSocket upgrade handling + */ +const server = createServer((req, res) => { + // Health check endpoint + if (req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + active: activeSessions.size, + sessions: Array.from(activeSessions.keys()), + })); + return; + } + + res.writeHead(404); + res.end("Not found"); +}); + +/** + * WebSocket server for terminal connections + */ +const wss = new WebSocketServer({ + server, + path: "/ws", +}); + +wss.on("connection", (ws, req) => { + const url = new URL(req.url ?? "/", "ws://localhost"); + const sessionId = url.searchParams.get("session"); + + if (!sessionId) { + console.error("[DirectTerminal] Missing session parameter"); + ws.close(1008, "Missing session parameter"); + return; + } + + // Validate session ID + if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) { + console.error("[DirectTerminal] Invalid session ID:", sessionId); + ws.close(1008, "Invalid session ID"); + return; + } + + // Validate session exists + const sessionPath = join(config.dataDir, sessionId); + if (!existsSync(sessionPath)) { + console.error("[DirectTerminal] Session not found:", sessionId); + ws.close(1008, "Session not found"); + return; + } + + console.log(`[DirectTerminal] New connection for session: ${sessionId}`); + + // Enable mouse mode for scrollback support + const mouseProc = spawn("tmux", ["set-option", "-t", sessionId, "mouse", "on"]); + mouseProc.on("error", (err) => { + console.error(`[DirectTerminal] Failed to set mouse mode for ${sessionId}:`, err.message); + }); + + // Hide the green status bar for cleaner appearance + const statusProc = spawn("tmux", ["set-option", "-t", sessionId, "status", "off"]); + statusProc.on("error", (err) => { + console.error(`[DirectTerminal] Failed to hide status bar for ${sessionId}:`, err.message); + }); + + // Spawn PTY attached to tmux session + // Use tmux from PATH for cross-platform compatibility + const tmuxPath = "tmux"; + + // Build complete environment - node-pty requires proper env setup + const homeDir = process.env.HOME || homedir(); + const currentUser = process.env.USER || userInfo().username; + const env = { + HOME: homeDir, + SHELL: process.env.SHELL || "/bin/bash", + USER: currentUser, + PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin", + TERM: "xterm-256color", + LANG: process.env.LANG || "en_US.UTF-8", + TMPDIR: process.env.TMPDIR || "/tmp", + }; + + let pty: IPty; + try { + console.log(`[DirectTerminal] Spawning PTY: ${tmuxPath} attach-session -t ${sessionId}`); + console.log(`[DirectTerminal] CWD: ${homeDir}, ENV keys:`, Object.keys(env).join(", ")); + + pty = ptySpawn(tmuxPath, ["attach-session", "-t", sessionId], { + name: "xterm-256color", + cols: 80, + rows: 24, + cwd: homeDir, + env, + }); + + console.log(`[DirectTerminal] PTY spawned successfully`); + } catch (err) { + console.error(`[DirectTerminal] Failed to spawn PTY:`, err); + ws.close(1011, `Failed to spawn terminal: ${err instanceof Error ? err.message : String(err)}`); + return; + } + + const session: TerminalSession = { sessionId, pty, ws }; + activeSessions.set(sessionId, session); + + // PTY -> WebSocket + pty.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data); + } + }); + + // PTY exit + pty.onExit(({ exitCode }) => { + console.log(`[DirectTerminal] PTY exited for ${sessionId} with code ${exitCode}`); + activeSessions.delete(sessionId); + if (ws.readyState === WebSocket.OPEN) { + ws.close(1000, "Terminal session ended"); + } + }); + + // WebSocket -> PTY + ws.on("message", (data) => { + const message = data.toString("utf8"); + + // Handle resize messages (sent by xterm.js FitAddon) + if (message.startsWith("{")) { + try { + const parsed = JSON.parse(message) as { type?: string; cols?: number; rows?: number }; + if (parsed.type === "resize" && parsed.cols && parsed.rows) { + pty.resize(parsed.cols, parsed.rows); + return; + } + } catch { + // Not JSON, treat as terminal input + } + } + + // Normal terminal input + pty.write(message); + }); + + // WebSocket close + ws.on("close", () => { + console.log(`[DirectTerminal] WebSocket closed for ${sessionId}`); + activeSessions.delete(sessionId); + pty.kill(); + }); + + // WebSocket error + ws.on("error", (err) => { + console.error(`[DirectTerminal] WebSocket error for ${sessionId}:`, err.message); + activeSessions.delete(sessionId); + pty.kill(); + }); +}); + +const PORT = parseInt(process.env.DIRECT_TERMINAL_PORT ?? "3003", 10); + +server.listen(PORT, () => { + console.log(`[DirectTerminal] WebSocket server listening on port ${PORT}`); +}); + +// Graceful shutdown +function shutdown(signal: string) { + console.log(`[DirectTerminal] Received ${signal}, shutting down...`); + for (const [, session] of activeSessions) { + session.pty.kill(); + session.ws.close(1001, "Server shutting down"); + } + server.close(() => { + console.log("[DirectTerminal] Server closed"); + process.exit(0); + }); + const forceExitTimer = setTimeout(() => { + console.error("[DirectTerminal] Forced shutdown after timeout"); + process.exit(1); + }, 5000); + forceExitTimer.unref(); +} + +process.on("SIGINT", () => shutdown("SIGINT")); +process.on("SIGTERM", () => shutdown("SIGTERM")); diff --git a/packages/web/src/server/terminal-websocket.ts b/packages/web/server/terminal-websocket.ts similarity index 95% rename from packages/web/src/server/terminal-websocket.ts rename to packages/web/server/terminal-websocket.ts index 365e0141a..7f9b724b7 100644 --- a/packages/web/src/server/terminal-websocket.ts +++ b/packages/web/server/terminal-websocket.ts @@ -122,7 +122,7 @@ function getOrSpawnTtyd(sessionId: string): TtydInstance { console.log(`[Terminal] Spawning ttyd for ${sessionId} on port ${port}`); - // Enable mouse mode so scroll works as scrollback, not input cycling + // Enable mouse mode for scrollback support const mouseProc = spawn("tmux", ["set-option", "-t", sessionId, "mouse", "on"]); mouseProc.on("error", (err) => { console.error(`[Terminal] Failed to set mouse mode for ${sessionId}:`, err.message); @@ -158,8 +158,11 @@ function getOrSpawnTtyd(sessionId: string): TtydInstance { const current = instances.get(sessionId); if (current?.process === proc) { instances.delete(sessionId); - // Recycle port for reuse - availablePorts.add(port); + // Only recycle port on clean exit (code 0), not on errors + // Failed ttyd processes may leave ports in TIME_WAIT state + if (code === 0) { + availablePorts.add(port); + } } }); @@ -169,8 +172,7 @@ function getOrSpawnTtyd(sessionId: string): TtydInstance { const current = instances.get(sessionId); if (current?.process === proc) { instances.delete(sessionId); - // Recycle port for reuse - availablePorts.add(port); + // Don't recycle port on error - may still be in use or TIME_WAIT } // Kill any running process try { @@ -192,7 +194,7 @@ const server = createServer(async (req, res) => { // CORS for dashboard - allow requests from the same host as the dashboard // TODO: Replace with proper session-based authentication const origin = req.headers.origin; - if (origin) { + if (origin && origin !== "null") { // Extract hostname from origin and compare with request host try { const originUrl = new URL(origin); @@ -204,6 +206,9 @@ const server = createServer(async (req, res) => { } catch { // Invalid origin URL, don't set CORS header } + } else { + // Allow null origin (file:// or local HTML files) + res.setHeader("Access-Control-Allow-Origin", "*"); } res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); diff --git a/packages/web/src/app/api/sessions/route.ts b/packages/web/src/app/api/sessions/route.ts index 9e18c10f9..5a06060f3 100644 --- a/packages/web/src/app/api/sessions/route.ts +++ b/packages/web/src/app/api/sessions/route.ts @@ -1,4 +1,4 @@ -import type { Session, ProjectConfig } from "@composio/ao-core"; +import { ACTIVITY_STATE, type Session, type ProjectConfig } from "@composio/ao-core"; import { NextResponse } from "next/server"; import { getServices, getSCM, getTracker } from "@/lib/services"; import { sessionToDashboard, enrichSessionPR, enrichSessionIssue, computeStats } from "@/lib/serialize"; @@ -23,15 +23,32 @@ function resolveProject( return firstKey ? projects[firstKey] : undefined; } -/** GET /api/sessions โ€” List all sessions with full state */ -export async function GET() { +/** GET /api/sessions โ€” List all sessions with full state + * Query params: + * - active=true: Only return non-exited sessions + */ +export async function GET(request: Request) { try { + const { searchParams } = new URL(request.url); + const activeOnly = searchParams.get("active") === "true"; + const { config, registry, sessionManager } = await getServices(); const coreSessions = await sessionManager.list(); // Filter out orchestrator sessions โ€” they get their own button, not a card - const workerSessions = coreSessions.filter((s) => !s.id.endsWith("-orchestrator")); - const dashboardSessions = workerSessions.map(sessionToDashboard); + let workerSessions = coreSessions.filter((s) => !s.id.endsWith("-orchestrator")); + + // Convert to dashboard format + let dashboardSessions = workerSessions.map(sessionToDashboard); + + // Filter to active sessions only if requested (keep workerSessions in sync) + if (activeOnly) { + const activeIndices = dashboardSessions + .map((s, i) => (s.activity !== ACTIVITY_STATE.EXITED ? i : -1)) + .filter((i) => i !== -1); + workerSessions = activeIndices.map((i) => workerSessions[i]); + dashboardSessions = activeIndices.map((i) => dashboardSessions[i]); + } // Enrich issue labels using tracker plugin (synchronous) workerSessions.forEach((core, i) => { diff --git a/packages/web/src/app/dev/terminal-test/page.tsx b/packages/web/src/app/dev/terminal-test/page.tsx new file mode 100644 index 000000000..c928e9042 --- /dev/null +++ b/packages/web/src/app/dev/terminal-test/page.tsx @@ -0,0 +1,533 @@ +"use client"; + +import { DirectTerminal } from "@/components/DirectTerminal"; +import { Terminal } from "@/components/Terminal"; +import { useSearchParams } from "next/navigation"; +import { useState, useEffect, Suspense } from "react"; + +// Force dynamic rendering (required for useSearchParams) +export const dynamic = "force-dynamic"; + +/** + * Terminal Implementation Test & Documentation + * + * This page compares two terminal implementations and documents why DirectTerminal + * (with XDA support) was necessary for proper clipboard functionality. + * + * By default, automatically picks two different sessions to avoid port conflicts. + * + * Examples: + * - http://localhost:3000/dev/terminal-test (auto-picks two different sessions) + * - http://localhost:3000/dev/terminal-test?old_session=ao-orchestrator&new_session=ao-20 + * - http://localhost:3000/dev/terminal-test?session=ao-20 (uses same session for both) + * + * Note: Using different sessions for old/new avoids port conflicts when both render simultaneously. + */ +function TerminalTestPageContent() { + const searchParams = useSearchParams(); + const [availableSessions, setAvailableSessions] = useState([]); + const [showComparison, setShowComparison] = useState(true); + + // Fetch available sessions on mount (only active ones) + useEffect(() => { + fetch("/api/sessions?active=true") + .then((res) => res.json()) + .then((data) => { + if (data.sessions && Array.isArray(data.sessions)) { + const ids = data.sessions.map((s: { id: string }) => s.id); + setAvailableSessions(ids); + } + }) + .catch((err) => { + console.error("Failed to fetch sessions:", err); + }); + }, []); + + // Determine which sessions to use + const sessionParam = searchParams.get("session"); + const oldSessionParam = searchParams.get("old_session"); + const newSessionParam = searchParams.get("new_session"); + + // If no params provided, use first two available sessions (or fallback to defaults) + const defaultOldSession = availableSessions[0] || "ao-orchestrator"; + const defaultNewSession = availableSessions[1] || availableSessions[0] || "ao-orchestrator"; + + // Allow overriding individual sessions + const oldSessionId = oldSessionParam || (sessionParam || defaultOldSession); + const newSessionId = newSessionParam || (sessionParam || defaultNewSession); + + return ( +
+
+ {/* Header */} +
+

+ Terminal Implementation Test & Documentation +

+

+ Comparing sessions: + OLD: {oldSessionId} + NEW: {newSessionId} +

+
+ + {/* The Problem */} +
+

๐Ÿ› The Problem

+
+

+ Issue: Browser clipboard (Cmd+C/Ctrl+C) + only worked when an iTerm2 client was attached to the tmux session. +

+

+ Impact: Users had to keep iTerm2 tabs open + in the background for clipboard to work in the web dashboard. +

+

+ Investigation time: 12+ hours of debugging + across tmux, ttyd, xterm.js, and macOS clipboard systems. +

+
+
+ + {/* Root Cause */} +
+

๐Ÿ” Root Cause Analysis

+
+
+

+ 1. How tmux Clipboard Works (OSC 52) +

+
    +
  • tmux uses OSC 52 escape sequences to synchronize clipboard with terminals
  • +
  • Format: \x1b]52;c;<base64>\x07
  • +
  • Terminal must support OSC 52 and have proper capabilities declared
  • +
+
+ +
+

+ 2. tmux Capability Detection (XDA) +

+
    +
  • tmux queries terminal capabilities using Device Attributes (DA/XDA)
  • +
  • + XDA query: CSI > q (also called XTVERSION) +
  • +
  • + Terminal responds with identification string containing terminal type (e.g., "XTerm(370)", "iTerm2 ") +
  • +
  • Based on response, tmux enables features like TTYC_MS (clipboard support)
  • +
+
+ +
+

3. The Missing Piece

+
+

+ xterm.js does NOT implement XDA (Extended Device Attributes) +

+
    +
  • + XDA is marked as TODO in xterm.js codebase:{" "} + + test.skip('CSI > Ps q - Report xterm name and version (XTVERSION)') + +
  • +
  • Without XDA response, tmux doesn't recognize the terminal as clipboard-capable
  • +
  • tmux never emits OSC 52 sequences โ†’ clipboard doesn't work
  • +
+
+
+ +
+

+ 4. Why iTerm2 "Fixed" It +

+
    +
  • + iTerm2 sends proper XDA response identifying itself as{" "} + "iTerm2 " +
  • +
  • tmux detects this and enables clipboard for the entire session
  • +
  • OSC 52 sequences are then sent to ALL clients, including browser (ttyd/xterm.js)
  • +
  • This is why clipboard "magically worked" when iTerm2 was attached
  • +
+
+
+
+ + {/* The Solution */} +
+

โœ… The Solution

+
+
+

DirectTerminal Implementation

+

+ Created custom terminal component that registers an XDA handler using xterm.js parser API: +

+
+                
+                  {`terminal.parser.registerCsiHandler(
+  { prefix: ">", final: "q" }, // CSI > q is XDA query
+  () => {
+    // Respond with XTerm identification
+    terminal.write("\\x1bP>|XTerm(370)\\x1b\\\\");
+    return true;
+  }
+);`}
+                
+              
+
+ +
+

What This Does

+
    +
  1. Intercepts XDA queries from tmux
  2. +
  3. + Responds with XTerm(370) identification +
  4. +
  5. tmux detects "XTerm(" in response and enables TTYC_MS capability
  6. +
  7. OSC 52 sequences now flow: tmux โ†’ WebSocket โ†’ xterm.js โ†’ navigator.clipboard
  8. +
  9. + Clipboard works without iTerm2! +
  10. +
+
+ +
+

Architecture Changes

+
    +
  • + Old: Browser โ†’ ttyd (iframe) โ†’ tmux +
  • +
  • + New: Browser โ†’ Custom WebSocket (node-pty) โ†’ tmux +
  • +
  • Bypasses ttyd for direct control over terminal initialization
  • +
  • Full control over escape sequence handling and capabilities
  • +
+
+
+
+ + {/* Node Version Requirement */} +
+

โš ๏ธ Node Version Requirement

+
+
+

+ CRITICAL: This implementation requires Node 20.x (currently 20.20.0) +

+
+ +
+

Why Node 20?

+
    +
  • + node-pty 1.1.0 is incompatible with Node 25+ +
  • +
  • + Error on Node 25.6.1:{" "} + posix_spawnp failed +
  • +
  • + Root cause: node-pty's native module (darwin-arm64 prebuild) fails to spawn processes on Node 25 +
  • +
  • No darwin-arm64 prebuilds available that work with Node 25
  • +
  • Building from source also fails with the same error
  • +
+
+ +
+

When Can We Upgrade?

+
    +
  • + Option 1: Wait for node-pty 1.2.0 stable release with Node 25+ support +
  • +
  • + Option 2: Test node-pty beta versions (currently 1.2.0-beta.11) +
  • +
  • + Option 3: Switch to alternative PTY library (e.g., xterm-pty, node-child-pty) +
  • +
+
+ +
+

+ โšก Testing Instructions for Upgrades +

+

Before upgrading Node or node-pty:

+
    +
  1. + Test node-pty directly:{" "} + + node -e "const pty = require('node-pty'); pty.spawn('/bin/bash', [], {})" + +
  2. +
  3. + If no posix_spawnp failed error, proceed +
  4. +
  5. Start dev servers and open this page
  6. +
  7. Test DirectTerminal: verify connection, clipboard, resize
  8. +
  9. Test multiple sessions to ensure stability
  10. +
  11. If all tests pass, upgrade is safe
  12. +
+
+
+
+ + {/* Comparison Toggle */} +
+
+ + + Compare old (ttyd iframe) vs new (DirectTerminal with XDA) + +
+ {oldSessionId === newSessionId && ( +
+ โš ๏ธ Using same session for both terminals. To avoid port conflicts, use different sessions: + ?old_session=ao-orchestrator&new_session=ao-20 +
+ )} +
+ + {/* Side-by-Side Comparison */} + {showComparison && ( +
+
+ {/* Old Implementation */} +
+
+ + OLD + +

+ ttyd iframe (no XDA) +

+
+
+ โŒ Clipboard requires iTerm2 attached +
+ โœ… Battle-tested (ttyd) +
+ โŒ No control over capabilities +
+ +
+ + {/* New Implementation */} +
+
+ + NEW + +

+ DirectTerminal (with XDA) +

+
+
+ โœ… Clipboard works standalone +
+ โœ… Full control over terminal +
+ โš ๏ธ Requires Node 20.x (node-pty) +
+ +
+
+ +
+

+ ๐Ÿงช How to Test Clipboard +

+
    +
  1. Drag-select text in BOTH terminals above
  2. +
  3. Press Cmd+C (macOS) or Ctrl+Shift+C (Linux/Windows)
  4. +
  5. Paste in another application (Notes, VS Code, etc.)
  6. +
  7. + + Expected: RIGHT terminal works, LEFT may not (unless iTerm2 is attached) + +
  8. +
+
+
+ )} + + {/* Debugging Journey */} +
+

๐Ÿ”ฌ The Debugging Journey

+ +
+
+

+ Total Time Wasted: 12+ hours across Feb 15-16, 2026 +

+
+ +
+

โŒ What We Tried (That Didn't Work)

+
    +
  1. + Suspected ttyd clipboard handling - Spent hours analyzing ttyd source code, checking OSC 52 + passthrough. ttyd was innocent - it passes escape sequences correctly. +
  2. +
  3. + Blamed xterm.js configuration - Tried every possible xterm.js option, clipboard addon + configurations, terminal type settings. None made a difference. +
  4. +
  5. + Investigated macOS clipboard permissions - Checked browser permissions, sandbox attributes, + navigator.clipboard API. All were correct. +
  6. +
  7. + Suspected WebSocket encoding issues - Checked binary vs text mode, UTF-8 encoding, + base64 handling. All correct. +
  8. +
  9. + Tried force-enabling tmux clipboard - Used + set-option -s set-clipboard on in tmux.conf. Didn't help - tmux needs the terminal to declare support. +
  10. +
+
+ +
+

๐Ÿ’ก The Breakthrough

+
+

+ What finally worked: Registering an XDA (Extended Device Attributes) handler in xterm.js + using terminal.parser.registerCsiHandler() +

+
+                  
+{`terminal.parser.registerCsiHandler(
+  { prefix: ">", final: "q" },
+  () => {
+    terminal.write("\\x1bP>|XTerm(370)\\x1b\\\\");
+    return true;
+  }
+);`}
+                  
+                
+

+ This single handler made tmux recognize our terminal as clipboard-capable. Clipboard immediately started working. +

+
+
+ +
+

๐ŸŽฏ How We Finally Figured It Out

+
    +
  1. + Deep-dive into tmux source code - Used DeepWiki.com to analyze tmux's terminal capability + detection logic in tty-keys.c and{" "} + tty.c +
  2. +
  3. + Discovered XDA queries - Found that tmux sends{" "} + CSI > q (XTVERSION) to detect terminal type +
  4. +
  5. + Traced the "iTerm2 magic" - Realized why clipboard worked when iTerm2 was attached: + iTerm2 responds to XDA queries, enabling clipboard for the entire tmux session +
  6. +
  7. + Checked xterm.js implementation - Found that XDA is marked as TODO in xterm.js tests:{" "} + + test.skip('CSI > Ps q - Report xterm name and version (XTVERSION)') + +
  8. +
  9. + Implemented custom handler - Used xterm.js parser API to register our own XDA handler +
  10. +
+
+ +
+

+ โšก How We Could Have Figured It Out Faster +

+
    +
  1. + Start with tmux source code first - Instead of debugging xterm.js and ttyd, we should have + immediately checked how tmux detects terminal capabilities +
  2. +
  3. + Monitor escape sequences - Running{" "} + tmux -vvv or using a terminal protocol analyzer + would have revealed the XDA queries being sent +
  4. +
  5. + Compare working vs broken scenarios - Should have captured and diffed the escape sequences + when iTerm2 was attached vs not attached earlier +
  6. +
  7. + Check xterm.js issues/limitations first - A GitHub search for "xterm.js XDA" or + "xterm.js device attributes" would have revealed it's unimplemented +
  8. +
  9. + Read tmux documentation on terminal types - tmux man page mentions terminal capability + detection, but we focused on clipboard settings instead +
  10. +
+
+ +
+

๐Ÿ“š Key Resources

+
    +
  • tmux source analysis: DeepWiki.com (Feb 15, 2026)
  • +
  • xterm.js parser API documentation
  • +
  • XTerm Control Sequences: XTVERSION / Device Attributes
  • +
  • tmux tty-keys.c: Terminal type detection logic
  • +
+
+
+
+ + {/* Implementation Files */} +
+

๐Ÿ“ Implementation Files

+
    +
  • + + packages/web/src/components/DirectTerminal.tsx + - Main component with XDA handler +
  • +
  • + + packages/web/server/direct-terminal-ws.ts + - WebSocket server using node-pty +
  • +
  • + + packages/web/src/app/dev/terminal-test/page.tsx + - This test page +
  • +
+
+ + {/* Footer */} +
+

Investigation: Feb 15-16, 2026 โ€ข Duration: 12+ hours โ€ข Status: โœ… Resolved

+

Node 20.20.0 โ€ข node-pty 1.1.0 โ€ข xterm.js 5.3.0

+
+
+
+ ); +} + +export default function TerminalTestPage() { + return ( + Loading...}> + + + ); +} diff --git a/packages/web/src/app/test-direct/page.tsx b/packages/web/src/app/test-direct/page.tsx new file mode 100644 index 000000000..053768136 --- /dev/null +++ b/packages/web/src/app/test-direct/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { DirectTerminal } from "@/components/DirectTerminal"; +import { useSearchParams } from "next/navigation"; +import { Suspense } from "react"; + +// Force dynamic rendering (required for useSearchParams) +export const dynamic = "force-dynamic"; + +/** + * Test page for DirectTerminal with XDA clipboard support. + * + * Examples: + * - http://localhost:3000/test-direct + * - http://localhost:3000/test-direct?session=ao-20 + * - http://localhost:3000/test-direct?session=ao-20&fullscreen=true + * + * This uses native xterm.js with registered XDA handler, + * which should enable clipboard (OSC 52) in tmux without + * requiring iTerm2 attachment. + */ +function TestDirectPageContent() { + const searchParams = useSearchParams(); + const startFullscreen = searchParams.get("fullscreen") === "true"; + const sessionId = searchParams.get("session") || "ao-orchestrator"; + return ( +
+
+
+

+ DirectTerminal Test - XDA Clipboard Support +

+

+ This terminal has XDA (Extended Device Attributes) handler registered. +
+ tmux should recognize it as XTerm and enable clipboard support (OSC 52). +

+
+

+ Testing: {sessionId} +

+

Test Steps:

+
    +
  1. Connected to tmux session: {sessionId}
  2. +
  3. + Verify XDA badge shows โœ“ XDA +
  4. +
  5. Drag-select text in the terminal
  6. +
  7. Press Cmd+C (macOS) or Ctrl+C (Linux/Windows)
  8. +
  9. Paste elsewhere to verify clipboard works
  10. +
  11. + Expected: Clipboard works without iTerm2 attachment! +
  12. +
+
+
+

Technical Details:

+
    +
  • Registers CSI > q (XDA) handler in xterm.js parser
  • +
  • Responds with DCS > | XTerm(370) ST sequence
  • +
  • tmux detects "XTerm(" and enables TTYC_MS (clipboard capability)
  • +
  • OSC 52 sequences flow: tmux โ†’ WebSocket โ†’ xterm.js โ†’ navigator.clipboard
  • +
+
+
+ + {/* Test with specified session - key forces remount on fullscreen change */} + +
+
+ ); +} + +export default function TestDirectPage() { + return ( + Loading...}> + + + ); +} diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx new file mode 100644 index 000000000..27deb6524 --- /dev/null +++ b/packages/web/src/components/DirectTerminal.tsx @@ -0,0 +1,324 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; +import { cn } from "@/lib/cn"; + +// Import xterm CSS (must be imported in client component) +import "xterm/css/xterm.css"; + +// Dynamically import xterm types for TypeScript +import type { Terminal as TerminalType } from "xterm"; +import type { FitAddon as FitAddonType } from "@xterm/addon-fit"; + +interface DirectTerminalProps { + sessionId: string; + startFullscreen?: boolean; +} + +/** + * Direct xterm.js terminal with native WebSocket connection. + * Implements Extended Device Attributes (XDA) handler to enable + * tmux clipboard support (OSC 52) without requiring iTerm2 attachment. + * + * Based on DeepWiki analysis: + * - tmux queries for XDA (CSI > q / XTVERSION) to detect terminal type + * - When tmux sees "XTerm(" in response, it enables TTYC_MS (clipboard) + * - xterm.js doesn't implement XDA by default, so we register custom handler + */ +export function DirectTerminal({ sessionId, startFullscreen = false }: DirectTerminalProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const terminalRef = useRef(null); + const terminalInstance = useRef(null); + const fitAddon = useRef(null); + const ws = useRef(null); + const [fullscreen, setFullscreen] = useState(startFullscreen); + const [status, setStatus] = useState<"connecting" | "connected" | "error">("connecting"); + const [error, setError] = useState(null); + + // Update URL when fullscreen changes + useEffect(() => { + const params = new URLSearchParams(searchParams.toString()); + + if (fullscreen) { + params.set("fullscreen", "true"); + } else { + params.delete("fullscreen"); + } + + const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname; + router.replace(newUrl, { scroll: false }); + }, [fullscreen, pathname, router, searchParams]); + + useEffect(() => { + if (!terminalRef.current) return; + // Prevent retry loop on persistent errors + if (error && status === "error") return; + + // Dynamically import xterm.js to avoid SSR issues + let mounted = true; + let cleanup: (() => void) | null = null; + + Promise.all([ + import("xterm").then((mod) => mod.Terminal), + import("@xterm/addon-fit").then((mod) => mod.FitAddon), + import("@xterm/addon-web-links").then((mod) => mod.WebLinksAddon), + ]) + .then(([Terminal, FitAddon, WebLinksAddon]) => { + if (!mounted || !terminalRef.current) return; + + // Initialize xterm.js Terminal + const terminal = new Terminal({ + cursorBlink: true, + fontSize: 14, + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + theme: { + background: "#000000", + foreground: "#ffffff", + cursor: "#ffffff", + cursorAccent: "#000000", + selectionBackground: "rgba(255, 255, 255, 0.3)", + }, + scrollback: 10000, + allowProposedApi: true, // Required for some advanced features + // Smooth scrolling configuration + fastScrollModifier: "alt", + fastScrollSensitivity: 3, + scrollSensitivity: 1, + }); + + // Add FitAddon for responsive sizing + const fit = new FitAddon(); + terminal.loadAddon(fit); + fitAddon.current = fit; + + // Add WebLinksAddon for clickable links + const webLinks = new WebLinksAddon(); + terminal.loadAddon(webLinks); + + // **CRITICAL FIX**: Register XDA (Extended Device Attributes) handler + // This makes tmux recognize our terminal and enable clipboard support + terminal.parser.registerCsiHandler( + { prefix: ">", final: "q" }, // CSI > q is XTVERSION / XDA + () => { + // Respond with XTerm identification that tmux recognizes + // tmux looks for "XTerm(" in the response (see tmux tty-keys.c) + // Format: DCS > | XTerm(version) ST + // DCS = \x1bP, ST = \x1b\\ + terminal.write("\x1bP>|XTerm(370)\x1b\\"); + console.log("[DirectTerminal] Sent XDA response for clipboard support"); + return true; // Handled + }, + ); + + // Open terminal in DOM + terminal.open(terminalRef.current); + terminalInstance.current = terminal; + + // Fit terminal to container + fit.fit(); + + // Connect WebSocket + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const hostname = window.location.hostname; + const port = process.env.NEXT_PUBLIC_DIRECT_TERMINAL_PORT ?? "3003"; + const wsUrl = `${protocol}//${hostname}:${port}/ws?session=${encodeURIComponent(sessionId)}`; + + console.log("[DirectTerminal] Connecting to:", wsUrl); + const websocket = new WebSocket(wsUrl); + ws.current = websocket; + + websocket.binaryType = "arraybuffer"; + + websocket.onopen = () => { + console.log("[DirectTerminal] WebSocket connected"); + setStatus("connected"); + setError(null); + + // Send initial size + websocket.send( + JSON.stringify({ + type: "resize", + cols: terminal.cols, + rows: terminal.rows, + }), + ); + }; + + websocket.onmessage = (event) => { + const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data); + terminal.write(data); + }; + + websocket.onerror = (event) => { + console.error("[DirectTerminal] WebSocket error:", event); + setStatus("error"); + setError("WebSocket connection error"); + }; + + websocket.onclose = (event) => { + console.log("[DirectTerminal] WebSocket closed:", event.code, event.reason); + if (status === "connected") { + setStatus("error"); + setError("Connection closed"); + } + }; + + // Terminal input โ†’ WebSocket + const disposable = terminal.onData((data) => { + if (websocket.readyState === WebSocket.OPEN) { + websocket.send(data); + } + }); + + // Handle window resize + const handleResize = () => { + if (fit && websocket.readyState === WebSocket.OPEN) { + fit.fit(); + websocket.send( + JSON.stringify({ + type: "resize", + cols: terminal.cols, + rows: terminal.rows, + }), + ); + } + }; + + window.addEventListener("resize", handleResize); + + // Store cleanup function to be called from useEffect cleanup + cleanup = () => { + window.removeEventListener("resize", handleResize); + disposable.dispose(); + websocket.close(); + terminal.dispose(); + }; + }) + .catch((err) => { + console.error("[DirectTerminal] Failed to load xterm.js:", err); + setStatus("error"); + setError("Failed to load terminal"); + }); + + return () => { + mounted = false; + cleanup?.(); + }; + }, [sessionId]); + + // Re-fit terminal when fullscreen changes + useEffect(() => { + const fit = fitAddon.current; + const terminal = terminalInstance.current; + const websocket = ws.current; + const container = terminalRef.current; + + if (!fit || !terminal || !websocket || websocket.readyState !== WebSocket.OPEN || !container) { + return; + } + + let resizeAttempts = 0; + const maxAttempts = 10; + + const resizeTerminal = () => { + resizeAttempts++; + + // Get container dimensions + const rect = container.getBoundingClientRect(); + const expectedHeight = rect.height; + + // Check if container has reached target dimensions (within 10px tolerance) + const isFullscreenTarget = fullscreen ? expectedHeight > window.innerHeight - 100 : expectedHeight < 700; + + if (!isFullscreenTarget && resizeAttempts < maxAttempts) { + // Container hasn't reached target size yet, try again + requestAnimationFrame(resizeTerminal); + return; + } + + // Container is at target size, now resize terminal + terminal.refresh(0, terminal.rows - 1); + fit.fit(); + terminal.refresh(0, terminal.rows - 1); + + // Send new size to server + websocket.send( + JSON.stringify({ + type: "resize", + cols: terminal.cols, + rows: terminal.rows, + }), + ); + }; + + // Start resize polling + requestAnimationFrame(resizeTerminal); + + // Also try on transitionend + const handleTransitionEnd = (e: TransitionEvent) => { + if (e.target === container.parentElement) { + resizeAttempts = 0; + setTimeout(() => requestAnimationFrame(resizeTerminal), 50); + } + }; + + const parent = container.parentElement; + parent?.addEventListener("transitionend", handleTransitionEnd); + + // Backup timers in case RAF polling doesn't work + const timer1 = setTimeout(() => { resizeAttempts = 0; resizeTerminal(); }, 300); + const timer2 = setTimeout(() => { resizeAttempts = 0; resizeTerminal(); }, 600); + + return () => { + parent?.removeEventListener("transitionend", handleTransitionEnd); + clearTimeout(timer1); + clearTimeout(timer2); + }; + }, [fullscreen]); + + const statusColor = + status === "connected" ? "bg-[#3fb950]" : status === "error" ? "bg-[#f85149]" : "bg-[#d29922] animate-pulse"; + + const statusText = + status === "connected" ? "Connected" : status === "error" ? error ?? "Error" : "Connecting..."; + + const statusTextColor = + status === "connected" + ? "text-[var(--color-accent-green)]" + : status === "error" + ? "text-[var(--color-accent-red)]" + : "text-[var(--color-text-muted)]"; + + return ( +
+
+
+ {sessionId} + {statusText} + + XDA + + +
+
+
+ ); +} diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx index 1f9164f80..9c0204e52 100644 --- a/packages/web/src/components/SessionDetail.tsx +++ b/packages/web/src/components/SessionDetail.tsx @@ -1,12 +1,13 @@ "use client"; import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; import { type DashboardSession, type DashboardPR, } from "@/lib/types"; import { CICheckList } from "./CIBadge"; -import { Terminal } from "./Terminal"; +import { DirectTerminal } from "./DirectTerminal"; interface SessionDetailProps { session: DashboardSession; @@ -105,6 +106,8 @@ async function askAgentToFix( } export function SessionDetail({ session }: SessionDetailProps) { + const searchParams = useSearchParams(); + const startFullscreen = searchParams.get("fullscreen") === "true"; const pr = session.pr; const activity = activityLabel[session.activity] ?? { label: session.activity, @@ -236,7 +239,7 @@ export function SessionDetail({ session }: SessionDetailProps) { Terminal - +
diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts index 65a83177d..696020e6c 100644 --- a/packages/web/src/lib/types.ts +++ b/packages/web/src/lib/types.ts @@ -15,16 +15,17 @@ export type { ReviewDecision, MergeReadiness, PRState, -} from "@composio/ao-core"; +} from "@composio/ao-core/types"; -import type { - CICheck as CoreCICheck, - MergeReadiness, - CIStatus, - SessionStatus, - ActivityState, - ReviewDecision, -} from "@composio/ao-core"; +import { + ACTIVITY_STATE, + type CICheck as CoreCICheck, + type MergeReadiness, + type CIStatus, + type SessionStatus, + type ActivityState, + type ReviewDecision, +} from "@composio/ao-core/types"; /** * Attention zone priority level, ordered by human action urgency: @@ -166,7 +167,7 @@ export function getAttentionLevel(session: DashboardSession): AttentionLevel { } // โ”€โ”€ Respond: agent is waiting for human input โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if (session.activity === "waiting_input" || session.activity === "blocked") { + if (session.activity === ACTIVITY_STATE.WAITING_INPUT || session.activity === ACTIVITY_STATE.BLOCKED) { return "respond"; } if ( @@ -177,7 +178,7 @@ export function getAttentionLevel(session: DashboardSession): AttentionLevel { return "respond"; } // Exited agent with non-terminal status = crashed, needs human attention - if (session.activity === "exited") { + if (session.activity === ACTIVITY_STATE.EXITED) { return "respond"; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1157163e8..8d9fdfd9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -457,15 +457,30 @@ importers: '@composio/ao-plugin-workspace-worktree': specifier: workspace:* version: link:../plugins/workspace-worktree + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/addon-web-links': + specifier: ^0.12.0 + version: 0.12.0 next: specifier: ^15.1.0 version: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + node-pty: + specifier: ^1.1.0 + version: 1.1.0 react: specifier: ^19.0.0 version: 19.2.4 react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + ws: + specifier: ^8.19.0 + version: 8.19.0 + xterm: + specifier: ^5.3.0 + version: 5.3.0 devDependencies: '@tailwindcss/postcss': specifier: ^4.0.0 @@ -482,6 +497,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 '@vitejs/plugin-react': specifier: ^4.3.0 version: 4.7.0(vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2)) @@ -491,6 +509,9 @@ importers: jsdom: specifier: ^25.0.0 version: 25.0.1 + node-gyp: + specifier: ^12.2.0 + version: 12.2.0 playwright: specifier: ^1.49.0 version: 1.58.2 @@ -1289,6 +1310,10 @@ packages: resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} engines: {node: '>=18'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1415,6 +1440,14 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/agent@4.0.0': + resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -1720,6 +1753,9 @@ packages: '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.55.0': resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1876,6 +1912,16 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/addon-web-links@0.12.0': + resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1988,6 +2034,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacache@20.0.3: + resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==} + engines: {node: ^20.17.0 || >=22.9.0} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2025,6 +2075,10 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2179,6 +2233,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -2191,6 +2248,13 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2302,6 +2366,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -2378,6 +2445,10 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2422,6 +2493,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@13.0.3: + resolution: {integrity: sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==} + engines: {node: 20 || >=22} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -2453,6 +2528,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2497,6 +2575,10 @@ packages: resolution: {integrity: sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg==} engines: {node: '>=18'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -2552,6 +2634,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + jackspeak@4.2.3: resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} @@ -2732,6 +2818,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2742,6 +2832,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-fetch-happen@15.0.3: + resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} + engines: {node: ^20.17.0 || >=22.9.0} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2778,6 +2872,38 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@5.0.1: + resolution: {integrity: sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2801,6 +2927,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next@15.5.12: resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -2822,9 +2952,25 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-gyp@12.2.0: + resolution: {integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-pty@1.1.0: + resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -2894,6 +3040,10 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -2940,6 +3090,10 @@ packages: resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} engines: {node: '>=0.10.0'} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3005,6 +3159,14 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -3064,6 +3226,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3138,6 +3304,18 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3148,6 +3326,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3216,6 +3398,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar@7.5.7: + resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} + engines: {node: '>=18'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -3328,6 +3514,14 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unique-filename@5.0.0: + resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==} + engines: {node: ^20.17.0 || >=22.9.0} + + unique-slug@6.0.0: + resolution: {integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==} + engines: {node: ^20.17.0 || >=22.9.0} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3543,6 +3737,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -3579,6 +3778,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xterm@5.3.0: + resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} + deprecated: This package is now deprecated. Move to @xterm/xterm instead. + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3586,6 +3789,13 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -4344,6 +4554,10 @@ snapshots: '@isaacs/cliui@9.0.0': {} + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4474,6 +4688,20 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@npmcli/agent@4.0.0': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.2.6 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.7.4 + '@opentelemetry/api@1.9.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -4720,6 +4948,10 @@ snapshots: '@types/wrap-ansi@3.0.0': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.2.3 + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4946,6 +5178,12 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@xterm/addon-fit@0.11.0': {} + + '@xterm/addon-web-links@0.12.0': {} + + abbrev@4.0.0: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5047,6 +5285,20 @@ snapshots: cac@6.7.14: {} + cacache@20.0.3: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.3 + lru-cache: 11.2.6 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 7.0.4 + ssri: 13.0.1 + unique-filename: 5.0.0 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5079,6 +5331,8 @@ snapshots: check-error@2.1.3: {} + chownr@3.0.0: {} + ci-info@3.9.0: {} cli-cursor@5.0.0: @@ -5217,6 +5471,11 @@ snapshots: emoji-regex@8.0.0: {} + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -5229,6 +5488,10 @@ snapshots: entities@6.0.1: {} + env-paths@2.2.1: {} + + err-code@2.0.3: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -5391,6 +5654,8 @@ snapshots: expect-type@1.3.0: {} + exponential-backoff@3.1.3: {} + extendable-error@0.1.7: {} external-editor@3.1.0: @@ -5468,6 +5733,10 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + fsevents@2.3.2: optional: true @@ -5512,6 +5781,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.3: + dependencies: + minimatch: 10.2.0 + minipass: 7.1.2 + path-scurry: 2.0.1 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -5541,6 +5816,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + http-cache-semantics@4.2.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -5588,6 +5865,8 @@ snapshots: run-async: 3.0.0 rxjs: 7.8.2 + ip-address@10.1.0: {} + is-docker@2.2.1: {} is-extglob@2.1.1: {} @@ -5622,6 +5901,8 @@ snapshots: isexe@2.0.0: {} + isexe@4.0.0: {} + jackspeak@4.2.3: dependencies: '@isaacs/cliui': 9.0.0 @@ -5795,6 +6076,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5805,6 +6088,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-fetch-happen@15.0.3: + dependencies: + '@npmcli/agent': 4.0.0 + cacache: 20.0.3 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 5.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + promise-retry: 2.0.1 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -5832,6 +6131,40 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + + minipass-fetch@5.0.1: + dependencies: + minipass: 7.1.2 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.2 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mri@1.2.0: {} ms@2.1.3: {} @@ -5844,6 +6177,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.12 @@ -5868,8 +6203,33 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + + node-gyp@12.2.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + make-fetch-happen: 15.0.3 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + tar: 7.5.7 + tinyglobby: 0.2.15 + which: 6.0.1 + transitivePeerDependencies: + - supports-color + + node-pty@1.1.0: + dependencies: + node-addon-api: 7.1.1 + node-releases@2.0.27: {} + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + nwsapi@2.2.23: {} obug@2.1.1: {} @@ -5938,6 +6298,8 @@ snapshots: p-map@2.1.0: {} + p-map@7.0.4: {} + p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -5978,6 +6340,11 @@ snapshots: dependencies: path-root-regex: 0.1.2 + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.2 + path-type@4.0.0: {} pathe@1.1.2: {} @@ -6026,6 +6393,13 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + proc-log@6.1.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -6076,6 +6450,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.1.0: {} rollup@4.57.1: @@ -6183,6 +6559,21 @@ snapshots: slash@3.0.0: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -6192,6 +6583,10 @@ snapshots: sprintf-js@1.0.3: {} + ssri@13.0.1: + dependencies: + minipass: 7.1.2 + stackback@0.0.2: {} std-env@3.10.0: {} @@ -6249,6 +6644,14 @@ snapshots: tapable@2.3.0: {} + tar@7.5.7: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -6336,6 +6739,14 @@ snapshots: undici-types@7.16.0: {} + unique-filename@5.0.0: + dependencies: + unique-slug: 6.0.0 + + unique-slug@6.0.0: + dependencies: + imurmurhash: 0.1.4 + universalify@0.1.2: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -6555,6 +6966,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -6580,10 +6995,16 @@ snapshots: xmlchars@2.2.0: {} + xterm@5.3.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} + yallist@4.0.0: {} + + yallist@5.0.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {}