feat: implement proper interactive terminal with WebSocket
Replace hacky SSE polling with real-time WebSocket for bidirectional terminal communication. This is a proper interactive terminal - type directly, like tmux attach in the browser. Architecture: - WebSocket server on port 3001 alongside Next.js - Uses tmux pipe-pane for real-time output streaming - Sends input character-by-character via tmux send-keys - Handles terminal resize events - Connection status indicator Implementation: - packages/web/src/server/terminal-websocket.ts: WebSocket server - Terminal component now fully interactive (not read-only) - Runs both servers via concurrently in dev mode - Green dot = connected, red dot = disconnected - Proper cursor, no more clunky input box Benefits: - Real-time streaming (not 2-second polling) - Type directly into terminal - Proper terminal control sequences - Handles resize - Like native tmux attach Dependencies added: - ws (WebSocket server) - @types/ws - concurrently (run multiple servers) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7c099c10eb
commit
c07cbc000a
|
|
@ -5,7 +5,9 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "concurrently \"npm:dev:next\" \"npm:dev:ws\"",
|
||||
"dev:next": "next dev",
|
||||
"dev:ws": "tsx src/server/terminal-websocket.ts",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
@ -26,7 +28,8 @@
|
|||
"@xterm/xterm": "^6.0.0",
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
|
|
@ -34,7 +37,9 @@
|
|||
"@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",
|
||||
"playwright": "^1.49.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -11,26 +11,24 @@ interface TerminalProps {
|
|||
|
||||
/**
|
||||
* Terminal embed using xterm.js.
|
||||
* Streams tmux pane output via SSE. Interactive terminal with direct input.
|
||||
* Proper interactive terminal via WebSocket - type directly, real-time streaming.
|
||||
*/
|
||||
export function Terminal({ sessionId }: TerminalProps) {
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [isInteractive, setIsInteractive] = useState(false);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const xtermRef = useRef<XTerm | null>(null);
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const lastContentRef = useRef<string>("");
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
// Initialize xterm.js
|
||||
useEffect(() => {
|
||||
if (!terminalRef.current) return;
|
||||
|
||||
// Create terminal instance
|
||||
// Create terminal instance - fully interactive
|
||||
const term = new XTerm({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
disableStdin: true, // Will be enabled when interactive mode is on
|
||||
theme: {
|
||||
background: "#000000",
|
||||
foreground: "#d0d0d0",
|
||||
|
|
@ -70,88 +68,84 @@ export function Terminal({ sessionId }: TerminalProps) {
|
|||
// Refit terminal when fullscreen changes
|
||||
useEffect(() => {
|
||||
if (fitAddonRef.current) {
|
||||
setTimeout(() => fitAddonRef.current?.fit(), 150);
|
||||
setTimeout(() => {
|
||||
fitAddonRef.current?.fit();
|
||||
// Send resize to server
|
||||
const term = xtermRef.current;
|
||||
const ws = wsRef.current;
|
||||
if (term && ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "resize",
|
||||
cols: term.cols,
|
||||
rows: term.rows,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}, [fullscreen]);
|
||||
|
||||
// Handle interactive mode
|
||||
// Connect to WebSocket for interactive terminal
|
||||
useEffect(() => {
|
||||
const term = xtermRef.current;
|
||||
if (!term) return;
|
||||
|
||||
if (isInteractive) {
|
||||
term.options.disableStdin = false;
|
||||
term.options.cursorBlink = true;
|
||||
// Connect to WebSocket server
|
||||
const wsUrl = `ws://localhost:${process.env.NEXT_PUBLIC_WS_PORT ?? "3001"}?session=${sessionId}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
// Handle keyboard input
|
||||
const disposable = term.onData((data) => {
|
||||
// Send each keystroke to the session
|
||||
void (async () => {
|
||||
try {
|
||||
await fetch(`/api/sessions/${sessionId}/send`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: data }),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[Terminal] Failed to send input:", err);
|
||||
}
|
||||
})();
|
||||
});
|
||||
ws.addEventListener("open", () => {
|
||||
setConnected(true);
|
||||
console.log("[Terminal] WebSocket connected");
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
} else {
|
||||
term.options.disableStdin = true;
|
||||
term.options.cursorBlink = false;
|
||||
}
|
||||
}, [isInteractive, sessionId]);
|
||||
// Send initial terminal size
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "resize",
|
||||
cols: term.cols,
|
||||
rows: term.rows,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Connect to SSE stream
|
||||
useEffect(() => {
|
||||
const term = xtermRef.current;
|
||||
if (!term) return;
|
||||
ws.addEventListener("message", (event) => {
|
||||
// Write output from server
|
||||
term.write(event.data);
|
||||
});
|
||||
|
||||
const eventSource = new EventSource(`/api/sessions/${sessionId}/terminal`);
|
||||
eventSourceRef.current = eventSource;
|
||||
ws.addEventListener("error", (err) => {
|
||||
console.error("[Terminal] WebSocket error:", err);
|
||||
term.writeln("\r\n\r\n[Connection error]");
|
||||
setConnected(false);
|
||||
});
|
||||
|
||||
eventSource.addEventListener("message", (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as
|
||||
| { type: "snapshot" | "update"; content: string }
|
||||
| { type: "exit" };
|
||||
ws.addEventListener("close", () => {
|
||||
console.log("[Terminal] WebSocket closed");
|
||||
term.writeln("\r\n\r\n[Connection closed]");
|
||||
setConnected(false);
|
||||
});
|
||||
|
||||
if (data.type === "snapshot" || data.type === "update") {
|
||||
const content = data.content;
|
||||
|
||||
// Only update if content changed
|
||||
if (content !== lastContentRef.current) {
|
||||
lastContentRef.current = content;
|
||||
|
||||
// Reset terminal and write new content
|
||||
term.reset();
|
||||
term.write(content);
|
||||
|
||||
// Scroll to bottom
|
||||
term.scrollToBottom();
|
||||
}
|
||||
} else if (data.type === "exit") {
|
||||
term.writeln("\r\n\r\n[Session exited]");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Terminal] Failed to parse SSE event:", err);
|
||||
// Handle keyboard input - send to server
|
||||
const inputDisposable = term.onData((data) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "input", data }));
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("error", () => {
|
||||
eventSource.close();
|
||||
term.writeln("\r\n\r\n[Connection lost]");
|
||||
// Handle terminal resize - send to server
|
||||
const resizeDisposable = term.onResize(({ cols, rows }) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
inputDisposable.dispose();
|
||||
resizeDisposable.dispose();
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
|
|
@ -164,15 +158,25 @@ export function Terminal({ sessionId }: TerminalProps) {
|
|||
>
|
||||
<div className="flex items-center gap-2 border-b border-[var(--color-border-default)] bg-[var(--color-bg-tertiary)] px-3 py-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#f85149]" />
|
||||
<div
|
||||
className={cn(
|
||||
"h-2.5 w-2.5 rounded-full transition-colors",
|
||||
connected ? "bg-[#3fb950]" : "bg-[#f85149]",
|
||||
)}
|
||||
/>
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#d29922]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#3fb950]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#484f58]" />
|
||||
</div>
|
||||
<span className="font-[var(--font-mono)] text-xs text-[var(--color-text-muted)]">
|
||||
{sessionId}
|
||||
</span>
|
||||
<span className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||
Read-only
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] font-medium uppercase tracking-wide",
|
||||
connected ? "text-[var(--color-accent-green)]" : "text-[var(--color-text-muted)]",
|
||||
)}
|
||||
>
|
||||
{connected ? "Interactive" : "Connecting..."}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* WebSocket server for interactive terminal sessions.
|
||||
*
|
||||
* Runs alongside Next.js on port 3001.
|
||||
* Provides bidirectional streaming for tmux sessions.
|
||||
*/
|
||||
|
||||
import { WebSocketServer, type WebSocket } from "ws";
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
interface TerminalSession {
|
||||
sessionId: string;
|
||||
ws: WebSocket;
|
||||
captureProcess: ChildProcess | null;
|
||||
}
|
||||
|
||||
const sessions = new Map<string, TerminalSession>();
|
||||
|
||||
const server = createServer();
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
wss.on("connection", (ws, req) => {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
const sessionId = url.searchParams.get("session");
|
||||
|
||||
if (!sessionId) {
|
||||
ws.close(1008, "Missing session parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebSocket] Client connected to session: ${sessionId}`);
|
||||
|
||||
// Create session
|
||||
const session: TerminalSession = {
|
||||
sessionId,
|
||||
ws,
|
||||
captureProcess: null,
|
||||
};
|
||||
|
||||
sessions.set(sessionId, session);
|
||||
|
||||
// Start streaming tmux output using tmux pipe-pane
|
||||
// This continuously pipes output to a command
|
||||
const captureProcess = spawn("tmux", [
|
||||
"pipe-pane",
|
||||
"-t",
|
||||
sessionId,
|
||||
"-O",
|
||||
"cat",
|
||||
]);
|
||||
|
||||
session.captureProcess = captureProcess;
|
||||
|
||||
// Stream output to WebSocket
|
||||
captureProcess.stdout?.on("data", (data: Buffer) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(data.toString("utf-8"));
|
||||
}
|
||||
});
|
||||
|
||||
captureProcess.stderr?.on("data", (data: Buffer) => {
|
||||
console.error(`[WebSocket] tmux pipe-pane error:`, data.toString());
|
||||
});
|
||||
|
||||
captureProcess.on("exit", (code) => {
|
||||
console.log(`[WebSocket] pipe-pane exited for ${sessionId} with code ${code}`);
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, "Session ended");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle input from client
|
||||
ws.on("message", (message) => {
|
||||
const data = message.toString("utf-8");
|
||||
|
||||
// Parse message: could be input or control command
|
||||
try {
|
||||
const msg = JSON.parse(data) as { type: string; data?: string; cols?: number; rows?: number };
|
||||
|
||||
if (msg.type === "input" && msg.data) {
|
||||
// Send input to tmux session
|
||||
const sendProcess = spawn("tmux", [
|
||||
"send-keys",
|
||||
"-t",
|
||||
sessionId,
|
||||
"-l",
|
||||
msg.data,
|
||||
]);
|
||||
|
||||
sendProcess.on("error", (err) => {
|
||||
console.error(`[WebSocket] Failed to send keys:`, err);
|
||||
});
|
||||
} else if (msg.type === "resize" && msg.cols && msg.rows) {
|
||||
// Resize tmux pane
|
||||
const resizeProcess = spawn("tmux", [
|
||||
"resize-pane",
|
||||
"-t",
|
||||
sessionId,
|
||||
"-x",
|
||||
String(msg.cols),
|
||||
"-y",
|
||||
String(msg.rows),
|
||||
]);
|
||||
|
||||
resizeProcess.on("error", (err) => {
|
||||
console.error(`[WebSocket] Failed to resize:`, err);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, treat as raw input
|
||||
const sendProcess = spawn("tmux", ["send-keys", "-t", sessionId, "-l", data]);
|
||||
sendProcess.on("error", (err) => {
|
||||
console.error(`[WebSocket] Failed to send keys:`, err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnect
|
||||
ws.on("close", () => {
|
||||
console.log(`[WebSocket] Client disconnected from session: ${sessionId}`);
|
||||
|
||||
// Stop pipe-pane
|
||||
if (session.captureProcess) {
|
||||
spawn("tmux", ["pipe-pane", "-t", sessionId]);
|
||||
session.captureProcess.kill();
|
||||
}
|
||||
|
||||
sessions.delete(sessionId);
|
||||
});
|
||||
|
||||
// Send initial output (capture recent history)
|
||||
spawn("tmux", ["capture-pane", "-t", sessionId, "-p", "-S", "-100"])
|
||||
.stdout?.on("data", (data: Buffer) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(data.toString("utf-8"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = parseInt(process.env.WS_PORT ?? "3001", 10);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[WebSocket] Terminal server listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on("SIGINT", () => {
|
||||
console.log("[WebSocket] Shutting down...");
|
||||
wss.close();
|
||||
server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
108
pnpm-lock.yaml
108
pnpm-lock.yaml
|
|
@ -460,6 +460,9 @@ importers:
|
|||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
ws:
|
||||
specifier: ^8.19.0
|
||||
version: 8.19.0
|
||||
devDependencies:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.0.0
|
||||
|
|
@ -476,9 +479,15 @@ 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))
|
||||
concurrently:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
jsdom:
|
||||
specifier: ^25.0.0
|
||||
version: 25.0.1
|
||||
|
|
@ -1626,6 +1635,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}
|
||||
|
|
@ -1931,6 +1943,10 @@ packages:
|
|||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
|
@ -1963,6 +1979,11 @@ packages:
|
|||
langchain: '>=0.2.11'
|
||||
openai: '>=4.50.0'
|
||||
|
||||
concurrently@9.2.1:
|
||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
console-table-printer@2.15.0:
|
||||
resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==}
|
||||
|
||||
|
|
@ -2226,6 +2247,10 @@ packages:
|
|||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-east-asian-width@1.4.0:
|
||||
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2761,6 +2786,10 @@ packages:
|
|||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-package-path@4.0.3:
|
||||
resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==}
|
||||
engines: {node: '>= 12'}
|
||||
|
|
@ -2821,6 +2850,10 @@ packages:
|
|||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shell-quote@1.8.3:
|
||||
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
|
|
@ -2885,6 +2918,10 @@ packages:
|
|||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
|
|
@ -2952,6 +2989,10 @@ packages:
|
|||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
ts-api-utils@2.4.0:
|
||||
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||
engines: {node: '>=18.12'}
|
||||
|
|
@ -3219,6 +3260,10 @@ packages:
|
|||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ws@8.19.0:
|
||||
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
|
@ -3238,6 +3283,10 @@ packages:
|
|||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
|
|
@ -3246,6 +3295,14 @@ packages:
|
|||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -4186,6 +4243,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
|
||||
|
|
@ -4543,6 +4604,12 @@ snapshots:
|
|||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
|
@ -4582,6 +4649,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
concurrently@9.2.1:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
rxjs: 7.8.2
|
||||
shell-quote: 1.8.3
|
||||
supports-color: 8.1.1
|
||||
tree-kill: 1.2.2
|
||||
yargs: 17.7.2
|
||||
|
||||
console-table-printer@2.15.0:
|
||||
dependencies:
|
||||
simple-wcswidth: 1.1.2
|
||||
|
|
@ -4863,6 +4939,8 @@ snapshots:
|
|||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-east-asian-width@1.4.0: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
|
|
@ -5348,6 +5426,8 @@ snapshots:
|
|||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-package-path@4.0.3:
|
||||
dependencies:
|
||||
path-root: 0.1.1
|
||||
|
|
@ -5450,6 +5530,8 @@ snapshots:
|
|||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shell-quote@1.8.3: {}
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
|
@ -5503,6 +5585,10 @@ snapshots:
|
|||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-color@8.1.1:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tailwindcss@4.1.18: {}
|
||||
|
|
@ -5550,6 +5636,8 @@ snapshots:
|
|||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
|
@ -5818,16 +5906,36 @@ snapshots:
|
|||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
ws@8.19.0: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue