diff --git a/README.md b/README.md index e470e18ed..569fb85bc 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ This fork is designed to keep up a faster rate of development customised to my n Adds full provider adapters (server managers, service layers, runtime layers) for agents that are not yet on the upstream roadmap: -| Provider | What's included | -| ----------- | ------------------------------------------------------------------------- | -| Amp | Adapter + `ampServerManager` for headless Amp sessions | -| Copilot | Adapter + CLI binary resolution + text generation layer | -| Cursor | Adapter + ACP probe integration + usage tracking | -| Gemini CLI | Adapter + `geminiCliServerManager` with full test coverage | -| Kilo | Adapter + `kiloServerManager` + OpenCode-style server URL config | -| OpenCode | Adapter + `opencodeServerManager` with hostname/port/workspace config | -| Claude Code | Full adapter with permission mode, thinking token limits, and SDK typings | +| Provider | What's included | +| ----------- | -------------------------------------------------------------------------------------------- | +| Amp | Adapter + `ampServerManager` for headless Amp sessions | +| Copilot | Adapter + CLI binary resolution + text generation layer | +| Cursor | Adapter + ACP probe integration + usage tracking | +| Gemini CLI | Adapter + `geminiCliServerManager` with full test coverage | +| Kilo | Modular adapter (`kilo/`) + HTTP/SSE via OpenCode SDK + dynamic port allocation + Basic Auth | +| OpenCode | Modular adapter (`opencode/`) + HTTP/SSE via OpenCode SDK + session resume + Basic Auth | +| Claude Code | Full adapter with permission mode, thinking token limits, and SDK typings | ### UX enhancements @@ -83,7 +83,7 @@ bun run dev ## Supported agents - [Codex CLI](https://github.com/openai/codex) (requires v0.37.0 or later) -- [Claude Code](https://github.com/anthropics/claude-code) — **not yet working in the desktop app** +- [Claude Code](https://github.com/anthropics/claude-code) - [Cursor](https://cursor.sh) - [Copilot](https://github.com/features/copilot) - [Gemini CLI](https://github.com/google-gemini/gemini-cli) diff --git a/apps/web/package.json b/apps/web/package.json index 7d4458031..d5b9c7c72 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,7 +31,6 @@ "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "effect": "catalog:", - "ghostty-web": "^0.4.0", "lexical": "^0.41.0", "lucide-react": "^0.564.0", "react": "^19.0.0", diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index e4a16bc91..29395240b 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -11,7 +11,6 @@ import { TerminalSquareIcon, MessageSquareIcon, StopCircleIcon, - GhostIcon, } from "lucide-react"; import { useNavigate } from "@tanstack/react-router"; import { @@ -73,8 +72,6 @@ interface CommandPaletteProps { onToggleRuntimeMode: () => void; onInterrupt: () => void | Promise; onRunProjectScript?: ((script: ProjectScript) => void | Promise) | undefined; - ghosttySplitOpen?: boolean; - onToggleGhosttySplit?: () => void; } const GROUP_LABELS: Record = { @@ -158,8 +155,6 @@ export default function CommandPalette({ onToggleRuntimeMode, onInterrupt, onRunProjectScript, - ghosttySplitOpen, - onToggleGhosttySplit, }: CommandPaletteProps) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); @@ -303,18 +298,6 @@ export default function CommandPalette({ onSelect: onSplitTerminal, }); - if (onToggleGhosttySplit) { - items.push({ - id: "action:toggle-ghostty-split", - group: "actions", - title: ghosttySplitOpen ? "Hide Ghostty split view" : "Show Ghostty split view", - subtitle: "Toggle the libghostty-powered split terminal (WASM)", - keywords: ["ghostty", "split", "wasm", "libghostty", "terminal"], - icon: , - onSelect: onToggleGhosttySplit, - }); - } - items.push({ id: "action:toggle-interaction-mode", group: "actions", @@ -374,8 +357,6 @@ export default function CommandPalette({ onToggleInteractionMode, onToggleRuntimeMode, onToggleTerminal, - onToggleGhosttySplit, - ghosttySplitOpen, openOrCreateThread, navigate, runtimeMode, diff --git a/apps/web/src/components/GhosttyTerminalSplitView.tsx b/apps/web/src/components/GhosttyTerminalSplitView.tsx deleted file mode 100644 index b9cd898e8..000000000 --- a/apps/web/src/components/GhosttyTerminalSplitView.tsx +++ /dev/null @@ -1,705 +0,0 @@ -/** - * Mini Terminal Split View powered by libghostty (via ghostty-web WASM). - * - * This component demonstrates embedding Ghostty's battle-tested VT100 parser - * (compiled to WebAssembly from the original Zig source) into a React-based - * split terminal pane layout. - * - * Instead of xterm.js's JavaScript-based terminal emulation, this uses - * libghostty-vt — the same core used by the native Ghostty terminal app — - * providing superior Unicode handling, SIMD-optimized parsing, and proper - * support for complex scripts (Devanagari, Arabic, etc.). - * - * Architecture: - * ghostty-web (npm) → WASM (libghostty-vt compiled from Zig) → Canvas renderer - * React component → manages split pane layout, focus, resize - * Server PTY → WebSocket → ghostty-web Terminal.write() - */ - -import { init as initGhostty, Terminal, FitAddon, type ITheme } from "ghostty-web"; -import { - GripVertical, - Maximize2, - Minimize2, - Plus, - Split, - Terminal as TerminalIcon, - X, -} from "lucide-react"; -import { - type PointerEvent as ReactPointerEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { readNativeApi } from "~/nativeApi"; -import type { ThreadId } from "@t3tools/contracts"; -import { - contrastSafeTerminalColor, - normalizeAccentColor, - resolveAccentColorRgba, -} from "../accentColor"; -import { resolveTerminalFontFamily } from "../lib/terminalFont"; - -// ─── Ghostty WASM Initialization ──────────────────────────────────────────── -// ghostty-web requires a one-time async init to load the WASM module. -// We track the promise globally so multiple components share the same load. - -let ghosttyInitPromise: Promise | null = null; -let ghosttyReady = false; - -function ensureGhosttyInit(): Promise { - if (ghosttyReady) return Promise.resolve(); - if (!ghosttyInitPromise) { - ghosttyInitPromise = initGhostty().then(() => { - ghosttyReady = true; - }); - } - return ghosttyInitPromise; -} - -// ─── Theme ────────────────────────────────────────────────────────────────── - -const DARK_BG_HEX = "#0e1218"; -const LIGHT_BG_HEX = "#ffffff"; - -function clampByte(v: number): number { - return Math.min(255, Math.max(0, Math.round(v))); -} - -function mixHexWithWhite(hex: string, ratio: number): string { - const r = Number.parseInt(hex.slice(1, 3), 16); - const g = Number.parseInt(hex.slice(3, 5), 16); - const b = Number.parseInt(hex.slice(5, 7), 16); - const mr = clampByte(r + (255 - r) * ratio); - const mg = clampByte(g + (255 - g) * ratio); - const mb = clampByte(b + (255 - b) * ratio); - return `#${mr.toString(16).padStart(2, "0")}${mg.toString(16).padStart(2, "0")}${mb.toString(16).padStart(2, "0")}`; -} - -function ghosttyThemeFromApp(): ITheme { - const isDark = document.documentElement.classList.contains("dark"); - const bodyStyles = getComputedStyle(document.body); - const rootStyles = getComputedStyle(document.documentElement); - const background = - bodyStyles.backgroundColor || (isDark ? "rgb(14, 18, 24)" : "rgb(255, 255, 255)"); - const foreground = bodyStyles.color || (isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)"); - const accentColor = normalizeAccentColor(rootStyles.getPropertyValue("--accent-color")); - const bgHex = isDark ? DARK_BG_HEX : LIGHT_BG_HEX; - const terminalBlue = contrastSafeTerminalColor(accentColor, bgHex); - const brightMix = isDark ? 0.3 : 0.18; - const terminalBrightBlue = contrastSafeTerminalColor( - mixHexWithWhite(accentColor, brightMix), - bgHex, - ); - const selectionBackground = resolveAccentColorRgba(accentColor, isDark ? 0.3 : 0.22); - - if (isDark) { - return { - background, - foreground, - cursor: terminalBrightBlue, - selectionBackground, - black: "rgb(24, 30, 38)", - red: "rgb(255, 122, 142)", - green: "rgb(134, 231, 149)", - yellow: "rgb(244, 205, 114)", - blue: terminalBlue, - magenta: "rgb(208, 176, 255)", - cyan: "rgb(124, 232, 237)", - white: "rgb(210, 218, 230)", - brightBlack: "rgb(110, 120, 136)", - brightRed: "rgb(255, 168, 180)", - brightGreen: "rgb(176, 245, 186)", - brightYellow: "rgb(255, 224, 149)", - brightBlue: terminalBrightBlue, - brightMagenta: "rgb(229, 203, 255)", - brightCyan: "rgb(167, 244, 247)", - brightWhite: "rgb(244, 247, 252)", - }; - } - - return { - background, - foreground, - cursor: terminalBlue, - selectionBackground, - black: "rgb(44, 53, 66)", - red: "rgb(191, 70, 87)", - green: "rgb(60, 126, 86)", - yellow: "rgb(146, 112, 35)", - blue: terminalBlue, - magenta: "rgb(132, 86, 149)", - cyan: "rgb(53, 127, 141)", - white: "rgb(210, 215, 223)", - brightBlack: "rgb(112, 123, 140)", - brightRed: "rgb(212, 95, 112)", - brightGreen: "rgb(85, 148, 111)", - brightYellow: "rgb(173, 133, 45)", - brightBlue: terminalBrightBlue, - brightMagenta: "rgb(153, 107, 172)", - brightCyan: "rgb(70, 149, 164)", - brightWhite: "rgb(236, 240, 246)", - }; -} - -// ─── Constants ────────────────────────────────────────────────────────────── - -const MIN_PANE_WIDTH_PX = 120; -const MAX_PANES = 4; -const MIN_CONTAINER_HEIGHT = 200; -const MAX_CONTAINER_HEIGHT = 600; -const DEFAULT_CONTAINER_HEIGHT = 350; - -// ─── Types ────────────────────────────────────────────────────────────────── - -interface SplitPane { - id: string; - terminalId: string; -} - -// ─── Single Ghostty Terminal Pane ─────────────────────────────────────────── - -interface GhosttyPaneProps { - threadId: ThreadId; - terminalId: string; - cwd: string; - runtimeEnv?: Record; - isActive: boolean; - onFocus: () => void; - onClose: () => void; - resizeEpoch: number; - containerHeight: number; -} - -function GhosttyPane({ - threadId, - terminalId, - cwd, - runtimeEnv, - isActive, - onFocus, - onClose, - resizeEpoch, - containerHeight, -}: GhosttyPaneProps) { - const containerRef = useRef(null); - const terminalRef = useRef(null); - const fitAddonRef = useRef(null); - const [status, setStatus] = useState<"loading" | "ready" | "error">("loading"); - - // Initialize ghostty-web terminal - useEffect(() => { - const mount = containerRef.current; - if (!mount) return; - - let disposed = false; - - const setup = async () => { - try { - // Ensure WASM is loaded - await ensureGhosttyInit(); - if (disposed) return; - - const fitAddon = new FitAddon(); - const terminal = new Terminal({ - cursorBlink: true, - fontSize: 12, - scrollback: 5_000, - fontFamily: resolveTerminalFontFamily(), - theme: ghosttyThemeFromApp(), - }); - - terminal.loadAddon(fitAddon); - terminal.open(mount); - fitAddon.fit(); - - terminalRef.current = terminal; - fitAddonRef.current = fitAddon; - - if (disposed) { - terminal.dispose(); - return; - } - - setStatus("ready"); - - // Connect to backend PTY - const api = readNativeApi(); - if (!api) return; - - // Handle user input → send to PTY - const inputDisposable = terminal.onData((data) => { - void api.terminal.write({ threadId, terminalId, data }).catch((err) => { - terminal.write( - `\r\n[ghostty] ${err instanceof Error ? err.message : "Write failed"}\r\n`, - ); - }); - }); - - // Listen for PTY output → write to terminal - const unsubscribe = api.terminal.onEvent((event) => { - if (event.threadId !== threadId || event.terminalId !== terminalId) return; - const activeTerminal = terminalRef.current; - if (!activeTerminal) return; - - switch (event.type) { - case "output": - activeTerminal.write(event.data); - break; - case "started": - case "restarted": - activeTerminal.write("\u001bc"); - if (event.snapshot.history.length > 0) { - activeTerminal.write(event.snapshot.history); - } - break; - case "cleared": - activeTerminal.clear(); - activeTerminal.write("\u001bc"); - break; - case "error": - activeTerminal.write(`\r\n[ghostty] ${event.message}\r\n`); - break; - case "exited": { - const details = [ - typeof event.exitCode === "number" ? `code ${event.exitCode}` : null, - typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null, - ] - .filter((v): v is string => v !== null) - .join(", "); - activeTerminal.write( - `\r\n[ghostty] ${details ? `Process exited (${details})` : "Process exited"}\r\n`, - ); - break; - } - } - }); - - // Open the terminal session on the server - try { - fitAddon.fit(); - const snapshot = await api.terminal.open({ - threadId, - terminalId, - cwd, - cols: terminal.cols, - rows: terminal.rows, - ...(runtimeEnv ? { env: runtimeEnv } : {}), - }); - if (disposed) return; - terminal.write("\u001bc"); - if (snapshot.history.length > 0) { - terminal.write(snapshot.history); - } - if (isActive) { - window.requestAnimationFrame(() => terminal.focus()); - } - } catch (err) { - if (disposed) return; - terminal.write( - `\r\n[ghostty] ${err instanceof Error ? err.message : "Failed to open terminal"}\r\n`, - ); - } - - // Theme observer - const themeObserver = new MutationObserver(() => { - const t = terminalRef.current; - if (!t) return; - t.options.theme = ghosttyThemeFromApp(); - }); - themeObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class", "style"], - }); - - // Cleanup on unmount - return () => { - disposed = true; - unsubscribe(); - inputDisposable.dispose(); - themeObserver.disconnect(); - terminalRef.current = null; - fitAddonRef.current = null; - terminal.dispose(); - }; - } catch (err) { - if (!disposed) { - setStatus("error"); - console.error("[ghostty-web] Init failed:", err); - } - } - }; - - const cleanupPromise = setup(); - - return () => { - disposed = true; - void cleanupPromise?.then((cleanup) => cleanup?.()); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cwd, runtimeEnv, terminalId, threadId]); - - // Handle focus - useEffect(() => { - if (!isActive) return; - const terminal = terminalRef.current; - if (!terminal) return; - const frame = window.requestAnimationFrame(() => terminal.focus()); - return () => window.cancelAnimationFrame(frame); - }, [isActive]); - - // Handle resize - useEffect(() => { - const api = readNativeApi(); - const terminal = terminalRef.current; - const fitAddon = fitAddonRef.current; - if (!api || !terminal || !fitAddon) return; - - const frame = window.requestAnimationFrame(() => { - fitAddon.fit(); - terminal.scrollToBottom(); - void api.terminal - .resize({ - threadId, - terminalId, - cols: terminal.cols, - rows: terminal.rows, - }) - .catch(() => undefined); - }); - return () => window.cancelAnimationFrame(frame); - }, [containerHeight, resizeEpoch, terminalId, threadId]); - - return ( -
- {/* Pane header */} -
-
- - - {status === "loading" ? "Loading WASM…" : status === "error" ? "Error" : "ghostty"} - - {status === "ready" && ( - - libghostty - - )} -
- -
- - {/* Terminal canvas area */} -
-
- ); -} - -// ─── Split Divider ────────────────────────────────────────────────────────── - -interface SplitDividerProps { - onPointerDown: (e: ReactPointerEvent) => void; - onPointerMove: (e: ReactPointerEvent) => void; - onPointerUp: (e: ReactPointerEvent) => void; -} - -function SplitDivider({ onPointerDown, onPointerMove, onPointerUp }: SplitDividerProps) { - return ( -
- -
- ); -} - -// ─── Main Split View Component ────────────────────────────────────────────── - -export interface GhosttyTerminalSplitViewProps { - threadId: ThreadId; - cwd: string; - runtimeEnv?: Record; -} - -let nextPaneCounter = 0; -function createPaneId(): string { - nextPaneCounter += 1; - return `ghostty-pane-${nextPaneCounter}-${Date.now().toString(36)}`; -} - -export default function GhosttyTerminalSplitView({ - threadId, - cwd, - runtimeEnv, -}: GhosttyTerminalSplitViewProps) { - const [panes, setPanes] = useState(() => { - const id = createPaneId(); - return [{ id, terminalId: `ghostty-${id}` }]; - }); - const [activePaneId, setActivePaneId] = useState(() => panes[0]!.id); - const [containerHeight, setContainerHeight] = useState(DEFAULT_CONTAINER_HEIGHT); - const [isCollapsed, setIsCollapsed] = useState(false); - const [resizeEpoch, setResizeEpoch] = useState(0); - const containerRef = useRef(null); - const resizeStateRef = useRef<{ - pointerId: number; - startY: number; - startHeight: number; - } | null>(null); - - const canSplit = panes.length < MAX_PANES; - - // ─── Pane management ──────────────────────────────────────────────── - - const handleSplit = useCallback(() => { - if (!canSplit) return; - const id = createPaneId(); - const newPane: SplitPane = { id, terminalId: `ghostty-${id}` }; - setPanes((prev) => [...prev, newPane]); - setActivePaneId(id); - setResizeEpoch((e) => e + 1); - }, [canSplit]); - - const handleClosePane = useCallback( - (paneId: string) => { - setPanes((prev) => { - if (prev.length <= 1) return prev; // keep at least one - const next = prev.filter((p) => p.id !== paneId); - if (activePaneId === paneId) { - setActivePaneId(next[0]?.id ?? ""); - } - setResizeEpoch((e) => e + 1); - return next; - }); - }, - [activePaneId], - ); - - // ─── Vertical resize (container height) ───────────────────────────── - - const handleResizePointerDown = useCallback( - (e: ReactPointerEvent) => { - if (e.button !== 0) return; - e.preventDefault(); - e.currentTarget.setPointerCapture(e.pointerId); - resizeStateRef.current = { - pointerId: e.pointerId, - startY: e.clientY, - startHeight: containerHeight, - }; - }, - [containerHeight], - ); - - const handleResizePointerMove = useCallback((e: ReactPointerEvent) => { - const state = resizeStateRef.current; - if (!state || state.pointerId !== e.pointerId) return; - e.preventDefault(); - const nextHeight = Math.min( - MAX_CONTAINER_HEIGHT, - Math.max(MIN_CONTAINER_HEIGHT, state.startHeight + (state.startY - e.clientY)), - ); - setContainerHeight(nextHeight); - }, []); - - const handleResizePointerUp = useCallback((e: ReactPointerEvent) => { - const state = resizeStateRef.current; - if (!state || state.pointerId !== e.pointerId) return; - resizeStateRef.current = null; - if (e.currentTarget.hasPointerCapture(e.pointerId)) { - e.currentTarget.releasePointerCapture(e.pointerId); - } - setResizeEpoch((v) => v + 1); - }, []); - - // ─── Window resize ────────────────────────────────────────────────── - - useEffect(() => { - const onResize = () => setResizeEpoch((v) => v + 1); - window.addEventListener("resize", onResize); - return () => window.removeEventListener("resize", onResize); - }, []); - - // ─── Keyboard shortcut for splitting ──────────────────────────────── - - useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - // Cmd/Ctrl + Shift + D to split - if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "d") { - e.preventDefault(); - handleSplit(); - } - }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [handleSplit]); - - // ─── Pane label map ───────────────────────────────────────────────── - - const paneLabelMap = useMemo( - () => new Map(panes.map((p, i) => [p.id, `Pane ${i + 1}`])), - [panes], - ); - - if (isCollapsed) { - return ( -
- -
- ); - } - - return ( -
- {/* Resize handle (top edge) */} -
- - {/* Toolbar */} -
-
- - - Ghostty Split View - - - libghostty - -
- -
- {/* Pane tabs */} - {panes.length > 1 && - panes.map((pane) => ( - - ))} - -
- - {/* Split button */} - - - {/* Add new pane */} - - - {/* Collapse */} - -
-
- - {/* Split pane container */} -
- {panes.map((pane, index) => ( -
- {index > 0 && ( - {}} - onPointerMove={() => {}} - onPointerUp={() => {}} - /> - )} - setActivePaneId(pane.id)} - onClose={() => handleClosePane(pane.id)} - resizeEpoch={resizeEpoch} - containerHeight={containerHeight} - /> -
- ))} -
-
- ); -} diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx index 50ab6372d..860fd36b9 100644 --- a/apps/web/src/components/Icons.tsx +++ b/apps/web/src/components/Icons.tsx @@ -183,12 +183,6 @@ export const FleetIcon: Icon = (props) => ( ); -export const GhosttyIcon: Icon = (props) => ( - - - -); - export const OpenAI: Icon = (props) => ( diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 9cbaf1b8b..56b138d33 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -30,12 +30,7 @@ export default defineConfig({ tailwindcss(), ], optimizeDeps: { - include: [ - "@pierre/diffs", - "@pierre/diffs/react", - "@pierre/diffs/worker/worker.js", - "ghostty-web", - ], + include: ["@pierre/diffs", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js"], }, define: { // In dev mode, tell the web app where the WebSocket server lives diff --git a/bun.lock b/bun.lock index 58486318c..e11de28d3 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/desktop": { "name": "@t3tools/desktop", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "effect": "catalog:", "electron": "40.6.0", @@ -43,7 +43,7 @@ }, "apps/server": { "name": "t3", - "version": "0.0.2", + "version": "0.0.3", "bin": { "t3": "./dist/index.mjs", }, @@ -76,7 +76,7 @@ }, "apps/web": { "name": "@t3tools/web", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "@base-ui/react": "^1.2.0", "@dnd-kit/core": "^6.3.1", @@ -95,7 +95,6 @@ "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "effect": "catalog:", - "ghostty-web": "^0.4.0", "lexical": "^0.41.0", "lucide-react": "^0.564.0", "react": "^19.0.0", @@ -127,7 +126,7 @@ }, "packages/contracts": { "name": "@t3tools/contracts", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "effect": "catalog:", }, @@ -1131,8 +1130,6 @@ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], - "ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],